From 90e4b9200cfcd7d070be14ef7b7847e0d7dd2f8b Mon Sep 17 00:00:00 2001 From: thepra Date: Sat, 3 Oct 2026 12:01:42 +0200 Subject: [PATCH] Add D-prefixed Blazor wrappers for every daisyUI 5 component Components/Daisy/ wraps all 69 components in daisyUI's llms.txt, grouped by daisyUI's categories (Actions, DataDisplay, Navigation, Feedback, DataInput, Layout, Mockup), with part components where daisyUI has repeating parts (DTab, DMenuItem, DTimelineItem, ...). - Each enum maps to whole literal class names in a switch, so Tailwind's scanner generates them. Nothing is concatenated. - Value-holding fields derive from DInputBase, Blazor's InputBase, so @bind-Value and EditForm validation work. Invalid fields get the component's *-error class. - Open and active state lives in C# (@bind-IsOpen, @bind-Active) and is rendered with daisyUI's *-open modifiers. DDropdown adds an outside-click layer and Escape. No component uses JS interop. - The one exception is DCalendar, which wraps the Cally web component, vendored as wwwroot/vendor/cally.js (0.9.2, MIT). /styleguide (not linked) renders every component in all its colours, sizes and states, with a theme and hue picker that previews without saving. neo.css follows what the styleguide showed: - select keeps daisyUI's arrow; - skeleton-text isn't sunk; - the phone mockup and dock are raised; - active menu items and OTP digits are inset; - a joined accordion is raised as one block. The primary, secondary and accent colours are deeper, so they still read as text. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw --- Components/Daisy/Actions/DButton.razor | 74 + Components/Daisy/Actions/DButtonEnums.cs | 20 + Components/Daisy/Actions/DDropdown.razor | 66 + Components/Daisy/Actions/DDropdownEnums.cs | 18 + Components/Daisy/Actions/DFab.razor | 53 + Components/Daisy/Actions/DModal.razor | 83 ++ Components/Daisy/Actions/DModalEnums.cs | 11 + Components/Daisy/Actions/DSwap.razor | 35 + Components/Daisy/Actions/DSwapEnums.cs | 8 + .../Daisy/Actions/DThemeController.razor | 27 + Components/Daisy/DComponentBase.cs | 27 + Components/Daisy/DInputBase.cs | 39 + Components/Daisy/DaisyEnums.cs | 34 + Components/Daisy/DataDisplay/DAccordion.razor | 46 + .../Daisy/DataDisplay/DAccordionItem.razor | 32 + Components/Daisy/DataDisplay/DAura.razor | 51 + Components/Daisy/DataDisplay/DAuraEnums.cs | 12 + Components/Daisy/DataDisplay/DAvatar.razor | 36 + Components/Daisy/DataDisplay/DAvatarEnums.cs | 8 + .../Daisy/DataDisplay/DAvatarGroup.razor | 11 + Components/Daisy/DataDisplay/DBadge.razor | 44 + Components/Daisy/DataDisplay/DBadgeEnums.cs | 10 + Components/Daisy/DataDisplay/DCard.razor | 66 + Components/Daisy/DataDisplay/DCardEnums.cs | 8 + Components/Daisy/DataDisplay/DCarousel.razor | 22 + .../Daisy/DataDisplay/DCarouselEnums.cs | 9 + .../Daisy/DataDisplay/DCarouselItem.razor | 10 + Components/Daisy/DataDisplay/DChat.razor | 32 + .../Daisy/DataDisplay/DChatBubble.razor | 27 + Components/Daisy/DataDisplay/DChatEnums.cs | 8 + Components/Daisy/DataDisplay/DCollapse.razor | 45 + .../Daisy/DataDisplay/DCollapseEnums.cs | 8 + Components/Daisy/DataDisplay/DCountdown.razor | 18 + Components/Daisy/DataDisplay/DDiff.razor | 20 + Components/Daisy/DataDisplay/DHover3D.razor | 24 + .../Daisy/DataDisplay/DHoverGallery.razor | 21 + Components/Daisy/DataDisplay/DKbd.razor | 20 + Components/Daisy/DataDisplay/DList.razor | 10 + Components/Daisy/DataDisplay/DListRow.razor | 11 + Components/Daisy/DataDisplay/DStat.razor | 37 + Components/Daisy/DataDisplay/DStats.razor | 14 + Components/Daisy/DataDisplay/DStatus.razor | 45 + Components/Daisy/DataDisplay/DTable.razor | 40 + .../Daisy/DataDisplay/DTextRotate.razor | 24 + Components/Daisy/DataDisplay/DTimeline.razor | 21 + .../Daisy/DataDisplay/DTimelineItem.razor | 36 + Components/Daisy/DataInput/DCalendar.razor | 40 + Components/Daisy/DataInput/DCheckbox.razor | 50 + Components/Daisy/DataInput/DFieldset.razor | 21 + Components/Daisy/DataInput/DFileInput.razor | 39 + Components/Daisy/DataInput/DFilter.razor | 43 + Components/Daisy/DataInput/DInput.razor | 65 + Components/Daisy/DataInput/DLabel.razor | 22 + Components/Daisy/DataInput/DOtp.razor | 58 + Components/Daisy/DataInput/DRadio.razor | 60 + Components/Daisy/DataInput/DRadioGroup.razor | 33 + Components/Daisy/DataInput/DRange.razor | 54 + Components/Daisy/DataInput/DRating.razor | 85 ++ Components/Daisy/DataInput/DRatingEnums.cs | 20 + Components/Daisy/DataInput/DSelect.razor | 59 + Components/Daisy/DataInput/DTextarea.razor | 50 + Components/Daisy/DataInput/DToggle.razor | 101 ++ .../Daisy/DataInput/DValidatorHint.razor | 12 + Components/Daisy/Feedback/DAlert.razor | 44 + Components/Daisy/Feedback/DAlertEnums.cs | 19 + Components/Daisy/Feedback/DLoading.razor | 47 + Components/Daisy/Feedback/DLoadingEnums.cs | 12 + Components/Daisy/Feedback/DProgress.razor | 30 + .../Daisy/Feedback/DRadialProgress.razor | 63 + Components/Daisy/Feedback/DSkeleton.razor | 13 + Components/Daisy/Feedback/DToast.razor | 29 + Components/Daisy/Feedback/DToastEnums.cs | 17 + Components/Daisy/Feedback/DTooltip.razor | 51 + Components/Daisy/Feedback/DTooltipEnums.cs | 18 + Components/Daisy/Layout/DDivider.razor | 40 + Components/Daisy/Layout/DDividerEnums.cs | 21 + Components/Daisy/Layout/DDrawer.razor | 60 + Components/Daisy/Layout/DDrawerEnums.cs | 12 + Components/Daisy/Layout/DFooter.razor | 25 + Components/Daisy/Layout/DFooterEnums.cs | 12 + Components/Daisy/Layout/DFooterNav.razor | 15 + Components/Daisy/Layout/DHero.razor | 36 + Components/Daisy/Layout/DIndicator.razor | 13 + Components/Daisy/Layout/DIndicatorEnums.cs | 17 + Components/Daisy/Layout/DIndicatorItem.razor | 26 + Components/Daisy/Layout/DJoin.razor | 15 + Components/Daisy/Layout/DMask.razor | 47 + Components/Daisy/Layout/DMaskEnums.cs | 28 + Components/Daisy/Layout/DStack.razor | 22 + Components/Daisy/Layout/DStackEnums.cs | 10 + Components/Daisy/Mockup/DMockupBrowser.razor | 30 + Components/Daisy/Mockup/DMockupCode.razor | 12 + Components/Daisy/Mockup/DMockupCodeLine.razor | 10 + Components/Daisy/Mockup/DMockupPhone.razor | 16 + Components/Daisy/Mockup/DMockupWindow.razor | 15 + .../Daisy/Navigation/DBreadcrumbItem.razor | 23 + .../Daisy/Navigation/DBreadcrumbs.razor | 13 + Components/Daisy/Navigation/DDock.razor | 21 + Components/Daisy/Navigation/DDockItem.razor | 38 + Components/Daisy/Navigation/DLink.razor | 40 + Components/Daisy/Navigation/DMegamenu.razor | 55 + Components/Daisy/Navigation/DMegamenuEnums.cs | 12 + .../Daisy/Navigation/DMegamenuItem.razor | 30 + Components/Daisy/Navigation/DMenu.razor | 27 + Components/Daisy/Navigation/DMenuItem.razor | 29 + Components/Daisy/Navigation/DMenuTitle.razor | 10 + Components/Daisy/Navigation/DNavbar.razor | 29 + Components/Daisy/Navigation/DPagination.razor | 99 ++ Components/Daisy/Navigation/DStep.razor | 32 + Components/Daisy/Navigation/DSteps.razor | 12 + Components/Daisy/Navigation/DSubmenu.razor | 46 + Components/Daisy/Navigation/DTab.razor | 21 + Components/Daisy/Navigation/DTabs.razor | 76 + Components/Daisy/Navigation/DTabsEnums.cs | 9 + Components/Styleguide/ActionsSection.razor | 224 +++ .../Styleguide/DataDisplaySection.razor | 573 ++++++++ Components/Styleguide/DataInputSection.razor | 380 +++++ Components/Styleguide/FeedbackSection.razor | 236 +++ Components/Styleguide/LayoutSection.razor | 222 +++ Components/Styleguide/MockupSection.razor | 53 + Components/Styleguide/NavigationSection.razor | 265 ++++ Pages/Styleguide.razor | 74 + Styles/neo.css | 35 +- Styles/theme.css | 24 +- _Imports.razor | 8 + wwwroot/index.html | 1 + wwwroot/vendor/cally.js | 1304 +++++++++++++++++ 127 files changed, 6962 insertions(+), 17 deletions(-) create mode 100644 Components/Daisy/Actions/DButton.razor create mode 100644 Components/Daisy/Actions/DButtonEnums.cs create mode 100644 Components/Daisy/Actions/DDropdown.razor create mode 100644 Components/Daisy/Actions/DDropdownEnums.cs create mode 100644 Components/Daisy/Actions/DFab.razor create mode 100644 Components/Daisy/Actions/DModal.razor create mode 100644 Components/Daisy/Actions/DModalEnums.cs create mode 100644 Components/Daisy/Actions/DSwap.razor create mode 100644 Components/Daisy/Actions/DSwapEnums.cs create mode 100644 Components/Daisy/Actions/DThemeController.razor create mode 100644 Components/Daisy/DComponentBase.cs create mode 100644 Components/Daisy/DInputBase.cs create mode 100644 Components/Daisy/DaisyEnums.cs create mode 100644 Components/Daisy/DataDisplay/DAccordion.razor create mode 100644 Components/Daisy/DataDisplay/DAccordionItem.razor create mode 100644 Components/Daisy/DataDisplay/DAura.razor create mode 100644 Components/Daisy/DataDisplay/DAuraEnums.cs create mode 100644 Components/Daisy/DataDisplay/DAvatar.razor create mode 100644 Components/Daisy/DataDisplay/DAvatarEnums.cs create mode 100644 Components/Daisy/DataDisplay/DAvatarGroup.razor create mode 100644 Components/Daisy/DataDisplay/DBadge.razor create mode 100644 Components/Daisy/DataDisplay/DBadgeEnums.cs create mode 100644 Components/Daisy/DataDisplay/DCard.razor create mode 100644 Components/Daisy/DataDisplay/DCardEnums.cs create mode 100644 Components/Daisy/DataDisplay/DCarousel.razor create mode 100644 Components/Daisy/DataDisplay/DCarouselEnums.cs create mode 100644 Components/Daisy/DataDisplay/DCarouselItem.razor create mode 100644 Components/Daisy/DataDisplay/DChat.razor create mode 100644 Components/Daisy/DataDisplay/DChatBubble.razor create mode 100644 Components/Daisy/DataDisplay/DChatEnums.cs create mode 100644 Components/Daisy/DataDisplay/DCollapse.razor create mode 100644 Components/Daisy/DataDisplay/DCollapseEnums.cs create mode 100644 Components/Daisy/DataDisplay/DCountdown.razor create mode 100644 Components/Daisy/DataDisplay/DDiff.razor create mode 100644 Components/Daisy/DataDisplay/DHover3D.razor create mode 100644 Components/Daisy/DataDisplay/DHoverGallery.razor create mode 100644 Components/Daisy/DataDisplay/DKbd.razor create mode 100644 Components/Daisy/DataDisplay/DList.razor create mode 100644 Components/Daisy/DataDisplay/DListRow.razor create mode 100644 Components/Daisy/DataDisplay/DStat.razor create mode 100644 Components/Daisy/DataDisplay/DStats.razor create mode 100644 Components/Daisy/DataDisplay/DStatus.razor create mode 100644 Components/Daisy/DataDisplay/DTable.razor create mode 100644 Components/Daisy/DataDisplay/DTextRotate.razor create mode 100644 Components/Daisy/DataDisplay/DTimeline.razor create mode 100644 Components/Daisy/DataDisplay/DTimelineItem.razor create mode 100644 Components/Daisy/DataInput/DCalendar.razor create mode 100644 Components/Daisy/DataInput/DCheckbox.razor create mode 100644 Components/Daisy/DataInput/DFieldset.razor create mode 100644 Components/Daisy/DataInput/DFileInput.razor create mode 100644 Components/Daisy/DataInput/DFilter.razor create mode 100644 Components/Daisy/DataInput/DInput.razor create mode 100644 Components/Daisy/DataInput/DLabel.razor create mode 100644 Components/Daisy/DataInput/DOtp.razor create mode 100644 Components/Daisy/DataInput/DRadio.razor create mode 100644 Components/Daisy/DataInput/DRadioGroup.razor create mode 100644 Components/Daisy/DataInput/DRange.razor create mode 100644 Components/Daisy/DataInput/DRating.razor create mode 100644 Components/Daisy/DataInput/DRatingEnums.cs create mode 100644 Components/Daisy/DataInput/DSelect.razor create mode 100644 Components/Daisy/DataInput/DTextarea.razor create mode 100644 Components/Daisy/DataInput/DToggle.razor create mode 100644 Components/Daisy/DataInput/DValidatorHint.razor create mode 100644 Components/Daisy/Feedback/DAlert.razor create mode 100644 Components/Daisy/Feedback/DAlertEnums.cs create mode 100644 Components/Daisy/Feedback/DLoading.razor create mode 100644 Components/Daisy/Feedback/DLoadingEnums.cs create mode 100644 Components/Daisy/Feedback/DProgress.razor create mode 100644 Components/Daisy/Feedback/DRadialProgress.razor create mode 100644 Components/Daisy/Feedback/DSkeleton.razor create mode 100644 Components/Daisy/Feedback/DToast.razor create mode 100644 Components/Daisy/Feedback/DToastEnums.cs create mode 100644 Components/Daisy/Feedback/DTooltip.razor create mode 100644 Components/Daisy/Feedback/DTooltipEnums.cs create mode 100644 Components/Daisy/Layout/DDivider.razor create mode 100644 Components/Daisy/Layout/DDividerEnums.cs create mode 100644 Components/Daisy/Layout/DDrawer.razor create mode 100644 Components/Daisy/Layout/DDrawerEnums.cs create mode 100644 Components/Daisy/Layout/DFooter.razor create mode 100644 Components/Daisy/Layout/DFooterEnums.cs create mode 100644 Components/Daisy/Layout/DFooterNav.razor create mode 100644 Components/Daisy/Layout/DHero.razor create mode 100644 Components/Daisy/Layout/DIndicator.razor create mode 100644 Components/Daisy/Layout/DIndicatorEnums.cs create mode 100644 Components/Daisy/Layout/DIndicatorItem.razor create mode 100644 Components/Daisy/Layout/DJoin.razor create mode 100644 Components/Daisy/Layout/DMask.razor create mode 100644 Components/Daisy/Layout/DMaskEnums.cs create mode 100644 Components/Daisy/Layout/DStack.razor create mode 100644 Components/Daisy/Layout/DStackEnums.cs create mode 100644 Components/Daisy/Mockup/DMockupBrowser.razor create mode 100644 Components/Daisy/Mockup/DMockupCode.razor create mode 100644 Components/Daisy/Mockup/DMockupCodeLine.razor create mode 100644 Components/Daisy/Mockup/DMockupPhone.razor create mode 100644 Components/Daisy/Mockup/DMockupWindow.razor create mode 100644 Components/Daisy/Navigation/DBreadcrumbItem.razor create mode 100644 Components/Daisy/Navigation/DBreadcrumbs.razor create mode 100644 Components/Daisy/Navigation/DDock.razor create mode 100644 Components/Daisy/Navigation/DDockItem.razor create mode 100644 Components/Daisy/Navigation/DLink.razor create mode 100644 Components/Daisy/Navigation/DMegamenu.razor create mode 100644 Components/Daisy/Navigation/DMegamenuEnums.cs create mode 100644 Components/Daisy/Navigation/DMegamenuItem.razor create mode 100644 Components/Daisy/Navigation/DMenu.razor create mode 100644 Components/Daisy/Navigation/DMenuItem.razor create mode 100644 Components/Daisy/Navigation/DMenuTitle.razor create mode 100644 Components/Daisy/Navigation/DNavbar.razor create mode 100644 Components/Daisy/Navigation/DPagination.razor create mode 100644 Components/Daisy/Navigation/DStep.razor create mode 100644 Components/Daisy/Navigation/DSteps.razor create mode 100644 Components/Daisy/Navigation/DSubmenu.razor create mode 100644 Components/Daisy/Navigation/DTab.razor create mode 100644 Components/Daisy/Navigation/DTabs.razor create mode 100644 Components/Daisy/Navigation/DTabsEnums.cs create mode 100644 Components/Styleguide/ActionsSection.razor create mode 100644 Components/Styleguide/DataDisplaySection.razor create mode 100644 Components/Styleguide/DataInputSection.razor create mode 100644 Components/Styleguide/FeedbackSection.razor create mode 100644 Components/Styleguide/LayoutSection.razor create mode 100644 Components/Styleguide/MockupSection.razor create mode 100644 Components/Styleguide/NavigationSection.razor create mode 100644 Pages/Styleguide.razor create mode 100644 wwwroot/vendor/cally.js 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. *@ + + +@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) + { + @Alt + } + 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. *@ +
+ @ChildContent +
+ +@code { + [Parameter] public RenderFragment ChildContent { get; set; } +} diff --git a/Components/Daisy/DataDisplay/DBadge.razor b/Components/Daisy/DataDisplay/DBadge.razor new file mode 100644 index 0000000..7a8c64c --- /dev/null +++ b/Components/Daisy/DataDisplay/DBadge.razor @@ -0,0 +1,44 @@ +@inherits DComponentBase + +@* daisyUI Badge: https://daisyui.com/components/badge/ . Without ChildContent it is an empty dot. *@ +@ChildContent + +@code { + [Parameter] public RenderFragment ChildContent { get; set; } + [Parameter] public DColor Color { get; set; } + [Parameter] public DBadgeVariant Variant { get; set; } + [Parameter] public DSize Size { get; set; } + + string CssClass => Cx("badge", ColorClass, VariantClass, SizeClass, Class); + + string ColorClass => Color switch + { + DColor.Neutral => "badge-neutral", + DColor.Primary => "badge-primary", + DColor.Secondary => "badge-secondary", + DColor.Accent => "badge-accent", + DColor.Info => "badge-info", + DColor.Success => "badge-success", + DColor.Warning => "badge-warning", + DColor.Error => "badge-error", + _ => null + }; + + string VariantClass => Variant switch + { + DBadgeVariant.Outline => "badge-outline", + DBadgeVariant.Dash => "badge-dash", + DBadgeVariant.Soft => "badge-soft", + DBadgeVariant.Ghost => "badge-ghost", + _ => null + }; + + string SizeClass => Size switch + { + DSize.Xs => "badge-xs", + DSize.Sm => "badge-sm", + DSize.Lg => "badge-lg", + DSize.Xl => "badge-xl", + _ => null + }; +} diff --git a/Components/Daisy/DataDisplay/DBadgeEnums.cs b/Components/Daisy/DataDisplay/DBadgeEnums.cs new file mode 100644 index 0000000..1c862a1 --- /dev/null +++ b/Components/Daisy/DataDisplay/DBadgeEnums.cs @@ -0,0 +1,10 @@ +namespace decePubClient.Components.Daisy.DataDisplay; + +public enum DBadgeVariant +{ + Default, + Outline, + Dash, + Soft, + Ghost +} diff --git a/Components/Daisy/DataDisplay/DCard.razor b/Components/Daisy/DataDisplay/DCard.razor new file mode 100644 index 0000000..2339c2d --- /dev/null +++ b/Components/Daisy/DataDisplay/DCard.razor @@ -0,0 +1,66 @@ +@inherits DComponentBase + +@* daisyUI Card: https://daisyui.com/components/card/ . Figure is wrapped in
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. *@ +
+ + +
+
+ +@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)) + { + @Alt + } + } + @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