🧱 Render email based on razor components
This commit is contained in:
parent
cbef69ba5e
commit
d3b56b741e
80
DysonNetwork.Sphere/Account/Email/EmailService.cs
Normal file
80
DysonNetwork.Sphere/Account/Email/EmailService.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using MailKit.Net.Smtp;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MimeKit;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Account.Email;
|
||||||
|
|
||||||
|
public class EmailServiceConfiguration
|
||||||
|
{
|
||||||
|
public string Server { get; set; } = null!;
|
||||||
|
public int Port { get; set; }
|
||||||
|
public bool UseSsl { get; set; }
|
||||||
|
public string Username { get; set; } = null!;
|
||||||
|
public string Password { get; set; } = null!;
|
||||||
|
public string FromAddress { get; set; } = null!;
|
||||||
|
public string FromName { get; set; } = null!;
|
||||||
|
public string SubjectPrefix { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EmailService
|
||||||
|
{
|
||||||
|
private readonly EmailServiceConfiguration _configuration;
|
||||||
|
private readonly RazorViewRenderer _viewRenderer;
|
||||||
|
private readonly ILogger<EmailService> _logger;
|
||||||
|
|
||||||
|
public EmailService(IConfiguration configuration, RazorViewRenderer viewRenderer, ILogger<EmailService> logger)
|
||||||
|
{
|
||||||
|
var cfg = configuration.GetSection("Email").Get<EmailServiceConfiguration>();
|
||||||
|
_configuration = cfg ?? throw new ArgumentException("Email service was not configured.");
|
||||||
|
_viewRenderer = viewRenderer;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody)
|
||||||
|
{
|
||||||
|
await SendEmailAsync(recipientName, recipientEmail, subject, textBody, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody,
|
||||||
|
string? htmlBody)
|
||||||
|
{
|
||||||
|
subject = $"[{_configuration.SubjectPrefix}] {subject}";
|
||||||
|
|
||||||
|
var emailMessage = new MimeMessage();
|
||||||
|
emailMessage.From.Add(new MailboxAddress(_configuration.FromName, _configuration.FromAddress));
|
||||||
|
emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail));
|
||||||
|
emailMessage.Subject = subject;
|
||||||
|
|
||||||
|
var bodyBuilder = new BodyBuilder
|
||||||
|
{
|
||||||
|
TextBody = textBody
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(htmlBody))
|
||||||
|
bodyBuilder.HtmlBody = htmlBody;
|
||||||
|
|
||||||
|
emailMessage.Body = bodyBuilder.ToMessageBody();
|
||||||
|
|
||||||
|
using var client = new SmtpClient();
|
||||||
|
await client.ConnectAsync(_configuration.Server, _configuration.Port, _configuration.UseSsl);
|
||||||
|
await client.AuthenticateAsync(_configuration.Username, _configuration.Password);
|
||||||
|
await client.SendAsync(emailMessage);
|
||||||
|
await client.DisconnectAsync(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendTemplatedEmailAsync<TComponent, TModel>(string? recipientName, string recipientEmail,
|
||||||
|
string subject, TModel model, string fallbackTextBody)
|
||||||
|
where TComponent : IComponent
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var htmlBody = await _viewRenderer.RenderComponentToStringAsync<TComponent, TModel>(model);
|
||||||
|
await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody, htmlBody);
|
||||||
|
}
|
||||||
|
catch (Exception err)
|
||||||
|
{
|
||||||
|
_logger.LogError(err, "Failed to render email template...");
|
||||||
|
await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
DysonNetwork.Sphere/Account/Email/LandingEmail.cs
Normal file
7
DysonNetwork.Sphere/Account/Email/LandingEmail.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace DysonNetwork.Sphere.Account.Email;
|
||||||
|
|
||||||
|
public class LandingEmailModel
|
||||||
|
{
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public required string VerificationLink { get; set; }
|
||||||
|
}
|
47
DysonNetwork.Sphere/Account/Email/RazorViewRenderer.cs
Normal file
47
DysonNetwork.Sphere/Account/Email/RazorViewRenderer.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
|
using RouteData = Microsoft.AspNetCore.Routing.RouteData;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Account.Email;
|
||||||
|
|
||||||
|
public class RazorViewRenderer(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
ILogger<RazorViewRenderer> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public async Task<string> RenderComponentToStringAsync<TComponent, TModel>(TModel model)
|
||||||
|
where TComponent : IComponent
|
||||||
|
{
|
||||||
|
await using var htmlRenderer = new HtmlRenderer(serviceProvider, loggerFactory);
|
||||||
|
|
||||||
|
var viewDictionary = new ViewDataDictionary<TModel>(
|
||||||
|
new EmptyModelMetadataProvider(),
|
||||||
|
new ModelStateDictionary())
|
||||||
|
{
|
||||||
|
Model = model
|
||||||
|
};
|
||||||
|
|
||||||
|
return await htmlRenderer.Dispatcher.InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parameterView = ParameterView.FromDictionary(viewDictionary);
|
||||||
|
var output = await htmlRenderer.RenderComponentAsync<TComponent>(parameterView);
|
||||||
|
return output.ToHtmlString();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error rendering component {ComponentName}", typeof(TComponent).Name);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,49 +0,0 @@
|
|||||||
using MailKit.Net.Smtp;
|
|
||||||
using MimeKit;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
|
||||||
|
|
||||||
public class EmailServiceConfiguration
|
|
||||||
{
|
|
||||||
public string Server { get; set; }
|
|
||||||
public int Port { get; set; }
|
|
||||||
public string Username { get; set; }
|
|
||||||
public string Password { get; set; }
|
|
||||||
public string FromAddress { get; set; }
|
|
||||||
public string FromName { get; set; }
|
|
||||||
public string SubjectPrefix { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EmailService
|
|
||||||
{
|
|
||||||
private readonly EmailServiceConfiguration _configuration;
|
|
||||||
|
|
||||||
public EmailService(IConfiguration configuration)
|
|
||||||
{
|
|
||||||
var cfg = configuration.GetSection("Email").Get<EmailServiceConfiguration>();
|
|
||||||
_configuration = cfg ?? throw new ArgumentException("Email service was not configured.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody)
|
|
||||||
{
|
|
||||||
subject = $"[{_configuration.SubjectPrefix}] {subject}";
|
|
||||||
|
|
||||||
var emailMessage = new MimeMessage();
|
|
||||||
emailMessage.From.Add(new MailboxAddress(_configuration.FromName, _configuration.FromAddress));
|
|
||||||
emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail));
|
|
||||||
emailMessage.Subject = subject;
|
|
||||||
|
|
||||||
var bodyBuilder = new BodyBuilder
|
|
||||||
{
|
|
||||||
TextBody = textBody
|
|
||||||
};
|
|
||||||
|
|
||||||
emailMessage.Body = bodyBuilder.ToMessageBody();
|
|
||||||
|
|
||||||
using var client = new SmtpClient();
|
|
||||||
await client.ConnectAsync(_configuration.Server, _configuration.Port, true);
|
|
||||||
await client.AuthenticateAsync(_configuration.Username, _configuration.Password);
|
|
||||||
await client.SendAsync(emailMessage);
|
|
||||||
await client.DisconnectAsync(true);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,13 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using DysonNetwork.Sphere.Account.Email;
|
||||||
|
using DysonNetwork.Sphere.Pages.Emails;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Account;
|
namespace DysonNetwork.Sphere.Account;
|
||||||
|
|
||||||
public class MagicSpellService(AppDatabase db, EmailService email, ILogger<MagicSpellService> logger)
|
public class MagicSpellService(AppDatabase db, EmailService email, IConfiguration configuration, ILogger<MagicSpellService> logger)
|
||||||
{
|
{
|
||||||
public async Task<MagicSpell> CreateMagicSpell(
|
public async Task<MagicSpell> CreateMagicSpell(
|
||||||
Account account,
|
Account account,
|
||||||
@ -42,23 +44,25 @@ public class MagicSpellService(AppDatabase db, EmailService email, ILogger<Magic
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (contact is null) throw new ArgumentException("Account has no contact method that can use");
|
if (contact is null) throw new ArgumentException("Account has no contact method that can use");
|
||||||
|
|
||||||
// TODO replace the baseurl
|
var link = $"${configuration.GetValue<string>("BaseUrl")}/spells/{Uri.EscapeDataString(spell.Spell)}";
|
||||||
var link = $"https://api.sn.solsynth.dev/spells/{Uri.EscapeDataString(spell.Spell)}";
|
|
||||||
|
|
||||||
logger.LogError($"Sending magic spell... {link}");
|
logger.LogInformation($"Sending magic spell... {link}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
switch (spell.Type)
|
switch (spell.Type)
|
||||||
{
|
{
|
||||||
case MagicSpellType.AccountActivation:
|
case MagicSpellType.AccountActivation:
|
||||||
await email.SendEmailAsync(
|
await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
|
||||||
contact.Account.Name,
|
contact.Account.Name,
|
||||||
contact.Content,
|
contact.Content,
|
||||||
"Confirm your registration",
|
"Confirm your registration",
|
||||||
"Thank you for creating an account.\n" +
|
new LandingEmailModel
|
||||||
"For accessing all the features, confirm your registration with the link below:\n\n" +
|
{
|
||||||
$"{link}"
|
Name = contact.Account.Name,
|
||||||
|
VerificationLink = link
|
||||||
|
},
|
||||||
|
$"Thank you for creating an account.\nFor accessing all the features, confirm your registration with the link below:\n\n{link}"
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
18
DysonNetwork.Sphere/Pages/Emails/EmailLayout.razor
Normal file
18
DysonNetwork.Sphere/Pages/Emails/EmailLayout.razor
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<div class="min-h-screen bg-gray-100 py-8 font-sans">
|
||||||
|
<div class="max-w-2xl mx-auto px-8">
|
||||||
|
<div class="bg-white rounded-lg shadow-md p-6">
|
||||||
|
@ChildContent
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-6 text-sm text-gray-500">
|
||||||
|
<p class="m-0">© @DateTime.Now.Year DysonNetwork. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment? ChildContent { get; set; }
|
||||||
|
}
|
47
DysonNetwork.Sphere/Pages/Emails/LandingEmail.razor
Normal file
47
DysonNetwork.Sphere/Pages/Emails/LandingEmail.razor
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<EmailLayout>
|
||||||
|
<div style="text-align: center; margin-bottom: 2rem;">
|
||||||
|
<h1 style="font-size: 1.875rem; font-weight: 700; color: #111827; margin: 0;">Welcome to DysonNetwork!</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
Dear @Name,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
Thank you for creating an account with DysonNetwork. We're excited to have you join our community!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
To access all features and ensure the security of your account, please confirm your registration by clicking
|
||||||
|
the button below:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin: 2rem 0;">
|
||||||
|
<a href="@VerificationLink"
|
||||||
|
style="display: inline-block; padding: 0.75rem 1.5rem; background-color: #2563eb; color: #ffffff; text-decoration: none; font-weight: 600; border-radius: 0.5rem;">
|
||||||
|
Confirm Registration
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
If the button doesn't work, you can also copy and paste this link into your browser:
|
||||||
|
<br>
|
||||||
|
<span style="color: #2563eb; word-break: break-all;">@VerificationLink</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="color: #374151; margin: 0;">
|
||||||
|
If you didn't create this account, please ignore this email.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="color: #374151; margin: 2rem 0 0 0;">
|
||||||
|
Best regards,<br>
|
||||||
|
The DysonNetwork Team
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</EmailLayout>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public required string Name { get; set; }
|
||||||
|
[Parameter] public required string VerificationLink { get; set; }
|
||||||
|
}
|
19
DysonNetwork.Sphere/Pages/Emails/_EmailLayout.cshtml
Normal file
19
DysonNetwork.Sphere/Pages/Emails/_EmailLayout.cshtml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>@ViewData["Title"]</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; background-color: #f3f4f6; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
|
||||||
|
<div style="max-width: 42rem; margin: 0 auto; padding: 2rem;">
|
||||||
|
<div style="background-color: #ffffff; border-radius: 0.5rem; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); padding: 1.5rem;">
|
||||||
|
@RenderBody()
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 1.5rem; font-size: 0.875rem; color: #6b7280;">
|
||||||
|
<p style="margin: 0;">© @DateTime.Now.Year DysonNetwork. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,11 +1,11 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.RateLimiting;
|
using System.Threading.RateLimiting;
|
||||||
using DysonNetwork.Sphere;
|
using DysonNetwork.Sphere;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Account.Email;
|
||||||
using DysonNetwork.Sphere.Activity;
|
using DysonNetwork.Sphere.Activity;
|
||||||
using DysonNetwork.Sphere.Auth;
|
using DysonNetwork.Sphere.Auth;
|
||||||
using DysonNetwork.Sphere.Chat;
|
using DysonNetwork.Sphere.Chat;
|
||||||
@ -20,12 +20,11 @@ using DysonNetwork.Sphere.Sticker;
|
|||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Sphere.Wallet;
|
using DysonNetwork.Sphere.Wallet;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.AspNetCore.Localization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.SystemTextJson;
|
using NodaTime.Serialization.SystemTextJson;
|
||||||
@ -34,7 +33,6 @@ using tusdotnet;
|
|||||||
using tusdotnet.Models;
|
using tusdotnet.Models;
|
||||||
using tusdotnet.Models.Configuration;
|
using tusdotnet.Models.Configuration;
|
||||||
using tusdotnet.Stores;
|
using tusdotnet.Stores;
|
||||||
using File = System.IO.File;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@ -141,6 +139,7 @@ builder.Services.AddSingleton(tusDiskStore);
|
|||||||
builder.Services.AddScoped<IWebSocketPacketHandler, MessageReadHandler>();
|
builder.Services.AddScoped<IWebSocketPacketHandler, MessageReadHandler>();
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
|
builder.Services.AddScoped<RazorViewRenderer>();
|
||||||
builder.Services.Configure<GeoIpOptions>(builder.Configuration.GetSection("GeoIP"));
|
builder.Services.Configure<GeoIpOptions>(builder.Configuration.GetSection("GeoIP"));
|
||||||
builder.Services.AddScoped<GeoIpService>();
|
builder.Services.AddScoped<GeoIpService>();
|
||||||
builder.Services.AddScoped<WebSocketService>();
|
builder.Services.AddScoped<WebSocketService>();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"Debug": true,
|
"Debug": true,
|
||||||
|
"BaseUrl": "http://localhost:5017",
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
@ -63,10 +64,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Email": {
|
"Email": {
|
||||||
"Server": "smtpdm.aliyun.com",
|
"Server": "smtp4dev.orb.local",
|
||||||
"Port": 465,
|
"Port": 25,
|
||||||
|
"UseSsl": false,
|
||||||
"Username": "no-reply@mail.solsynth.dev",
|
"Username": "no-reply@mail.solsynth.dev",
|
||||||
"Password": "Pe1UeV405PMcQZgv",
|
"Password": "password",
|
||||||
"FromAddress": "no-reply@mail.solsynth.dev",
|
"FromAddress": "no-reply@mail.solsynth.dev",
|
||||||
"FromName": "Alphabot",
|
"FromName": "Alphabot",
|
||||||
"SubjectPrefix": "Solar Network"
|
"SubjectPrefix": "Solar Network"
|
||||||
|
@ -227,9 +227,15 @@
|
|||||||
.z-50 {
|
.z-50 {
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
.m-0 {
|
||||||
|
margin: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
.mx-auto {
|
.mx-auto {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
.my-8 {
|
||||||
|
margin-block: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.mt-4 {
|
.mt-4 {
|
||||||
margin-top: calc(var(--spacing) * 4);
|
margin-top: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@ -269,6 +275,9 @@
|
|||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.inline-block {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
.table {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
@ -281,6 +290,9 @@
|
|||||||
.h-full {
|
.h-full {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.min-h-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -305,6 +317,13 @@
|
|||||||
.justify-center {
|
.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.space-y-6 {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));
|
||||||
|
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
.gap-x-6 {
|
.gap-x-6 {
|
||||||
column-gap: calc(var(--spacing) * 6);
|
column-gap: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
@ -314,6 +333,12 @@
|
|||||||
.bg-blue-500 {
|
.bg-blue-500 {
|
||||||
background-color: var(--color-blue-500);
|
background-color: var(--color-blue-500);
|
||||||
}
|
}
|
||||||
|
.bg-blue-600 {
|
||||||
|
background-color: var(--color-blue-600);
|
||||||
|
}
|
||||||
|
.bg-gray-100 {
|
||||||
|
background-color: var(--color-gray-100);
|
||||||
|
}
|
||||||
.bg-green-100 {
|
.bg-green-100 {
|
||||||
background-color: var(--color-green-100);
|
background-color: var(--color-green-100);
|
||||||
}
|
}
|
||||||
@ -329,21 +354,33 @@
|
|||||||
.p-6 {
|
.p-6 {
|
||||||
padding: calc(var(--spacing) * 6);
|
padding: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.p-8 {
|
||||||
|
padding: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.px-4 {
|
.px-4 {
|
||||||
padding-inline: calc(var(--spacing) * 4);
|
padding-inline: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
.px-6 {
|
.px-6 {
|
||||||
padding-inline: calc(var(--spacing) * 6);
|
padding-inline: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.px-8 {
|
||||||
|
padding-inline: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.py-3 {
|
.py-3 {
|
||||||
padding-block: calc(var(--spacing) * 3);
|
padding-block: calc(var(--spacing) * 3);
|
||||||
}
|
}
|
||||||
|
.py-8 {
|
||||||
|
padding-block: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.py-12 {
|
.py-12 {
|
||||||
padding-block: calc(var(--spacing) * 12);
|
padding-block: calc(var(--spacing) * 12);
|
||||||
}
|
}
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.font-sans {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
.text-2xl {
|
.text-2xl {
|
||||||
font-size: var(--text-2xl);
|
font-size: var(--text-2xl);
|
||||||
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||||
@ -368,6 +405,10 @@
|
|||||||
font-size: var(--text-xl);
|
font-size: var(--text-xl);
|
||||||
line-height: var(--tw-leading, var(--text-xl--line-height));
|
line-height: var(--tw-leading, var(--text-xl--line-height));
|
||||||
}
|
}
|
||||||
|
.leading-6 {
|
||||||
|
--tw-leading: calc(var(--spacing) * 6);
|
||||||
|
line-height: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.leading-8 {
|
.leading-8 {
|
||||||
--tw-leading: calc(var(--spacing) * 8);
|
--tw-leading: calc(var(--spacing) * 8);
|
||||||
line-height: calc(var(--spacing) * 8);
|
line-height: calc(var(--spacing) * 8);
|
||||||
@ -388,6 +429,12 @@
|
|||||||
--tw-tracking: var(--tracking-tight);
|
--tw-tracking: var(--tracking-tight);
|
||||||
letter-spacing: var(--tracking-tight);
|
letter-spacing: var(--tracking-tight);
|
||||||
}
|
}
|
||||||
|
.break-all {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.text-blue-600 {
|
||||||
|
color: var(--color-blue-600);
|
||||||
|
}
|
||||||
.text-gray-500 {
|
.text-gray-500 {
|
||||||
color: var(--color-gray-500);
|
color: var(--color-gray-500);
|
||||||
}
|
}
|
||||||
@ -420,6 +467,10 @@
|
|||||||
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
.shadow-md {
|
||||||
|
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
.shadow-sm {
|
.shadow-sm {
|
||||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
@ -453,6 +504,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:bg-blue-700 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-blue-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:text-blue-600 {
|
.hover\:text-blue-600 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@ -460,6 +518,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:text-gray-700 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.focus\:ring-2 {
|
.focus\:ring-2 {
|
||||||
&:focus {
|
&:focus {
|
||||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
@ -503,6 +568,11 @@
|
|||||||
background-color: var(--color-yellow-900);
|
background-color: var(--color-yellow-900);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:text-gray-100 {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
color: var(--color-gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
.dark\:text-gray-300 {
|
.dark\:text-gray-300 {
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
color: var(--color-gray-300);
|
color: var(--color-gray-300);
|
||||||
@ -537,6 +607,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dark\:hover\:text-gray-300 {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
color: var(--color-gray-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@layer theme, base, components, utilities;
|
@layer theme, base, components, utilities;
|
||||||
@layer theme;
|
@layer theme;
|
||||||
@ -779,6 +858,11 @@
|
|||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
@property --tw-space-y-reverse {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
@property --tw-leading {
|
@property --tw-leading {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@ -932,6 +1016,7 @@
|
|||||||
--tw-rotate-z: initial;
|
--tw-rotate-z: initial;
|
||||||
--tw-skew-x: initial;
|
--tw-skew-x: initial;
|
||||||
--tw-skew-y: initial;
|
--tw-skew-y: initial;
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
--tw-leading: initial;
|
--tw-leading: initial;
|
||||||
--tw-font-weight: initial;
|
--tw-font-weight: initial;
|
||||||
--tw-tracking: initial;
|
--tw-tracking: initial;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user