diff --git a/Components/Daisy/Actions/DButton.razor b/Components/Daisy/Actions/DButton.razor
new file mode 100644
index 0000000..5e70cc2
--- /dev/null
+++ b/Components/Daisy/Actions/DButton.razor
@@ -0,0 +1,74 @@
+@inherits DComponentBase
+
+@* daisyUI Button: https://daisyui.com/components/button/ . Renders an when Href is set. *@
+@if (Href is null)
+{
+
+}
+else
+{
+
+ @ChildContent
+
+}
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public DColor Color { get; set; }
+ [Parameter] public DButtonVariant Variant { get; set; }
+ [Parameter] public DSize Size { get; set; }
+ [Parameter] public DButtonShape Shape { get; set; }
+ /// Pressed look (btn-active); the neo theme renders it inset.
+ [Parameter] public bool Active { get; set; }
+ [Parameter] public bool Disabled { get; set; }
+ [Parameter] public string Type { get; set; } = "button";
+ [Parameter] public string Href { get; set; }
+ [Parameter] public EventCallback OnClick { get; set; }
+
+ string CssClass => Cx("btn", ColorClass, VariantClass, SizeClass, ShapeClass,
+ Active ? "btn-active" : null, Disabled && Href is not null ? "btn-disabled" : null, Class);
+
+ string ColorClass => Color switch
+ {
+ DColor.Neutral => "btn-neutral",
+ DColor.Primary => "btn-primary",
+ DColor.Secondary => "btn-secondary",
+ DColor.Accent => "btn-accent",
+ DColor.Info => "btn-info",
+ DColor.Success => "btn-success",
+ DColor.Warning => "btn-warning",
+ DColor.Error => "btn-error",
+ _ => null
+ };
+
+ string VariantClass => Variant switch
+ {
+ DButtonVariant.Outline => "btn-outline",
+ DButtonVariant.Dash => "btn-dash",
+ DButtonVariant.Soft => "btn-soft",
+ DButtonVariant.Ghost => "btn-ghost",
+ DButtonVariant.Link => "btn-link",
+ _ => null
+ };
+
+ string SizeClass => Size switch
+ {
+ DSize.Xs => "btn-xs",
+ DSize.Sm => "btn-sm",
+ DSize.Lg => "btn-lg",
+ DSize.Xl => "btn-xl",
+ _ => null
+ };
+
+ string ShapeClass => Shape switch
+ {
+ DButtonShape.Wide => "btn-wide",
+ DButtonShape.Block => "btn-block",
+ DButtonShape.Square => "btn-square",
+ DButtonShape.Circle => "btn-circle",
+ _ => null
+ };
+}
diff --git a/Components/Daisy/Actions/DButtonEnums.cs b/Components/Daisy/Actions/DButtonEnums.cs
new file mode 100644
index 0000000..58311ab
--- /dev/null
+++ b/Components/Daisy/Actions/DButtonEnums.cs
@@ -0,0 +1,20 @@
+namespace decePubClient.Components.Daisy.Actions;
+
+public enum DButtonVariant
+{
+ Default,
+ Outline,
+ Dash,
+ Soft,
+ Ghost,
+ Link
+}
+
+public enum DButtonShape
+{
+ Default,
+ Wide,
+ Block,
+ Square,
+ Circle
+}
diff --git a/Components/Daisy/Actions/DDropdown.razor b/Components/Daisy/Actions/DDropdown.razor
new file mode 100644
index 0000000..097e017
--- /dev/null
+++ b/Components/Daisy/Actions/DDropdown.razor
@@ -0,0 +1,66 @@
+@inherits DComponentBase
+
+@* daisyUI Dropdown: https://daisyui.com/components/dropdown/ . The open state lives in C# (@bind-IsOpen):
+ dropdown-open shows the content and dropdown-close keeps the focus-within rule from showing it while closed.
+ While open, a transparent full-screen layer under the content closes it on an outside click; Escape closes it
+ too. With Hover the CSS hover rule opens it instead. *@
+
+
+ @Trigger
+
+ @if (IsOpen)
+ {
+
+ }
+
+ @ChildContent
+
+
+
+@code {
+ [Parameter] public RenderFragment Trigger { get; set; }
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public bool IsOpen { get; set; }
+ [Parameter] public EventCallback IsOpenChanged { get; set; }
+ [Parameter] public DDropdownPlacement Placement { get; set; }
+ [Parameter] public DDropdownAlign Align { get; set; }
+ /// Open on hover (dropdown-hover) as well as on click.
+ [Parameter] public bool Hover { get; set; }
+ /// Close after any click inside the content, e.g. on a menu item.
+ [Parameter] public bool CloseOnContentClick { get; set; }
+ [Parameter] public string TriggerClass { get; set; }
+ [Parameter] public string ContentClass { get; set; }
+
+ string CssClass => Cx("dropdown", PlacementClass, AlignClass,
+ IsOpen ? "dropdown-open" : Hover ? "dropdown-hover" : "dropdown-close", Class);
+
+ string PlacementClass => Placement switch
+ {
+ DDropdownPlacement.Top => "dropdown-top",
+ DDropdownPlacement.Left => "dropdown-left",
+ DDropdownPlacement.Right => "dropdown-right",
+ _ => null
+ };
+
+ string AlignClass => Align switch
+ {
+ DDropdownAlign.Center => "dropdown-center",
+ DDropdownAlign.End => "dropdown-end",
+ _ => null
+ };
+
+ Task Toggle() => SetOpen(!IsOpen);
+
+ Task Close() => SetOpen(false);
+
+ Task OnContentClick() => CloseOnContentClick ? SetOpen(false) : Task.CompletedTask;
+
+ Task OnKeyDown(KeyboardEventArgs e) => e.Key == "Escape" && IsOpen ? SetOpen(false) : Task.CompletedTask;
+
+ async Task SetOpen(bool isOpen)
+ {
+ IsOpen = isOpen;
+ await IsOpenChanged.InvokeAsync(isOpen);
+ }
+}
diff --git a/Components/Daisy/Actions/DDropdownEnums.cs b/Components/Daisy/Actions/DDropdownEnums.cs
new file mode 100644
index 0000000..3053e65
--- /dev/null
+++ b/Components/Daisy/Actions/DDropdownEnums.cs
@@ -0,0 +1,18 @@
+namespace decePubClient.Components.Daisy.Actions;
+
+/// Which side of the trigger the content opens on; Bottom is daisyUI's default.
+public enum DDropdownPlacement
+{
+ Bottom,
+ Top,
+ Left,
+ Right
+}
+
+/// How the content lines up with the trigger along that side; Start is daisyUI's default.
+public enum DDropdownAlign
+{
+ Start,
+ Center,
+ End
+}
diff --git a/Components/Daisy/Actions/DFab.razor b/Components/Daisy/Actions/DFab.razor
new file mode 100644
index 0000000..b0ccec4
--- /dev/null
+++ b/Components/Daisy/Actions/DFab.razor
@@ -0,0 +1,53 @@
+@inherits DComponentBase
+
+@* daisyUI FAB: https://daisyui.com/components/fab/ . Opening is daisyUI's own :focus-within rule (there is no
+ open-state class): the trigger is the focusable first child, and focus moving out of the FAB closes it. The order
+ of the children matters to daisyUI: trigger, then Close or MainAction, then the speed-dial items (ChildContent,
+ e.g. DButton Size="DSize.Lg" Shape="DButtonShape.Circle", optionally inside a
with a text label). *@
+
+
+ @Trigger
+
+ @if (Close is not null)
+ {
+
@Close
+ }
+ @if (MainAction is not null)
+ {
+
@MainAction
+ }
+ @ChildContent
+
+
+@code {
+ /// Content of the button shown while closed, usually an icon.
+ [Parameter] public RenderFragment Trigger { get; set; }
+ /// Accessible name of the trigger (aria-label), since it usually holds only an icon.
+ [Parameter] public string LabelText { get; set; }
+ [Parameter] public DColor Color { get; set; } = DColor.Primary;
+ [Parameter] public string TriggerClass { get; set; }
+ /// Replaces the trigger while open; clicking it closes the FAB. Not focusable, e.g. a text label and a
+ /// <span class="btn btn-circle btn-lg">.
+ [Parameter] public RenderFragment Close { get; set; }
+ /// Replaces the trigger while open with a main action, e.g. a label and a button.
+ [Parameter] public RenderFragment MainAction { get; set; }
+ /// The speed-dial items.
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ /// Arrange the items in a quarter circle (fab-flower) instead of a column.
+ [Parameter] public bool Flower { get; set; }
+
+ string CssClass => Cx("fab", Flower ? "fab-flower" : null, Class);
+
+ string ColorClass => Color switch
+ {
+ DColor.Neutral => "btn-neutral",
+ DColor.Primary => "btn-primary",
+ DColor.Secondary => "btn-secondary",
+ DColor.Accent => "btn-accent",
+ DColor.Info => "btn-info",
+ DColor.Success => "btn-success",
+ DColor.Warning => "btn-warning",
+ DColor.Error => "btn-error",
+ _ => null
+ };
+}
diff --git a/Components/Daisy/Actions/DModal.razor b/Components/Daisy/Actions/DModal.razor
new file mode 100644
index 0000000..59a8ad6
--- /dev/null
+++ b/Components/Daisy/Actions/DModal.razor
@@ -0,0 +1,83 @@
+@inherits DComponentBase
+
+@* daisyUI Modal: https://daisyui.com/components/modal/ . Always rendered; the open state lives in C# (@bind-IsOpen)
+ and shows as modal-open. On opening, the box takes the focus (Blazor's FocusAsync) so that Escape, handled on the
+ root, closes it; the backdrop button stretches over the area outside the box and closes it on click. *@
+
+
+ @if (Heading is not null)
+ {
+
@Heading
+ }
+ @ChildContent
+ @if (Actions is not null)
+ {
+
+ @Actions
+
+ }
+
+ @if (CloseOnBackdrop)
+ {
+
+
+
+ }
+
+
+@code {
+ [Parameter] public RenderFragment Heading { get; set; }
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public RenderFragment Actions { get; set; }
+ [Parameter] public bool IsOpen { get; set; }
+ [Parameter] public EventCallback IsOpenChanged { get; set; }
+ [Parameter] public DModalPlacement Placement { get; set; }
+ /// Close on a click outside the box (renders the modal-backdrop).
+ [Parameter] public bool CloseOnBackdrop { get; set; } = true;
+ /// Text of the invisible backdrop button, read by screen readers.
+ [Parameter] public string CloseText { get; set; } = "close";
+ [Parameter] public string BoxClass { get; set; }
+
+ readonly string uid = "d" + Guid.NewGuid().ToString("N")[..8];
+ ElementReference box;
+ bool wasOpen;
+ bool focusPending;
+
+ string CssClass => Cx("modal", PlacementClass, IsOpen ? "modal-open" : null, Class);
+
+ string PlacementClass => Placement switch
+ {
+ DModalPlacement.Top => "modal-top",
+ DModalPlacement.Bottom => "modal-bottom",
+ DModalPlacement.Start => "modal-start",
+ DModalPlacement.End => "modal-end",
+ _ => null
+ };
+
+ protected override void OnParametersSet()
+ {
+ if (IsOpen && !wasOpen)
+ focusPending = true;
+ wasOpen = IsOpen;
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (!focusPending)
+ return;
+ focusPending = false;
+ await box.FocusAsync();
+ }
+
+ Task Close() => SetOpen(false);
+
+ Task OnKeyDown(KeyboardEventArgs e) => e.Key == "Escape" && IsOpen ? SetOpen(false) : Task.CompletedTask;
+
+ async Task SetOpen(bool isOpen)
+ {
+ IsOpen = isOpen;
+ wasOpen = isOpen;
+ await IsOpenChanged.InvokeAsync(isOpen);
+ }
+}
diff --git a/Components/Daisy/Actions/DModalEnums.cs b/Components/Daisy/Actions/DModalEnums.cs
new file mode 100644
index 0000000..0182e4a
--- /dev/null
+++ b/Components/Daisy/Actions/DModalEnums.cs
@@ -0,0 +1,11 @@
+namespace decePubClient.Components.Daisy.Actions;
+
+/// Where the box sits on the screen; Middle is daisyUI's default.
+public enum DModalPlacement
+{
+ Middle,
+ Top,
+ Bottom,
+ Start,
+ End
+}
diff --git a/Components/Daisy/Actions/DSwap.razor b/Components/Daisy/Actions/DSwap.razor
new file mode 100644
index 0000000..ff12960
--- /dev/null
+++ b/Components/Daisy/Actions/DSwap.razor
@@ -0,0 +1,35 @@
+@inherits DComponentBase
+
+@* daisyUI Swap: https://daisyui.com/components/swap/ . A label around daisyUI's hidden checkbox: clicking anywhere on
+ it toggles the checkbox, whose state is bound to Active (@bind-Active); On shows while checked, Off otherwise. *@
+
+
+@code {
+ [Parameter] public RenderFragment On { get; set; }
+ [Parameter] public RenderFragment Off { get; set; }
+ [Parameter] public bool Active { get; set; }
+ [Parameter] public EventCallback ActiveChanged { get; set; }
+ [Parameter] public DSwapEffect Effect { get; set; }
+ [Parameter] public bool Disabled { get; set; }
+ /// Accessible name of the checkbox (aria-label), for swaps that show only icons.
+ [Parameter] public string LabelText { get; set; }
+
+ string CssClass => Cx("swap", EffectClass, Disabled ? "cursor-not-allowed opacity-50" : null, Class);
+
+ string EffectClass => Effect switch
+ {
+ DSwapEffect.Rotate => "swap-rotate",
+ DSwapEffect.Flip => "swap-flip",
+ _ => null
+ };
+
+ async Task OnChange(ChangeEventArgs e)
+ {
+ Active = e.Value is true;
+ await ActiveChanged.InvokeAsync(Active);
+ }
+}
diff --git a/Components/Daisy/Actions/DSwapEnums.cs b/Components/Daisy/Actions/DSwapEnums.cs
new file mode 100644
index 0000000..1942352
--- /dev/null
+++ b/Components/Daisy/Actions/DSwapEnums.cs
@@ -0,0 +1,8 @@
+namespace decePubClient.Components.Daisy.Actions;
+
+public enum DSwapEffect
+{
+ None,
+ Rotate,
+ Flip
+}
diff --git a/Components/Daisy/Actions/DThemeController.razor b/Components/Daisy/Actions/DThemeController.razor
new file mode 100644
index 0000000..c26ce84
--- /dev/null
+++ b/Components/Daisy/Actions/DThemeController.razor
@@ -0,0 +1,27 @@
+@inherits DComponentBase
+
+@* daisyUI Theme controller: https://daisyui.com/components/theme-controller/ . While checked, the page uses the theme
+ named in Value (daisyUI's :root:has(.theme-controller:checked) rule). The app itself sets data-theme from C#, so
+ this exists for completeness. Style it with class="toggle", "checkbox", "radio", "btn" and so on. For a radio
+ group, pass the same name attribute to each and set Checked from one shared value: a radio raises no change event
+ when another one unchecks it. *@
+
+
+@code {
+ /// A daisyUI theme name, e.g. "neo-dark".
+ [Parameter] public string Value { get; set; }
+ [Parameter] public bool Checked { get; set; }
+ [Parameter] public EventCallback CheckedChanged { get; set; }
+ /// Render a radio instead of a checkbox.
+ [Parameter] public bool Radio { get; set; }
+
+ string CssClass => Cx("theme-controller", Class);
+
+ async Task OnChange(ChangeEventArgs e)
+ {
+ // A checkbox reports its checked state; a radio only reports its value when it becomes checked.
+ Checked = Radio || e.Value is true;
+ await CheckedChanged.InvokeAsync(Checked);
+ }
+}
diff --git a/Components/Daisy/DComponentBase.cs b/Components/Daisy/DComponentBase.cs
new file mode 100644
index 0000000..4e13ab6
--- /dev/null
+++ b/Components/Daisy/DComponentBase.cs
@@ -0,0 +1,27 @@
+using Microsoft.AspNetCore.Components;
+
+namespace decePubClient.Components.Daisy;
+
+///
+/// Base of the daisyUI wrappers. A consumer's class lands in and is appended after the
+/// component's own classes; every other unmatched attribute is passed through to the root element.
+///
+public abstract class DComponentBase : ComponentBase
+{
+ [Parameter] public string Class { get; set; }
+
+ [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary AdditionalAttributes { get; set; }
+
+ protected static string Cx(params string[] classes) => DCx.Join(classes);
+}
+
+public static class DCx
+{
+ ///
+ /// Joins the non-empty class names. Every daisyUI class handed to it must be written out whole somewhere in the
+ /// source, e.g. a switch arm DColor.Primary => "btn-primary", never assembled like "btn-" + color:
+ /// Tailwind generates only the classes it finds in .razor and .cs files.
+ ///
+ public static string Join(params string[] classes) =>
+ string.Join(' ', classes.Where(c => !string.IsNullOrWhiteSpace(c)));
+}
diff --git a/Components/Daisy/DInputBase.cs b/Components/Daisy/DInputBase.cs
new file mode 100644
index 0000000..70604be
--- /dev/null
+++ b/Components/Daisy/DInputBase.cs
@@ -0,0 +1,39 @@
+using System.Globalization;
+
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Forms;
+
+namespace decePubClient.Components.Daisy;
+
+///
+/// Base of the daisyUI form fields. It is Blazor's , so @bind-Value and
+/// EditForm validation work as with InputText; it also works outside an EditForm, as long as the value is bound.
+/// A consumer's class stays in AdditionalAttributes, and adds the
+/// EditContext's valid/invalid/modified markers to it.
+///
+public abstract class DInputBase : InputBase
+{
+ /// Whether the bound field currently has validation messages.
+ protected bool IsInvalid => EditContext != null && EditContext.GetValidationMessages(FieldIdentifier).Any();
+
+ /// The attributes for the inner input element, without class (that goes on the styled element).
+ protected IReadOnlyDictionary InputAttributes =>
+ AdditionalAttributes?.Where(a => a.Key != "class").ToDictionary(a => a.Key, a => a.Value);
+
+ protected static string Cx(params string[] classes) => DCx.Join(classes);
+
+ protected override string FormatValueAsString(TValue value) =>
+ BindConverter.FormatValue(value, CultureInfo.InvariantCulture)?.ToString();
+
+ protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage)
+ {
+ if (BindConverter.TryConvertTo(value, CultureInfo.InvariantCulture, out result))
+ {
+ validationErrorMessage = null;
+ return true;
+ }
+
+ validationErrorMessage = $"The {DisplayName ?? FieldIdentifier.FieldName} field is not valid.";
+ return false;
+ }
+}
diff --git a/Components/Daisy/DaisyEnums.cs b/Components/Daisy/DaisyEnums.cs
new file mode 100644
index 0000000..236dcb1
--- /dev/null
+++ b/Components/Daisy/DaisyEnums.cs
@@ -0,0 +1,34 @@
+namespace decePubClient.Components.Daisy;
+
+// Modifiers shared by many components. Each component maps them to its own class names in a switch whose arms are
+// whole literals (DColor.Primary => "btn-primary"), so Tailwind finds them. The first member is the default and
+// maps to no class.
+
+public enum DColor
+{
+ None,
+ Neutral,
+ Primary,
+ Secondary,
+ Accent,
+ Info,
+ Success,
+ Warning,
+ Error
+}
+
+/// daisyUI's sizes; is the default and renders no class.
+public enum DSize
+{
+ Md,
+ Xs,
+ Sm,
+ Lg,
+ Xl
+}
+
+public enum DDirection
+{
+ Horizontal,
+ Vertical
+}
diff --git a/Components/Daisy/DataDisplay/DAccordion.razor b/Components/Daisy/DataDisplay/DAccordion.razor
new file mode 100644
index 0000000..4b0bb7a
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DAccordion.razor
@@ -0,0 +1,46 @@
+@inherits DComponentBase
+
+@* daisyUI Accordion: https://daisyui.com/components/accordion/ . Instead of daisyUI's radio group, the open item is an
+ index in C# (@bind-Active, -1 = none). Each DAccordionItem registers itself through the cascading value and renders a
+ DCollapse that is open only while its index is Active. The cascading value isn't fixed, so every item re-renders
+ whenever the accordion does. *@
+
+
+ @ChildContent
+
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ /// Index of the open item, in registration order; -1 when all are closed.
+ [Parameter] public int Active { get; set; } = -1;
+ [Parameter] public EventCallback ActiveChanged { get; set; }
+ [Parameter] public DCollapseIcon Icon { get; set; }
+ /// Join the items into one block (join join-vertical, join-item).
+ [Parameter] public bool Joined { get; set; }
+
+ readonly List items = new();
+
+ string CssClass => Cx(Joined ? "join join-vertical rounded-box neo-raised" : "flex flex-col gap-2", Class);
+
+ internal int IndexOf(DAccordionItem item) => items.IndexOf(item);
+
+ internal void Add(DAccordionItem item)
+ {
+ items.Add(item);
+ StateHasChanged();
+ }
+
+ internal void Remove(DAccordionItem item)
+ {
+ items.Remove(item);
+ StateHasChanged();
+ }
+
+ internal async Task Select(int index)
+ {
+ Active = index;
+ await ActiveChanged.InvokeAsync(index);
+ StateHasChanged();
+ }
+}
diff --git a/Components/Daisy/DataDisplay/DAccordionItem.razor b/Components/Daisy/DataDisplay/DAccordionItem.razor
new file mode 100644
index 0000000..48df0c3
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DAccordionItem.razor
@@ -0,0 +1,32 @@
+@inherits DComponentBase
+@implements IDisposable
+
+@* One item of a DAccordion: a DCollapse (so collapse + neo-raised, keyboard and aria-expanded come from there) whose
+ open state is the accordion's Active index. Opening it closes the others; clicking it while open closes it. *@
+
+ @ChildContent
+
+
+@code {
+ [CascadingParameter] DAccordion Parent { get; set; }
+ [Parameter] public RenderFragment Header { get; set; }
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public bool Disabled { get; set; }
+ [Parameter] public string HeaderClass { get; set; }
+ [Parameter] public string ContentClass { get; set; }
+
+ bool IsOpen => Parent.Active >= 0 && Parent.Active == Parent.IndexOf(this);
+
+ protected override void OnInitialized()
+ {
+ if (Parent is null)
+ throw new InvalidOperationException($"{nameof(DAccordionItem)} must be inside a {nameof(DAccordion)}.");
+ Parent.Add(this);
+ }
+
+ Task OnIsOpenChanged(bool isOpen) => Parent.Select(isOpen ? Parent.IndexOf(this) : -1);
+
+ public void Dispose() => Parent?.Remove(this);
+}
diff --git a/Components/Daisy/DataDisplay/DAura.razor b/Components/Daisy/DataDisplay/DAura.razor
new file mode 100644
index 0000000..b0ba040
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DAura.razor
@@ -0,0 +1,51 @@
+@inherits DComponentBase
+
+@* daisyUI Aura: https://daisyui.com/components/aura/ . ChildContent must be exactly one element (a btn, card, …).
+ The light takes the text colour (Color, or any text-* in Class); a bg-* sets its background and duration-* its
+ speed. daisyUI advises at most one aura per page. *@
+
+ @ChildContent
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public DAuraVariant Variant { get; set; }
+ /// Colour of the light, as text-*; the child inherits it unless it sets its own text colour.
+ [Parameter] public DColor Color { get; set; }
+ [Parameter] public DSize Size { get; set; }
+
+ string CssClass => Cx("aura", VariantClass, ColorClass, SizeClass, Class);
+
+ string VariantClass => Variant switch
+ {
+ DAuraVariant.Dual => "aura-dual",
+ DAuraVariant.Rainbow => "aura-rainbow",
+ DAuraVariant.Holo => "aura-holo",
+ DAuraVariant.Gold => "aura-gold",
+ DAuraVariant.Silver => "aura-silver",
+ DAuraVariant.Glow => "aura-glow",
+ _ => null
+ };
+
+ string ColorClass => Color switch
+ {
+ DColor.Neutral => "text-neutral",
+ DColor.Primary => "text-primary",
+ DColor.Secondary => "text-secondary",
+ DColor.Accent => "text-accent",
+ DColor.Info => "text-info",
+ DColor.Success => "text-success",
+ DColor.Warning => "text-warning",
+ DColor.Error => "text-error",
+ _ => null
+ };
+
+ string SizeClass => Size switch
+ {
+ DSize.Xs => "aura-xs",
+ DSize.Sm => "aura-sm",
+ DSize.Lg => "aura-lg",
+ DSize.Xl => "aura-xl",
+ _ => null
+ };
+}
diff --git a/Components/Daisy/DataDisplay/DAuraEnums.cs b/Components/Daisy/DataDisplay/DAuraEnums.cs
new file mode 100644
index 0000000..45cba40
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DAuraEnums.cs
@@ -0,0 +1,12 @@
+namespace decePubClient.Components.Daisy.DataDisplay;
+
+public enum DAuraVariant
+{
+ Default,
+ Dual,
+ Rainbow,
+ Holo,
+ Gold,
+ Silver,
+ Glow
+}
diff --git a/Components/Daisy/DataDisplay/DAvatar.razor b/Components/Daisy/DataDisplay/DAvatar.razor
new file mode 100644
index 0000000..3d7a73b
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DAvatar.razor
@@ -0,0 +1,36 @@
+@inherits DComponentBase
+
+@* daisyUI Avatar: https://daisyui.com/components/avatar/ . Class goes on the inner frame, where daisyUI puts the size
+ and shape (w-16 rounded-full, mask mask-squircle) and, for a placeholder, the colours (bg-neutral
+ text-neutral-content); the other attributes go on the outer .avatar. Without Src the frame shows ChildContent. *@
+
+
+ @if (Src is not null)
+ {
+
+ }
+ else
+ {
+ @ChildContent
+ }
+
+
+
+@code {
+ [Parameter] public string Src { get; set; }
+ [Parameter] public string Alt { get; set; } = "";
+ /// Shown in the frame when there is no Src, e.g. initials with Placeholder.
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public DAvatarStatus Status { get; set; }
+ /// Centre ChildContent in the frame (avatar-placeholder); give the frame a bg-* in Class.
+ [Parameter] public bool Placeholder { get; set; }
+
+ string CssClass => Cx("avatar", StatusClass, Placeholder ? "avatar-placeholder" : null);
+
+ string StatusClass => Status switch
+ {
+ DAvatarStatus.Online => "avatar-online",
+ DAvatarStatus.Offline => "avatar-offline",
+ _ => null
+ };
+}
diff --git a/Components/Daisy/DataDisplay/DAvatarEnums.cs b/Components/Daisy/DataDisplay/DAvatarEnums.cs
new file mode 100644
index 0000000..a57a53e
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DAvatarEnums.cs
@@ -0,0 +1,8 @@
+namespace decePubClient.Components.Daisy.DataDisplay;
+
+public enum DAvatarStatus
+{
+ None,
+ Online,
+ Offline
+}
diff --git a/Components/Daisy/DataDisplay/DAvatarGroup.razor b/Components/Daisy/DataDisplay/DAvatarGroup.razor
new file mode 100644
index 0000000..3232a5b
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DAvatarGroup.razor
@@ -0,0 +1,11 @@
+@inherits DComponentBase
+
+@* daisyUI Avatar group: https://daisyui.com/components/avatar/ . Overlap the DAvatar children with a negative
+ space-x in Class, e.g. -space-x-6. *@
+
before the body (after it with
+ FigureLast); the body renders only when Heading, ChildContent or Actions is set. Give it a background in Class
+ (bg-base-100). *@
+
+ @if (Figure is not null && !FigureLast)
+ {
+ @Figure
+ }
+ @if (Heading is not null || ChildContent is not null || Actions is not null)
+ {
+
+ @if (Heading is not null)
+ {
+
@Heading
+ }
+ @ChildContent
+ @if (Actions is not null)
+ {
+
@Actions
+ }
+
+ }
+ @if (Figure is not null && FigureLast)
+ {
+ @Figure
+ }
+
+
+@code {
+ [Parameter] public RenderFragment Figure { get; set; }
+ [Parameter] public RenderFragment Heading { get; set; }
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public RenderFragment Actions { get; set; }
+ [Parameter] public DCardVariant Variant { get; set; }
+ [Parameter] public DSize Size { get; set; }
+ /// Figure beside the body instead of above it (card-side).
+ [Parameter] public bool Side { get; set; }
+ /// Figure as the background behind the body (image-full).
+ [Parameter] public bool ImageFull { get; set; }
+ /// Put the figure after the body: at the bottom, or on the right with Side.
+ [Parameter] public bool FigureLast { get; set; }
+ [Parameter] public string FigureClass { get; set; }
+ [Parameter] public string BodyClass { get; set; }
+ /// Extra classes on card-actions, e.g. justify-end.
+ [Parameter] public string ActionsClass { get; set; }
+
+ string CssClass => Cx("card", VariantClass, SizeClass, Side ? "card-side" : null, ImageFull ? "image-full" : null, Class);
+
+ string VariantClass => Variant switch
+ {
+ DCardVariant.Border => "card-border",
+ DCardVariant.Dash => "card-dash",
+ _ => null
+ };
+
+ string SizeClass => Size switch
+ {
+ DSize.Xs => "card-xs",
+ DSize.Sm => "card-sm",
+ DSize.Lg => "card-lg",
+ DSize.Xl => "card-xl",
+ _ => null
+ };
+}
diff --git a/Components/Daisy/DataDisplay/DCardEnums.cs b/Components/Daisy/DataDisplay/DCardEnums.cs
new file mode 100644
index 0000000..13a8456
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DCardEnums.cs
@@ -0,0 +1,8 @@
+namespace decePubClient.Components.Daisy.DataDisplay;
+
+public enum DCardVariant
+{
+ Default,
+ Border,
+ Dash
+}
diff --git a/Components/Daisy/DataDisplay/DCarousel.razor b/Components/Daisy/DataDisplay/DCarousel.razor
new file mode 100644
index 0000000..fa38295
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DCarousel.razor
@@ -0,0 +1,22 @@
+@inherits DComponentBase
+
+@* daisyUI Carousel: https://daisyui.com/components/carousel/ . A scroll-snap strip of DCarouselItem children; the
+ user scrolls or swipes it. Give each item w-full for one slide at a time. *@
+
+ @ChildContent
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public DCarouselSnap Snap { get; set; }
+ [Parameter] public DDirection Direction { get; set; }
+
+ string CssClass => Cx("carousel", SnapClass, Direction == DDirection.Vertical ? "carousel-vertical" : null, Class);
+
+ string SnapClass => Snap switch
+ {
+ DCarouselSnap.Center => "carousel-center",
+ DCarouselSnap.End => "carousel-end",
+ _ => null
+ };
+}
diff --git a/Components/Daisy/DataDisplay/DCarouselEnums.cs b/Components/Daisy/DataDisplay/DCarouselEnums.cs
new file mode 100644
index 0000000..86a277e
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DCarouselEnums.cs
@@ -0,0 +1,9 @@
+namespace decePubClient.Components.Daisy.DataDisplay;
+
+/// Where each item snaps; is daisyUI's default, so it renders no class.
+public enum DCarouselSnap
+{
+ Start,
+ Center,
+ End
+}
diff --git a/Components/Daisy/DataDisplay/DCarouselItem.razor b/Components/Daisy/DataDisplay/DCarouselItem.razor
new file mode 100644
index 0000000..7a8c9bc
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DCarouselItem.razor
@@ -0,0 +1,10 @@
+@inherits DComponentBase
+
+@* One slide of a DCarousel. *@
+
+ @ChildContent
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+}
diff --git a/Components/Daisy/DataDisplay/DChat.razor b/Components/Daisy/DataDisplay/DChat.razor
new file mode 100644
index 0000000..66a82a4
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DChat.razor
@@ -0,0 +1,32 @@
+@inherits DComponentBase
+
+@* daisyUI Chat: https://daisyui.com/components/chat/ . One line of a conversation: an optional avatar, header and
+ footer around one DChatBubble (ChildContent). Avatar renders inside
, so it takes
+ the avatar's frame, e.g.
. *@
+
+ @if (Avatar is not null)
+ {
+
@Avatar
+ }
+ @if (Header is not null)
+ {
+
@Header
+ }
+ @ChildContent
+ @if (Footer is not null)
+ {
+
@Footer
+ }
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public RenderFragment Avatar { get; set; }
+ [Parameter] public RenderFragment Header { get; set; }
+ [Parameter] public RenderFragment Footer { get; set; }
+ [Parameter] public DChatPlacement Placement { get; set; }
+ [Parameter] public string HeaderClass { get; set; }
+ [Parameter] public string FooterClass { get; set; } = "opacity-50";
+
+ string CssClass => Cx("chat", Placement == DChatPlacement.End ? "chat-end" : "chat-start", Class);
+}
diff --git a/Components/Daisy/DataDisplay/DChatBubble.razor b/Components/Daisy/DataDisplay/DChatBubble.razor
new file mode 100644
index 0000000..83b4c7f
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DChatBubble.razor
@@ -0,0 +1,27 @@
+@inherits DComponentBase
+
+@* daisyUI Chat bubble: https://daisyui.com/components/chat/ . The message of a DChat; one per DChat, since daisyUI
+ places every bubble in the same grid cell. *@
+
+ @ChildContent
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public DColor Color { get; set; }
+
+ string CssClass => Cx("chat-bubble", ColorClass, Class);
+
+ string ColorClass => Color switch
+ {
+ DColor.Neutral => "chat-bubble-neutral",
+ DColor.Primary => "chat-bubble-primary",
+ DColor.Secondary => "chat-bubble-secondary",
+ DColor.Accent => "chat-bubble-accent",
+ DColor.Info => "chat-bubble-info",
+ DColor.Success => "chat-bubble-success",
+ DColor.Warning => "chat-bubble-warning",
+ DColor.Error => "chat-bubble-error",
+ _ => null
+ };
+}
diff --git a/Components/Daisy/DataDisplay/DChatEnums.cs b/Components/Daisy/DataDisplay/DChatEnums.cs
new file mode 100644
index 0000000..a1608e3
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DChatEnums.cs
@@ -0,0 +1,8 @@
+namespace decePubClient.Components.Daisy.DataDisplay;
+
+/// daisyUI requires a placement, so unlike other enums both members render a class.
+public enum DChatPlacement
+{
+ Start,
+ End
+}
diff --git a/Components/Daisy/DataDisplay/DCollapse.razor b/Components/Daisy/DataDisplay/DCollapse.razor
new file mode 100644
index 0000000..69d707d
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DCollapse.razor
@@ -0,0 +1,45 @@
+@inherits DComponentBase
+
+@* daisyUI Collapse: https://daisyui.com/components/collapse/ . The open state lives in C# (@bind-IsOpen) and is
+ rendered with collapse-open / collapse-close; clicking the header (or Enter/Space on it) toggles it. neo-raised
+ gives it the neumorphic panel look (see Styles/neo.css for why it isn't a @utility override). *@
+
+
+ @Header
+
+
+ @ChildContent
+
+
+
+@code {
+ [Parameter] public RenderFragment Header { get; set; }
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public bool IsOpen { get; set; }
+ [Parameter] public EventCallback IsOpenChanged { get; set; }
+ [Parameter] public DCollapseIcon Icon { get; set; }
+ [Parameter] public bool Disabled { get; set; }
+ [Parameter] public string HeaderClass { get; set; }
+ [Parameter] public string ContentClass { get; set; }
+
+ string CssClass => Cx("collapse neo-raised", IconClass, IsOpen ? "collapse-open" : "collapse-close", Class);
+
+ string IconClass => Icon switch
+ {
+ DCollapseIcon.Arrow => "collapse-arrow",
+ DCollapseIcon.Plus => "collapse-plus",
+ _ => null
+ };
+
+ async Task Toggle()
+ {
+ if (Disabled)
+ return;
+ IsOpen = !IsOpen;
+ await IsOpenChanged.InvokeAsync(IsOpen);
+ }
+
+ Task OnKeyDown(KeyboardEventArgs e) => e.Key is "Enter" or " " ? Toggle() : Task.CompletedTask;
+}
diff --git a/Components/Daisy/DataDisplay/DCollapseEnums.cs b/Components/Daisy/DataDisplay/DCollapseEnums.cs
new file mode 100644
index 0000000..45b394c
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DCollapseEnums.cs
@@ -0,0 +1,8 @@
+namespace decePubClient.Components.Daisy.DataDisplay;
+
+public enum DCollapseIcon
+{
+ None,
+ Arrow,
+ Plus
+}
diff --git a/Components/Daisy/DataDisplay/DCountdown.razor b/Components/Daisy/DataDisplay/DCountdown.razor
new file mode 100644
index 0000000..b501545
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DCountdown.razor
@@ -0,0 +1,18 @@
+@inherits DComponentBase
+
+@* daisyUI Countdown: https://daisyui.com/components/countdown/ . Shows one number (0-999) that rolls to each new
+ Value; --value and --digits go in the inner span's style. For a clock, put several side by side with separators. *@
+
+ @Number
+
+
+@code {
+ /// The number to show, clamped to 0-999.
+ [Parameter] public int Value { get; set; }
+ /// Minimum digits shown (1-3); 2 pads 5 to 05.
+ [Parameter] public int Digits { get; set; } = 1;
+
+ int Number => Math.Clamp(Value, 0, 999);
+
+ string ValueStyle => Digits > 1 ? $"--value:{Number}; --digits:{Math.Min(Digits, 3)};" : $"--value:{Number};";
+}
diff --git a/Components/Daisy/DataDisplay/DDiff.razor b/Components/Daisy/DataDisplay/DDiff.razor
new file mode 100644
index 0000000..c35ba5a
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DDiff.razor
@@ -0,0 +1,20 @@
+@inherits DComponentBase
+
+@* daisyUI Diff: https://daisyui.com/components/diff/ . Item1 and Item2 overlap, and the resizer handle reveals one over
+ the other; each slot takes one element (an img, or a div that fills it). Keyboard focus on the figure or on item 1
+ slides the handle to either end. It has no height of its own: AspectClass (or a height in Class) gives it one. *@
+
+
@Item1
+
@Item2
+
+
+
+@code {
+ [Parameter] public RenderFragment Item1 { get; set; }
+ [Parameter] public RenderFragment Item2 { get; set; }
+ /// Accessible name of item 1 (its role is img, which hides the alt of an img inside).
+ [Parameter] public string Item1Alt { get; set; }
+ [Parameter] public string Item2Alt { get; set; }
+ /// An aspect-* class for the figure; null when Class gives it a height instead.
+ [Parameter] public string AspectClass { get; set; } = "aspect-16/9";
+}
diff --git a/Components/Daisy/DataDisplay/DHover3D.razor b/Components/Daisy/DataDisplay/DHover3D.razor
new file mode 100644
index 0000000..cc1b7fd
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DHover3D.razor
@@ -0,0 +1,24 @@
+@inherits DComponentBase
+
+@* daisyUI Hover 3D: https://daisyui.com/components/hover-3d/ . ChildContent must be one non-interactive element (a
+
, a card); the eight empty divs after it are the hover zones that tilt it. With Href the whole thing is a
+ link, the only way to make it clickable. *@
+@if (Href is null)
+{
+
+ @ChildContent
+
+
+}
+else
+{
+
+ @ChildContent
+
+
+}
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public string Href { get; set; }
+}
diff --git a/Components/Daisy/DataDisplay/DHoverGallery.razor b/Components/Daisy/DataDisplay/DHoverGallery.razor
new file mode 100644
index 0000000..f49fc93
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DHoverGallery.razor
@@ -0,0 +1,21 @@
+@inherits DComponentBase
+
+@* daisyUI Hover gallery: https://daisyui.com/components/hover-gallery/ . Shows the first image; moving the pointer
+ across it shows the others. Up to 10 images of the same size, from Images or as children; cap its width in
+ Class (max-w-60), or it fills the container. *@
+
+ @if (Images is not null)
+ {
+ foreach (var src in Images.Take(10))
+ {
+
+ }
+ }
+ @ChildContent
+
+
+@code {
+ [Parameter] public IEnumerable Images { get; set; }
+ [Parameter] public string Alt { get; set; } = "";
+ [Parameter] public RenderFragment ChildContent { get; set; }
+}
diff --git a/Components/Daisy/DataDisplay/DKbd.razor b/Components/Daisy/DataDisplay/DKbd.razor
new file mode 100644
index 0000000..d0e1826
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DKbd.razor
@@ -0,0 +1,20 @@
+@inherits DComponentBase
+
+@* daisyUI Kbd: https://daisyui.com/components/kbd/ *@
+@ChildContent
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public DSize Size { get; set; }
+
+ string CssClass => Cx("kbd", SizeClass, Class);
+
+ string SizeClass => Size switch
+ {
+ DSize.Xs => "kbd-xs",
+ DSize.Sm => "kbd-sm",
+ DSize.Lg => "kbd-lg",
+ DSize.Xl => "kbd-xl",
+ _ => null
+ };
+}
diff --git a/Components/Daisy/DataDisplay/DList.razor b/Components/Daisy/DataDisplay/DList.razor
new file mode 100644
index 0000000..9c8ab2b
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DList.razor
@@ -0,0 +1,10 @@
+@inherits DComponentBase
+
+@* daisyUI List: https://daisyui.com/components/list/ . Rows are DListRow children; a plain
works as a caption. *@
+
+ @ChildContent
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+}
diff --git a/Components/Daisy/DataDisplay/DListRow.razor b/Components/Daisy/DataDisplay/DListRow.razor
new file mode 100644
index 0000000..199343f
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DListRow.razor
@@ -0,0 +1,11 @@
+@inherits DComponentBase
+
+@* One row of a DList. Its second child fills the remaining width; put list-col-grow on another child to change that,
+ and list-col-wrap on a child to move it to a second line. *@
+
+ @ChildContent
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+}
diff --git a/Components/Daisy/DataDisplay/DStat.razor b/Components/Daisy/DataDisplay/DStat.razor
new file mode 100644
index 0000000..a4c6152
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DStat.razor
@@ -0,0 +1,37 @@
+@inherits DComponentBase
+
+@* One stat of a DStats: ChildContent is the value (stat-value); each slot renders its part only when set. *@
+
+ @if (Figure is not null)
+ {
+
@Figure
+ }
+ @if (Heading is not null)
+ {
+
@Heading
+ }
+ @if (ChildContent is not null)
+ {
+
@ChildContent
+ }
+ @if (Desc is not null)
+ {
+
@Desc
+ }
+ @if (Actions is not null)
+ {
+
@Actions
+ }
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public RenderFragment Figure { get; set; }
+ [Parameter] public RenderFragment Heading { get; set; }
+ [Parameter] public RenderFragment Desc { get; set; }
+ [Parameter] public RenderFragment Actions { get; set; }
+ /// Extra classes on stat-figure, e.g. text-primary.
+ [Parameter] public string FigureClass { get; set; }
+ /// Extra classes on stat-value, e.g. text-primary.
+ [Parameter] public string ValueClass { get; set; }
+}
diff --git a/Components/Daisy/DataDisplay/DStats.razor b/Components/Daisy/DataDisplay/DStats.razor
new file mode 100644
index 0000000..e60fc61
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DStats.razor
@@ -0,0 +1,14 @@
+@inherits DComponentBase
+
+@* daisyUI Stat: https://daisyui.com/components/stat/ . The container of DStat items; horizontal unless Direction is
+ Vertical (for a responsive switch add lg:stats-horizontal in Class). *@
+
+ @ChildContent
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public DDirection Direction { get; set; }
+
+ string CssClass => Cx("stats", Direction == DDirection.Vertical ? "stats-vertical" : null, Class);
+}
diff --git a/Components/Daisy/DataDisplay/DStatus.razor b/Components/Daisy/DataDisplay/DStatus.razor
new file mode 100644
index 0000000..aeb8bfb
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DStatus.razor
@@ -0,0 +1,45 @@
+@inherits DComponentBase
+
+@* daisyUI Status: https://daisyui.com/components/status/ . Give it an aria-label (online, error, …). Ping stacks an
+ animate-ping copy under the dot, as in daisyUI's ping example; the attributes stay on the visible dot. *@
+@if (Ping)
+{
+
+
+
+
+}
+else
+{
+
+}
+
+@code {
+ [Parameter] public DColor Color { get; set; }
+ [Parameter] public DSize Size { get; set; }
+ [Parameter] public bool Ping { get; set; }
+
+ string CssClass => Cx("status", ColorClass, SizeClass, Class);
+
+ string ColorClass => Color switch
+ {
+ DColor.Neutral => "status-neutral",
+ DColor.Primary => "status-primary",
+ DColor.Secondary => "status-secondary",
+ DColor.Accent => "status-accent",
+ DColor.Info => "status-info",
+ DColor.Success => "status-success",
+ DColor.Warning => "status-warning",
+ DColor.Error => "status-error",
+ _ => null
+ };
+
+ string SizeClass => Size switch
+ {
+ DSize.Xs => "status-xs",
+ DSize.Sm => "status-sm",
+ DSize.Lg => "status-lg",
+ DSize.Xl => "status-xl",
+ _ => null
+ };
+}
diff --git a/Components/Daisy/DataDisplay/DTable.razor b/Components/Daisy/DataDisplay/DTable.razor
new file mode 100644
index 0000000..84d335b
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DTable.razor
@@ -0,0 +1,40 @@
+@inherits DComponentBase
+
+@* daisyUI Table: https://daisyui.com/components/table/ . ChildContent is the // markup. Class and
+ the other attributes go on the
; with Scroll (the default) it sits in an overflow-x-auto div, whose classes
+ are WrapperClass (pinned rows need it to have a height, e.g. h-96). *@
+@if (Scroll)
+{
+
+
@ChildContent
+
+}
+else
+{
+
@ChildContent
+}
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public DSize Size { get; set; }
+ [Parameter] public bool Zebra { get; set; }
+ /// Sticky thead/tfoot rows while the wrapper scrolls vertically.
+ [Parameter] public bool PinRows { get; set; }
+ /// Sticky <th> columns while the wrapper scrolls horizontally.
+ [Parameter] public bool PinCols { get; set; }
+ /// Wrap the table in an overflow-x-auto div so it scrolls on narrow screens.
+ [Parameter] public bool Scroll { get; set; } = true;
+ [Parameter] public string WrapperClass { get; set; }
+
+ string CssClass => Cx("table", Zebra ? "table-zebra" : null, PinRows ? "table-pin-rows" : null,
+ PinCols ? "table-pin-cols" : null, SizeClass, Class);
+
+ string SizeClass => Size switch
+ {
+ DSize.Xs => "table-xs",
+ DSize.Sm => "table-sm",
+ DSize.Lg => "table-lg",
+ DSize.Xl => "table-xl",
+ _ => null
+ };
+}
diff --git a/Components/Daisy/DataDisplay/DTextRotate.razor b/Components/Daisy/DataDisplay/DTextRotate.razor
new file mode 100644
index 0000000..7b874f1
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DTextRotate.razor
@@ -0,0 +1,24 @@
+@inherits DComponentBase
+
+@* daisyUI Text rotate: https://daisyui.com/components/text-rotate/ . Loops through 2-6 lines, one at a time, pausing on
+ hover. The lines are Items, or children in ChildContent (for per-line classes). Set the loop time with a
+ duration-* in Class (default 10s). *@
+
+
+ @if (Items is not null)
+ {
+ foreach (var item in Items.Take(6))
+ {
+ @item
+ }
+ }
+ @ChildContent
+
+
+
+@code {
+ [Parameter] public IEnumerable Items { get; set; }
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ /// Centre the lines horizontally.
+ [Parameter] public bool Centered { get; set; }
+}
diff --git a/Components/Daisy/DataDisplay/DTimeline.razor b/Components/Daisy/DataDisplay/DTimeline.razor
new file mode 100644
index 0000000..b8c7f15
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DTimeline.razor
@@ -0,0 +1,21 @@
+@inherits DComponentBase
+
+@* daisyUI Timeline: https://daisyui.com/components/timeline/ . Horizontal unless Direction is Vertical (for a
+ responsive switch add lg:timeline-horizontal in Class). Every DTimelineItem draws its connecting on both
+ sides; the two arbitrary variants here hide the outer ends, so the line runs only between items. *@
+
+ @ChildContent
+
+
+@code {
+ [Parameter] public RenderFragment ChildContent { get; set; }
+ [Parameter] public DDirection Direction { get; set; }
+ /// Put every item on one side of the line (timeline-compact).
+ [Parameter] public bool Compact { get; set; }
+ /// Align the icon with the start of the content instead of its middle (timeline-snap-icon).
+ [Parameter] public bool SnapIcon { get; set; }
+
+ string CssClass => Cx("timeline", Direction == DDirection.Vertical ? "timeline-vertical" : null,
+ Compact ? "timeline-compact" : null, SnapIcon ? "timeline-snap-icon" : null,
+ "[&>li:first-child>hr:first-child]:hidden [&>li:last-child>hr:last-child]:hidden", Class);
+}
diff --git a/Components/Daisy/DataDisplay/DTimelineItem.razor b/Components/Daisy/DataDisplay/DTimelineItem.razor
new file mode 100644
index 0000000..103ed30
--- /dev/null
+++ b/Components/Daisy/DataDisplay/DTimelineItem.razor
@@ -0,0 +1,36 @@
+@inherits DComponentBase
+
+@* One event of a DTimeline: Start and End on either side of the line, Middle (an icon) on it. Each part renders only
+ when set. The before and after are the line segments to the neighbouring items; colour them with
+ LineBeforeClass / LineAfterClass (bg-primary). *@
+
+
+ @if (Start is not null)
+ {
+
@Start
+ }
+ @if (Middle is not null)
+ {
+
@Middle
+ }
+ @if (End is not null)
+ {
+
@End
+ }
+
+
+
+@code {
+ [Parameter] public RenderFragment Start { get; set; }
+ [Parameter] public RenderFragment Middle { get; set; }
+ [Parameter] public RenderFragment End { get; set; }
+ /// Draw Start as a box (timeline-box).
+ [Parameter] public bool StartBox { get; set; }
+ /// Draw End as a box (timeline-box).
+ [Parameter] public bool EndBox { get; set; }
+ [Parameter] public string StartClass { get; set; }
+ [Parameter] public string MiddleClass { get; set; }
+ [Parameter] public string EndClass { get; set; }
+ [Parameter] public string LineBeforeClass { get; set; }
+ [Parameter] public string LineAfterClass { get; set; }
+}
diff --git a/Components/Daisy/DataInput/DCalendar.razor b/Components/Daisy/DataInput/DCalendar.razor
new file mode 100644
index 0000000..cf2f889
--- /dev/null
+++ b/Components/Daisy/DataInput/DCalendar.razor
@@ -0,0 +1,40 @@
+@inherits DInputBase
+@using System.Globalization
+
+@* daisyUI Calendar (Cally): https://daisyui.com/components/calendar/ . Wraps the web component from
+ wwwroot/vendor/cally.js; value, min and max are yyyy-MM-dd, and Blazor reads the element's value on its change
+ event. Other Cally attributes (locale, first-day-of-week, months) pass through. *@
+
+
+
+
+
+
+@code {
+ [Parameter] public DateOnly? Min { get; set; }
+ [Parameter] public DateOnly? Max { get; set; }
+ [Parameter] public string PreviousText { get; set; } = "Previous";
+ [Parameter] public string NextText { get; set; } = "Next";
+
+ static string Format(DateOnly? date) => date?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
+
+ protected override string FormatValueAsString(DateOnly? value) => Format(value);
+
+ protected override bool TryParseValueFromString(string value, out DateOnly? result, out string validationErrorMessage)
+ {
+ validationErrorMessage = null;
+ result = null;
+ if (string.IsNullOrEmpty(value))
+ return true;
+ if (DateOnly.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
+ {
+ result = date;
+ return true;
+ }
+ validationErrorMessage = $"The {DisplayName ?? FieldIdentifier.FieldName} field is not a valid date.";
+ return false;
+ }
+
+ void OnChange(ChangeEventArgs e) => CurrentValueAsString = e.Value?.ToString();
+}
diff --git a/Components/Daisy/DataInput/DCheckbox.razor b/Components/Daisy/DataInput/DCheckbox.razor
new file mode 100644
index 0000000..01f8799
--- /dev/null
+++ b/Components/Daisy/DataInput/DCheckbox.razor
@@ -0,0 +1,50 @@
+@inherits DInputBase
+
+@* daisyUI Checkbox: https://daisyui.com/components/checkbox/ . With LabelText the checkbox sits in a