diff --git a/src/FluentCMS.sln b/src/FluentCMS.sln index 95e8fb97d..f34a1a9e4 100644 --- a/src/FluentCMS.sln +++ b/src/FluentCMS.sln @@ -101,6 +101,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentCMS.Repositories.EFCo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentCMS.Repositories.EFCore.Sqlite", "Backend\Repositories\FluentCMS.Repositories.EFCore.Sqlite\FluentCMS.Repositories.EFCore.Sqlite.csproj", "{1914FAAB-9B86-4B76-90CA-E16D3C8984FC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentCMS.Web.UI.TailwindStyleBuilder", "Frontend\FluentCMS.Web.UI.TailwindStyleBuilder\FluentCMS.Web.UI.TailwindStyleBuilder.csproj", "{9A614AAB-D386-4D5F-971B-33BA9E820AF8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -259,6 +261,10 @@ Global {1914FAAB-9B86-4B76-90CA-E16D3C8984FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {1914FAAB-9B86-4B76-90CA-E16D3C8984FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {1914FAAB-9B86-4B76-90CA-E16D3C8984FC}.Release|Any CPU.Build.0 = Release|Any CPU + {9A614AAB-D386-4D5F-971B-33BA9E820AF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A614AAB-D386-4D5F-971B-33BA9E820AF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A614AAB-D386-4D5F-971B-33BA9E820AF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A614AAB-D386-4D5F-971B-33BA9E820AF8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -308,6 +314,7 @@ Global {8E46C73B-D442-489F-B313-CD26F3C1F2DF} = {1CC73AD2-CFC9-4FE7-96C0-0E4FEFBB66F1} {E4350BBB-204F-4FD1-A187-7C8381860831} = {1CC73AD2-CFC9-4FE7-96C0-0E4FEFBB66F1} {1914FAAB-9B86-4B76-90CA-E16D3C8984FC} = {1CC73AD2-CFC9-4FE7-96C0-0E4FEFBB66F1} + {9A614AAB-D386-4D5F-971B-33BA9E820AF8} = {5961A5E0-54A6-42F5-92A2-9A9E48DE2878} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2E9F4217-7A58-48A4-9850-84CD0CDA31DA} diff --git a/src/FluentCMS.slnx b/src/FluentCMS.slnx index 8c9095b55..dd7609097 100644 --- a/src/FluentCMS.slnx +++ b/src/FluentCMS.slnx @@ -19,6 +19,7 @@ + diff --git a/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/Component/TailwindStyleBuilder.razor b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/Component/TailwindStyleBuilder.razor new file mode 100644 index 000000000..4ae3cbe14 --- /dev/null +++ b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/Component/TailwindStyleBuilder.razor @@ -0,0 +1,2 @@ +@namespace FluentCMS.Web.UI.TailwindStyleBuilder +@rendermode RenderMode.InteractiveServer diff --git a/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilder.razor.cs b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/Component/TailwindStyleBuilder.razor.cs similarity index 57% rename from src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilder.razor.cs rename to src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/Component/TailwindStyleBuilder.razor.cs index 20d079139..48afe6778 100644 --- a/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilder.razor.cs +++ b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/Component/TailwindStyleBuilder.razor.cs @@ -1,17 +1,18 @@ +using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; -namespace FluentCMS.Web.UI; +namespace FluentCMS.Web.UI.TailwindStyleBuilder; public partial class TailwindStyleBuilder : IAsyncDisposable { [Inject] - private ViewState ViewState { get; set; } = default!; + public IJSRuntime JS { get; set; } = default!; - [Inject] - private ApiClientFactory ApiClient { get; set; } = default!; + [Parameter] + public EventCallback OnCssGenerated { get; set; } = default!; - [Inject] - public IJSRuntime JS { get; set; } = default!; + [Parameter] + public string Config { get; set; } = "{}"; private IJSObjectReference Module { get; set; } = default!; @@ -23,24 +24,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender) return; DotNetRef = DotNetObjectReference.Create(this); - Module = await JS.InvokeAsync("import", "/_content/FluentCMS.Web.UI/Components/TailwindStyleBuilder.razor.js"); - - var css = await Module.InvokeAsync("initialize", DotNetRef); + Module = await JS.InvokeAsync("import", "/_content/FluentCMS.Web.UI.TailwindStyleBuilder/TailwindStyleBuilder.js"); - await OnCssGenerated(css); - } - - private async Task OnCssGenerated(string css) - { - var cssFilePath = Path.Combine("wwwroot", "tailwind", ViewState.Site.Id.ToString(), $"{ViewState.Page.Id}.css"); - - var directoryPath = Path.GetDirectoryName(cssFilePath); - if (!Directory.Exists(directoryPath)) - { - Directory.CreateDirectory(directoryPath); - } + var css = await Module.InvokeAsync("initialize", DotNetRef, Config); - await File.WriteAllTextAsync(cssFilePath, css); + await OnCssGenerated.InvokeAsync(css); } public async ValueTask DisposeAsync() diff --git a/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/FluentCMS.Web.UI.TailwindStyleBuilder.csproj b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/FluentCMS.Web.UI.TailwindStyleBuilder.csproj new file mode 100644 index 000000000..b9ddb053a --- /dev/null +++ b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/FluentCMS.Web.UI.TailwindStyleBuilder.csproj @@ -0,0 +1,40 @@ + + + net9.0 + enable + enable + enable + true + FluentCMS.Web.UI.TailwindStyleBuilder + 0.0.1 + Amir Pournasserian + FluentCMS + TailwindStyleBuilder component for blazor. + fluentcms;cms;tailwind;style;core + https://github.com/fluentcms/FluentCMS + https://fluentcms.com + icon.png + MIT + README.md + + + + + + + + + + + + + + PreserveNewest + + + + + + + + diff --git a/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/README.md b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/README.md new file mode 100644 index 000000000..036418b39 --- /dev/null +++ b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/README.md @@ -0,0 +1,150 @@ +# Tailwind Style Builder Component for Blazor + +TailwindStyleBuilder is a utility for Blazor applications that integrates with Tailwind CSS via CDN to generate dynamic CSS classes. It allows developers to dynamically construct styles for their Blazor components, making it ideal for projects with rich, interactive, and dynamic content. The component provides a seamless way to apply Tailwind CSS styling without requiring a full build pipeline. By eliminating the need for a Node.js-based build system, it streamlines the integration of Tailwind CSS into Blazor projects, ensuring efficient and up-to-date design implementation. + +## Features + +- **Dynamic Style Building**: Generate CSS dynamically by specifying Tailwind CSS classes. +- **Tailwind CDN Integration**: Uses the Tailwind CSS CDN to ensure up-to-date styling. +- **Blazor Compatibility**: Designed specifically for Blazor-based projects. +- **Support for Dynamic Content**: Ideal for projects that render content dynamically in Blazor and need corresponding Tailwind CSS styling. + +## Why TailwindStyleBuilder? +At [FluentCMS](https://github.com/FluentCMS/FluentCMS), we leverage Tailwind CSS to build our UIs. Since page content is dynamic and fetched from the database, it requires efficient handling of styles. In our initial approach, we considered building styles on the server-side using Node.js. However, this method proved to be resource-intensive and did not perform well. + +We also explored using the Tailwind CDN for runtime styling. While this can work in some cases, generating styles on the fly during each request negatively impacts page load times and overall performance. + +This is where TailwindStyleBuilder comes in. With TailwindStyleBuilder, we optimize style generation by building the CSS only once when the page content is first updated or visited by an admin. On subsequent visits, we serve the pre-generated CSS file, ensuring fast and efficient page rendering without the overhead of runtime styling. + +By adopting TailwindStyleBuilder, we improve both performance and resource usage, making it an ideal solution for FluentCMS’s dynamic page content. + +## Use Cases + +- Dynamically styled components in Blazor. +- Applications generated base where CSS classes ared on runtime data. +- Projects using Tailwind CSS with Blazor, needing lightweight integration without a full build pipeline. + +## Installation + +To add **TailwindStyleBuilder** to your Blazor project, follow these steps: + +1. Install the package via NuGet: + + ```bash + dotnet add package FluentCMS.Web.UI.TailwindStyleBuilder + ``` + +2. Import the namespace in your Blazor components or pages: + + ```csharp + @using FluentCMS.Web.UI.TailwindStyleBuilder + ``` + + +## Usage + +### Basic Example + +Here’s a simple example of how to use TailwindStyleBuilder to generate styles dynamically: + + +Use the component in Head section of your App.razor file: + +```csharp + + + + + ... + + + + + ... +
+ Hello World! +
+ + + + +``` + +now you are able to use Tailwind in your components. + +### Advanced Example + +While it functions similarly to using pure Tailwind CDN, our component offers an additional feature: the ability to generate CSS code dynamically and write it to the `wwwroot` directory. This allows you to use the generated CSS file directly, reducing runtime dependencies on the CDN and improving performance. + + +To utilize the dynamic CSS generation feature, you can build an interactive component that acts as a wrapper and writes the generated CSS to a file:  + +```csharp +@rendermode RenderMode.InteractiveServer + +@if (System.IO.File.Exists($"wwwroot/{Name}.css")) +{ + +} +else +{ + @* You can pass tailwind config using Config property *@ + +} + +@code { + + [Inject] IWebHostEnvironment Environment { get; set; } = default!; + + [Parameter] + public string Name { get; set; } = "generated"; + + public void OnCssGenerated(string css) + { + var fileName = Name + ".css"; + var filePath = Path.Combine(Environment.WebRootPath, "css", fileName); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); + File.WriteAllText(filePath, css); + } +} +``` +Here is full implementation of Wrapper component used in fluentCMS [TailwindStyleBuilderWrapper.razor](https://github.com/fluentcms/FluentCMS/blob/dev/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilderWrapper.razor) and [TailwindStyleBuilderWrapper.razor.cs](https://github.com/fluentcms/FluentCMS/blob/dev/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilderWrapper.razor.cs) + +Now you can use this component in your page like this: + +```csharp +@page "/example" + + + @* will use example.css if exists, otherwise it will build that file in first visit *@ + + +
+

Hello Tailwind!

+
+ +``` + +## Update Styles + +to update the css files, you need to remove previously generated css file.\ +in above example, when you update /example page,  you should remove /wwwroot/example.css file.  + +## Benefits + +- No need to pre-compile styles or configure a full Tailwind CSS build pipeline. +- Tailwind CSS updates are automatically applied via the CDN. +- Simplifies styling for Blazor components. + +## Limitations + +- CSS generation happens at runtime, which might impact performance for highly dynamic or complex styling requirements. (only first visit) + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests on the [GitHub repository](https://github.com/fluentcms/FluentCMS). + +## License + +This project is licensed under the [MIT License](LICENSE). + diff --git a/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/_Imports.razor b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/_Imports.razor new file mode 100644 index 000000000..77285129d --- /dev/null +++ b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web diff --git a/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/wwwroot/TailwindStyleBuilder.js b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/wwwroot/TailwindStyleBuilder.js new file mode 100644 index 000000000..fe191cbb1 --- /dev/null +++ b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/wwwroot/TailwindStyleBuilder.js @@ -0,0 +1,18 @@ +import "/_content/FluentCMS.Web.UI.TailwindStyleBuilder/tailwind.cdn.js" + +export async function initialize(dotnet, config) +{ + tailwind.config = JSON.parse(config) + await new Promise(resolve => setTimeout(resolve, 1000)); + + const styleTags = document.querySelectorAll('style') + let result = '' + + styleTags.forEach(style => { + if(style.textContent.slice(0, 20).includes('tailwind')){ + result = style.textContent + } + }) + + return result +} \ No newline at end of file diff --git a/src/Frontend/FluentCMS.Web.UI/wwwroot/js/tailwind.cdn.js b/src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/wwwroot/tailwind.cdn.js similarity index 100% rename from src/Frontend/FluentCMS.Web.UI/wwwroot/js/tailwind.cdn.js rename to src/Frontend/FluentCMS.Web.UI.TailwindStyleBuilder/wwwroot/tailwind.cdn.js diff --git a/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilder.razor b/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilder.razor deleted file mode 100644 index 07a502f83..000000000 --- a/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilder.razor +++ /dev/null @@ -1,2 +0,0 @@ -@namespace FluentCMS.Web.UI -@rendermode RenderMode.InteractiveServer diff --git a/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilder.razor.js b/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilder.razor.js deleted file mode 100644 index 8bf6780d0..000000000 --- a/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilder.razor.js +++ /dev/null @@ -1,54 +0,0 @@ -import "/_content/FluentCMS.Web.UI/js/tailwind.cdn.js" - -tailwind.config = { - darkMode: 'class', - theme: { - extend: { - colors: { - primary: { - 50: 'hsl(var(--f-primary-50, var(--f-primary), 100%, 95%))', - 100: 'hsl(var(--f-primary-100, var(--f-primary), 80%, 90%))', - 200: 'hsl(var(--f-primary-200, var(--f-primary), 70%, 80%))', - 300: 'hsl(var(--f-primary-300, var(--f-primary), 60%, 70%))', - 400: 'hsl(var(--f-primary-400, var(--f-primary), 60%, 60%))', - 500: 'hsl(var(--f-primary-500, var(--f-primary), 60%, 50%))', - 600: 'hsl(var(--f-primary-600, var(--f-primary), 80%, 40%))', - 700: 'hsl(var(--f-primary-700, var(--f-primary), 80%, 30%))', - 800: 'hsl(var(--f-primary-800, var(--f-primary), 80%, 20%))', - 900: 'hsl(var(--f-primary-900, var(--f-primary), 90%, 15%))', - }, - surface: { - DEFAULT: 'hsl(var(--f-surface))', - muted: 'hsl(var(--f-surface-muted))', - accent: 'hsl(var(--f-surface-accent))', - }, - content: { - DEFAULT: 'hsl(var(--f-content))', - muted: 'hsl(var(--f-content-muted))', - accent: 'hsl(var(--f-content-accent))' - }, - border: { - DEFAULT: 'hsl(var(--f-border))', - muted: 'hsl(var(--f-border-muted))', - accent: 'hsl(var(--f-border-accent))', - } - } - }, - } -} - -export async function initialize(dotnet) -{ - await new Promise(resolve => setTimeout(resolve, 1000)); - - const styleTags = document.querySelectorAll('style') - let result = '' - - styleTags.forEach(style => { - if(style.textContent.slice(0, 20).includes('tailwind')){ - result = style.textContent - } - }) - - return result -} \ No newline at end of file diff --git a/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilderWrapper.razor b/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilderWrapper.razor new file mode 100644 index 000000000..fd0200476 --- /dev/null +++ b/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilderWrapper.razor @@ -0,0 +1,5 @@ +@namespace FluentCMS.Web.UI +@rendermode RenderMode.InteractiveServer +@using FluentCMS.Web.UI.TailwindStyleBuilder + + \ No newline at end of file diff --git a/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilderWrapper.razor.cs b/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilderWrapper.razor.cs new file mode 100644 index 000000000..7cdabad26 --- /dev/null +++ b/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyleBuilderWrapper.razor.cs @@ -0,0 +1,60 @@ +using Microsoft.JSInterop; + +namespace FluentCMS.Web.UI; + +public partial class TailwindStyleBuilderWrapper +{ + [Inject] + private ViewState ViewState { get; set; } = default!; + + public string Config => @" + { + ""darkMode"": ""class"", + ""theme"": { + ""extend"": { + ""colors"": { + ""primary"": { + ""50"": ""hsl(var(--f-primary-50, var(--f-primary), 100%, 95%))"", + ""100"": ""hsl(var(--f-primary-100, var(--f-primary), 80%, 90%))"", + ""200"": ""hsl(var(--f-primary-200, var(--f-primary), 70%, 80%))"", + ""300"": ""hsl(var(--f-primary-300, var(--f-primary), 60%, 70%))"", + ""400"": ""hsl(var(--f-primary-400, var(--f-primary), 60%, 60%))"", + ""500"": ""hsl(var(--f-primary-500, var(--f-primary), 60%, 50%))"", + ""600"": ""hsl(var(--f-primary-600, var(--f-primary), 80%, 40%))"", + ""700"": ""hsl(var(--f-primary-700, var(--f-primary), 80%, 30%))"", + ""800"": ""hsl(var(--f-primary-800, var(--f-primary), 80%, 20%))"", + ""900"": ""hsl(var(--f-primary-900, var(--f-primary), 90%, 15%))"" + }, + ""surface"": { + ""DEFAULT"": ""hsl(var(--f-surface))"", + ""muted"": ""hsl(var(--f-surface-muted))"", + ""accent"": ""hsl(var(--f-surface-accent))"" + }, + ""content"": { + ""DEFAULT"": ""hsl(var(--f-content))"", + ""muted"": ""hsl(var(--f-content-muted))"", + ""accent"": ""hsl(var(--f-content-accent))"" + }, + ""border"": { + ""DEFAULT"": ""hsl(var(--f-border))"", + ""muted"": ""hsl(var(--f-border-muted))"", + ""accent"": ""hsl(var(--f-border-accent))"" + } + } + } + } + }"; + + private async Task OnCssGenerated(string css) + { + var cssFilePath = Path.Combine("wwwroot", "tailwind", ViewState.Site.Id.ToString(), $"{ViewState.Page.Id}.css"); + + var directoryPath = Path.GetDirectoryName(cssFilePath); + if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + await File.WriteAllTextAsync(cssFilePath, css); + } +} diff --git a/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyles.razor b/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyles.razor index 8448fc022..178336e55 100644 --- a/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyles.razor +++ b/src/Frontend/FluentCMS.Web.UI/Components/TailwindStyles.razor @@ -46,5 +46,5 @@ } else { - + } diff --git a/src/Frontend/FluentCMS.Web.UI/FluentCMS.Web.UI.csproj b/src/Frontend/FluentCMS.Web.UI/FluentCMS.Web.UI.csproj index 398d66592..067344a5f 100644 --- a/src/Frontend/FluentCMS.Web.UI/FluentCMS.Web.UI.csproj +++ b/src/Frontend/FluentCMS.Web.UI/FluentCMS.Web.UI.csproj @@ -36,6 +36,7 @@ + diff --git a/src/Frontend/FluentCMS.Web.UI/SetupPage.razor b/src/Frontend/FluentCMS.Web.UI/SetupPage.razor index 724a2f6e4..b5a5f2c80 100644 --- a/src/Frontend/FluentCMS.Web.UI/SetupPage.razor +++ b/src/Frontend/FluentCMS.Web.UI/SetupPage.razor @@ -17,7 +17,7 @@ - +