diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor index 2298ef346..25b9d8b0a 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor @@ -1,40 +1,73 @@ @namespace BlazorBootstrap.Demo.RCL @inherits MainLayoutBase -
+ + + + + + + - + + @Body -
-
- @Body + + + + If you like Blazor Bootstrap, give it a star on GitHub! + + + + - - - - If you like Blazor Bootstrap, give it a star on GitHub! - + +
+
+ + Blazor Bootstrap + Blazor Bootstrap - +
    +
  • Designed and built with all the love in the world by the Blazor Bootstrap team with the help of our contributors.
  • +
  • Code licensed Apache License 2.0.
  • +
  • Currently @Version.
  • +
+
+
+
Links
+ +
+ +
+
Community
+ +
+
- - - -
-
+ + diff --git a/blazorbootstrap/Components/Core/BlazorBootstrapLayoutComponentBase.cs b/blazorbootstrap/Components/Core/BlazorBootstrapLayoutComponentBase.cs new file mode 100644 index 000000000..d4fd797d5 --- /dev/null +++ b/blazorbootstrap/Components/Core/BlazorBootstrapLayoutComponentBase.cs @@ -0,0 +1,165 @@ +using Microsoft.Extensions.Configuration; + +namespace BlazorBootstrap; + +public abstract class BlazorBootstrapLayoutComponentBase : LayoutComponentBase, IDisposable, IAsyncDisposable +{ + #region Fields and Constants + + private bool isAsyncDisposed; + + private bool isDisposed; + + #endregion + + #region Methods + + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + IsRenderComplete = true; + + await base.OnAfterRenderAsync(firstRender); + } + + /// + protected override void OnInitialized() + { + Id ??= IdUtility.GetNextId(); + + base.OnInitialized(); + } + + public static string BuildClassNames(params (string? cssClass, bool when)[] cssClassList) + { + var list = new HashSet(); + + if (cssClassList is not null && cssClassList.Any()) + foreach (var (cssClass, when) in cssClassList) + if (!string.IsNullOrWhiteSpace(cssClass) && when) + list.Add(cssClass); + + if (list.Any()) + return string.Join(" ", list); + + return string.Empty; + } + + public static string BuildClassNames(string? userDefinedCssClass, params (string? cssClass, bool when)[] cssClassList) + { + var list = new HashSet(); + + if (cssClassList is not null && cssClassList.Any()) + foreach (var (cssClass, when) in cssClassList) + if (!string.IsNullOrWhiteSpace(cssClass) && when) + list.Add(cssClass); + + if (!string.IsNullOrWhiteSpace(userDefinedCssClass)) + list.Add(userDefinedCssClass.Trim()); + + if (list.Any()) + return string.Join(" ", list); + + return string.Empty; + } + + public static string BuildStyleNames(string? userDefinedCssStyle, params (string? cssStyle, bool when)[] cssStyleList) + { + var list = new HashSet(); + + if (cssStyleList is not null && cssStyleList.Any()) + foreach (var (cssStyle, when) in cssStyleList) + if (!string.IsNullOrWhiteSpace(cssStyle) && when) + list.Add(cssStyle); + + if (!string.IsNullOrWhiteSpace(userDefinedCssStyle)) + list.Add(userDefinedCssStyle.Trim()); + + if (list.Any()) + return string.Join(';', list); + + return string.Empty; + } + + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore(true).ConfigureAwait(false); + + Dispose(false); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!isDisposed) + { + if (disposing) + { + // cleanup + } + + isDisposed = true; + } + } + + protected virtual ValueTask DisposeAsyncCore(bool disposing) + { + if (!isAsyncDisposed) + { + if (disposing) + { + // cleanup + } + + isAsyncDisposed = true; + } + + return ValueTask.CompletedTask; + } + + #endregion + + #region Properties, Indexers + + [Parameter(CaptureUnmatchedValues = true)] public Dictionary AdditionalAttributes { get; set; } = default!; + + [Parameter] public string? Class { get; set; } + + protected virtual string? ClassNames => Class; + + [Inject] protected IConfiguration Configuration { get; set; } = default!; + + public ElementReference Element { get; set; } + + [Parameter] public string? Id { get; set; } + + protected bool IsRenderComplete { get; private set; } + + [Inject] protected IJSRuntime JSRuntime { get; set; } = default!; + + [Parameter] public string? Style { get; set; } + + protected virtual string? StyleNames => Style; + + #endregion + + #region Other + + ~BlazorBootstrapLayoutComponentBase() + { + Dispose(false); + } + + #endregion +} diff --git a/blazorbootstrap/Components/Layout/BlazorBootstrapLayout.razor b/blazorbootstrap/Components/Layout/BlazorBootstrapLayout.razor new file mode 100644 index 000000000..fbc86571f --- /dev/null +++ b/blazorbootstrap/Components/Layout/BlazorBootstrapLayout.razor @@ -0,0 +1,27 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapLayoutComponentBase + +
+ + @SidebarSection + +
+ @if (HeaderSection is not null) + { +
+ @HeaderSection +
+ } + +
+ @ContentSection +
+ + @if (FooterSection is not null) + { +
+ @FooterSection +
+ } +
+
\ No newline at end of file diff --git a/blazorbootstrap/Components/Layout/BlazorBootstrapLayout.razor.cs b/blazorbootstrap/Components/Layout/BlazorBootstrapLayout.razor.cs new file mode 100644 index 000000000..f9dc56f8c --- /dev/null +++ b/blazorbootstrap/Components/Layout/BlazorBootstrapLayout.razor.cs @@ -0,0 +1,24 @@ +namespace BlazorBootstrap; + +public partial class BlazorBootstrapLayout : BlazorBootstrapLayoutComponentBase +{ + #region Properties, Indexers + + protected override string? ClassNames => BuildClassNames(Class, ("bb-page", true)); + + [Parameter] public RenderFragment? ContentSection { get; set; } + [Parameter] public string? ContentSectionCssClass { get; set; } + protected string? ContentSectionCssClassNames => BuildClassNames(ContentSectionCssClass, ("p-4", true)); + + [Parameter] public RenderFragment? FooterSection { get; set; } + [Parameter] public string? FooterSectionCssClass { get; set; } = "bg-body-tertiary"; + protected string? FooterSectionCssClassNames => BuildClassNames(FooterSectionCssClass, ("bb-footer p-4", true)); + + [Parameter] public RenderFragment? HeaderSection { get; set; } + [Parameter] public string? HeaderSectionCssClass { get; set; } = "d-flex justify-content-end"; + protected string? HeaderSectionCssClassNames => BuildClassNames(HeaderSectionCssClass, ("bb-top-row px-4", true)); + + [Parameter] public RenderFragment? SidebarSection { get; set; } + + #endregion +} diff --git a/blazorbootstrap/Components/Layout/BlazorBootstrapLayout.razor.css b/blazorbootstrap/Components/Layout/BlazorBootstrapLayout.razor.css new file mode 100644 index 000000000..e69de29bb diff --git a/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcher.razor b/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcher.razor new file mode 100644 index 000000000..ebbd52192 --- /dev/null +++ b/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcher.razor @@ -0,0 +1,24 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapComponentBase + + + \ No newline at end of file diff --git a/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcher.razor.cs b/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcher.razor.cs new file mode 100644 index 000000000..07ef82085 --- /dev/null +++ b/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcher.razor.cs @@ -0,0 +1,28 @@ +namespace BlazorBootstrap; + +public partial class ThemeSwitcher : BlazorBootstrapComponentBase +{ + #region Methods + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + await ThemeSwitcherJsInterop.InitializeAsync(); + + await base.OnAfterRenderAsync(firstRender); + } + + internal Task SetAutoTheme() => ThemeSwitcherJsInterop.SetAutoThemeAsync(); + + internal Task SetDarkTheme() => ThemeSwitcherJsInterop.SetDarkThemeAsync(); + + internal Task SetLightTheme() => ThemeSwitcherJsInterop.SetLightThemeAsync(); + + #endregion + + #region Properties, Indexers + + [Inject] private ThemeSwitcherJsInterop ThemeSwitcherJsInterop { get; set; } = default!; + + #endregion +} diff --git a/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcher.razor.css b/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcher.razor.css new file mode 100644 index 000000000..e69de29bb diff --git a/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcherJsInterop.cs b/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcherJsInterop.cs new file mode 100644 index 000000000..6bef7b702 --- /dev/null +++ b/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcherJsInterop.cs @@ -0,0 +1,50 @@ +namespace BlazorBootstrap; + +public class ThemeSwitcherJsInterop : IAsyncDisposable +{ + #region Fields and Constants + + private readonly Lazy> moduleTask; + + #endregion + + #region Constructors + + public ThemeSwitcherJsInterop(IJSRuntime jsRuntime) + { + moduleTask = new Lazy>(() => jsRuntime.InvokeAsync("import", "./_content/Blazor.Bootstrap/blazor.bootstrap.theme-switcher.js").AsTask()); + } + + #endregion + + #region Methods + + public async ValueTask DisposeAsync() + { + if (moduleTask.IsValueCreated) + { + var module = await moduleTask.Value; + await module.DisposeAsync(); + } + } + + public async Task InitializeAsync() + { + var module = await moduleTask.Value; + await module.InvokeVoidAsync("initializeTheme"); + } + + internal Task SetAutoThemeAsync() => SetThemeAsync("system"); + + internal Task SetDarkThemeAsync() => SetThemeAsync("dark"); + + internal Task SetLightThemeAsync() => SetThemeAsync("light"); + + internal async Task SetThemeAsync(string themeName) + { + var module = await moduleTask.Value; + await module.InvokeVoidAsync("setTheme", themeName); + } + + #endregion +} diff --git a/blazorbootstrap/Config.cs b/blazorbootstrap/Config.cs index fe8146e37..0a26e404a 100644 --- a/blazorbootstrap/Config.cs +++ b/blazorbootstrap/Config.cs @@ -21,6 +21,7 @@ public static IServiceCollection AddBlazorBootstrap(this IServiceCollection serv services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.css b/blazorbootstrap/wwwroot/blazor.bootstrap.css index 5e532d082..047277b75 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.css +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.css @@ -627,3 +627,14 @@ main { .pdf-viewer-dropdown-toggle::after { content: inherit !important; } + +/* layout */ +.bb-footer a { + color: var(--bs-body-color); + text-decoration: none +} + +.bb-footer a:hover, .bb-footer a:focus { + color: var(--bs-link-hover-color); + text-decoration: underline; +} diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.theme-switcher.js b/blazorbootstrap/wwwroot/blazor.bootstrap.theme-switcher.js new file mode 100644 index 000000000..67e9c9710 --- /dev/null +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.theme-switcher.js @@ -0,0 +1,99 @@ +// THEMES +const STORAGE_KEY = "blazorbootstrap-theme"; +const DEFAULT_THEME = "light"; +const SYSTEM_THEME = "system"; + +const state = { + chosenTheme: SYSTEM_THEME, // light|dark|system + appliedTheme: DEFAULT_THEME // light|dark +}; + +const showActiveTheme = () => { + let $themeIndicator = document.querySelector(".blazorbootstrap-theme-indicator>i"); + if ($themeIndicator) { + if (state.appliedTheme === "light") { + $themeIndicator.className = "bi bi-sun-fill"; + } else if (state.appliedTheme === "dark") { + $themeIndicator.className = "bi bi-moon-stars-fill"; + } else { + $themeIndicator.className = "bi bi-circle-half"; + } + } + + let $themeSwitchers = document.querySelectorAll(".blazorbootstrap-theme-item>button"); + if ($themeSwitchers) { + $themeSwitchers.forEach((el) => { + const bsThemeValue = el.dataset.bsThemeValue; + const iEl = el.querySelector(".bi.bi-check2"); + if (state.chosenTheme === bsThemeValue) { + el.classList.add("active"); + if (iEl) + iEl.classList.remove("d-none"); + } else { + el.classList.remove("active"); + if (iEl) + iEl.classList.add("d-none"); + } + }); + } +}; + +export function setTheme(theme, save = true) { + state.chosenTheme = theme; + state.appliedTheme = theme; + + if (theme === SYSTEM_THEME) { + state.appliedTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + document.documentElement.setAttribute("data-bs-theme", state.appliedTheme); + if (save) { + window.localStorage.setItem(STORAGE_KEY, state.chosenTheme); + } + showActiveTheme(); + updateDemoCodeThemeCss(state.appliedTheme); +}; + +export function initializeTheme() { + const localTheme = window.localStorage.getItem(STORAGE_KEY); + if (localTheme) { + setTheme(localTheme, false); + } else { + setTheme(SYSTEM_THEME); + } + + // register events + window + .matchMedia("(prefers-color-scheme: dark)") + .addEventListener("change", (event) => { + const theme = event.matches ? "dark" : "light"; + setTheme(theme); + }); +} + +export function updateDemoCodeThemeCss(theme) { + if (theme === "dark") { + let prismThemeLightLinkEl = document.getElementById('prismThemeLightLink'); + if (prismThemeLightLinkEl) + prismThemeLightLinkEl?.remove(); + + let prismThemeDarkLinkEl = document.createElement("link"); + prismThemeDarkLinkEl.setAttribute("rel", "stylesheet"); + prismThemeDarkLinkEl.setAttribute("href", "/_content/BlazorBootstrap.Demo.RCL/css/prism-vsc-dark-plus.min.css"); + prismThemeDarkLinkEl.setAttribute("id", "prismThemeDarkLink"); + + document.head.append(prismThemeDarkLinkEl); + } + else if (theme === "light") { + let prismThemeDarkLinkEl = document.getElementById('prismThemeDarkLink'); + if (prismThemeDarkLinkEl) + prismThemeDarkLinkEl?.remove(); + + let prismThemeLightLinkEl = document.createElement("link"); + prismThemeLightLinkEl.setAttribute("rel", "stylesheet"); + prismThemeLightLinkEl.setAttribute("href", "/_content/BlazorBootstrap.Demo.RCL/css/prism-vs.min.css"); + prismThemeLightLinkEl.setAttribute("id", "prismThemeLightLink"); + + document.head.append(prismThemeLightLinkEl); + } +} \ No newline at end of file