mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 21:40:41 -04:00
Add click to copy and form validation (#181)
This commit is contained in:
committed by
Leendert de Borst
parent
697abc6828
commit
a53575b4bf
@@ -36,29 +36,34 @@
|
||||
@if (IsAddFormVisible)
|
||||
{
|
||||
<div class="p-4 mb-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-white">Add 2FA TOTP Code</h4>
|
||||
<button @onclick="HideAddForm" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
<span class="sr-only">Close form</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">If the website offers or requires 2FA for your account (such as Google Authenticator), you can use AliasVault instead to generate the codes for you.</p>
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name</label>
|
||||
<input type="text" id="name" @bind="NewTotpCode.Name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="Service Name" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="secretKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key (32 characters)</label>
|
||||
<input type="text" id="secretKey" @bind="NewTotpCode.SecretKey" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="Enter 32-character code" required>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="button" @onclick="AddTotpCode" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<EditForm Model="@NewTotpCode" OnValidSubmit="AddTotpCode">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-white">Add 2FA TOTP Code</h4>
|
||||
<button @onclick="HideAddForm" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
<span class="sr-only">Close form</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">If the website offers or requires 2FA for your account (such as Google Authenticator), you can use AliasVault instead to generate the codes for you.</p>
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Name (optional)</label>
|
||||
<InputText id="name" @bind-Value="NewTotpCode.Name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" />
|
||||
<ValidationMessage For="@(() => NewTotpCode.Name)" class="text-red-600 dark:text-red-400 text-sm mt-1" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="secretKey" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Secret Key</label>
|
||||
<InputText id="secretKey" @bind-Value="NewTotpCode.SecretKey" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="Enter secret key (manual entry)" />
|
||||
<ValidationMessage For="@(() => NewTotpCode.SecretKey)" class="text-red-600 dark:text-red-400 text-sm mt-1" />
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -102,12 +107,6 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The service name.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string ServiceName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The list of TOTP codes.
|
||||
/// </summary>
|
||||
@@ -122,7 +121,7 @@
|
||||
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private bool IsAddFormVisible { get; set; } = false;
|
||||
private TotpCode NewTotpCode { get; set; } = new();
|
||||
private TotpCodeEdit NewTotpCode { get; set; } = new();
|
||||
private Timer? _refreshTimer;
|
||||
private Dictionary<string, string> _currentCodes = new();
|
||||
private List<Guid> OriginalTotpCodeIds { get; set; } = [];
|
||||
@@ -208,10 +207,7 @@
|
||||
|
||||
private void ShowAddForm()
|
||||
{
|
||||
NewTotpCode = new TotpCode
|
||||
{
|
||||
Name = ServiceName
|
||||
};
|
||||
NewTotpCode = new TotpCodeEdit();
|
||||
IsAddFormVisible = true;
|
||||
}
|
||||
|
||||
@@ -222,22 +218,53 @@
|
||||
|
||||
private async Task AddTotpCode()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewTotpCode.Name))
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Name is required.", true);
|
||||
return;
|
||||
}
|
||||
string secretKey = NewTotpCode.SecretKey;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(NewTotpCode.SecretKey))
|
||||
// Sanitize the secret key (remove whitespace and hyphens)
|
||||
secretKey = secretKey.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
|
||||
string? name = NewTotpCode.Name;
|
||||
|
||||
// Check if the input is a TOTP URI
|
||||
if (secretKey.StartsWith("otpauth://totp/"))
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Secret key is required.", true);
|
||||
return;
|
||||
try
|
||||
{
|
||||
var uri = new Uri(secretKey);
|
||||
var queryParams = System.Web.HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
// Extract the secret from query parameters
|
||||
secretKey = queryParams["secret"] ?? throw new Exception("Secret not found in URI");
|
||||
|
||||
// If no name was provided, try to get it from the URI
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
// The label is everything after 'totp/' and before '?'
|
||||
var label = uri.AbsolutePath.TrimStart('/');
|
||||
// If the label contains ':', take the part after it
|
||||
name = label.Contains(':') ? label.Split(':')[1] : label;
|
||||
|
||||
// If there's an issuer in the query params, use it as a prefix
|
||||
var issuer = queryParams["issuer"];
|
||||
if (!string.IsNullOrWhiteSpace(issuer))
|
||||
{
|
||||
name = $"{issuer}: {name}";
|
||||
}
|
||||
NewTotpCode.Name = name;
|
||||
}
|
||||
NewTotpCode.SecretKey = secretKey;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Invalid TOTP URI format. Please check and try again.", true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Validate the secret key by trying to generate a code
|
||||
TotpGenerator.GenerateTotpCode(NewTotpCode.SecretKey);
|
||||
TotpGenerator.GenerateTotpCode(secretKey);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -246,14 +273,8 @@
|
||||
}
|
||||
|
||||
// Create a new TOTP code in memory
|
||||
var newTotpCode = new TotpCode
|
||||
{
|
||||
Id = Guid.Empty,
|
||||
Name = NewTotpCode.Name,
|
||||
SecretKey = NewTotpCode.SecretKey,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
var newTotpCode = NewTotpCode.ToEntity();
|
||||
newTotpCode.Name = name ?? "Authenticator";
|
||||
|
||||
// Add to the list
|
||||
TotpCodeList.Add(newTotpCode);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@inherits ComponentBase
|
||||
@inject TotpCodeService TotpCodeService
|
||||
@inject ClipboardCopyService ClipboardCopyService
|
||||
@inject JsInteropService JsInteropService
|
||||
@implements IDisposable
|
||||
@using TotpGenerator
|
||||
|
||||
@@ -32,8 +34,19 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-col items-end">
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-white">@GetTotpCode(totpCode.SecretKey)</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">@GetRemainingSeconds()s</div>
|
||||
<div class="text-2xl font-bold cursor-pointer text-gray-900 dark:text-white hover:text-primary-600 dark:hover:text-primary-400 transition-colors" @onclick="() => CopyToClipboard(totpCode)">
|
||||
@GetTotpCode(totpCode.SecretKey)
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
@if (IsCopied(totpCode.Id.ToString()))
|
||||
{
|
||||
<span class="text-green-600 dark:text-green-400">Copied!</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-gray-500 dark:text-gray-400">@GetRemainingSeconds()s</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1.5 h-8 bg-gray-200 rounded-full dark:bg-gray-600">
|
||||
<div class="bg-blue-600 rounded-full transition-all" style="height: @(GetRemainingPercentage())%; width: 100%"></div>
|
||||
@@ -118,4 +131,22 @@
|
||||
// Invert the percentage so it counts down instead of up
|
||||
return (int)(((30.0 - remaining) / 30.0) * 100);
|
||||
}
|
||||
|
||||
private async Task CopyToClipboard(TotpCode totpCode)
|
||||
{
|
||||
var code = GetTotpCode(totpCode.SecretKey);
|
||||
await JsInteropService.CopyToClipboard(code);
|
||||
ClipboardCopyService.SetCopied(totpCode.Id.ToString());
|
||||
StateHasChanged();
|
||||
|
||||
// After 2 seconds, reset the copied state
|
||||
await Task.Delay(2000);
|
||||
if (ClipboardCopyService.GetCopiedId() == totpCode.Id.ToString())
|
||||
{
|
||||
ClipboardCopyService.SetCopied(string.Empty);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private bool IsCopied(string code) => ClipboardCopyService.GetCopiedId() == code;
|
||||
}
|
||||
|
||||
66
src/AliasVault.Client/Main/Models/TotpCodeEdit.cs
Normal file
66
src/AliasVault.Client/Main/Models/TotpCodeEdit.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TotpCodeEdit.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Main.Models;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasClientDb;
|
||||
|
||||
/// <summary>
|
||||
/// Credential edit model.
|
||||
/// </summary>
|
||||
public sealed class TotpCodeEdit
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Id of the TOTP code.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the TOTP code.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret key of the TOTP code.
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "Secret key is required")]
|
||||
public string SecretKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the created at date of the TOTP code.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the updated at date of the TOTP code.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the TOTP code is deleted.
|
||||
/// </summary>
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the edit model to a TotpCode entity.
|
||||
/// </summary>
|
||||
/// <returns>The TotpCode entity.</returns>
|
||||
public TotpCode ToEntity()
|
||||
{
|
||||
return new TotpCode
|
||||
{
|
||||
Id = Id,
|
||||
Name = Name ?? string.Empty,
|
||||
SecretKey = SecretKey,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
IsDeleted = IsDeleted,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -47,13 +47,13 @@ else
|
||||
@if (EditMode && Id.HasValue)
|
||||
{
|
||||
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
||||
<TotpCodes ServiceName="@Obj.ServiceName" TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" />
|
||||
<TotpCodes TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
||||
<TotpCodes ServiceName="@Obj.ServiceName" TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" />
|
||||
<TotpCodes TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -674,10 +674,6 @@ video {
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.top-0 {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.-z-10 {
|
||||
z-index: -10;
|
||||
}
|
||||
@@ -809,6 +805,10 @@ video {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.ms-auto {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
@@ -837,10 +837,6 @@ video {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.ms-auto {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
@@ -944,10 +940,6 @@ video {
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.max-h-full {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.min-h-\[250px\] {
|
||||
min-height: 250px;
|
||||
}
|
||||
@@ -956,6 +948,14 @@ video {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-1 {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
.w-1\.5 {
|
||||
width: 0.375rem;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -1016,14 +1016,6 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-1 {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
.w-1\.5 {
|
||||
width: 0.375rem;
|
||||
}
|
||||
|
||||
.min-w-0 {
|
||||
min-width: 0px;
|
||||
}
|
||||
@@ -1072,11 +1064,6 @@ video {
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
.rotate-180 {
|
||||
--tw-rotate: 180deg;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.transform {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
@@ -1346,11 +1333,6 @@ video {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-t {
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-top-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
@@ -1458,6 +1440,16 @@ video {
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
@@ -1573,6 +1565,10 @@ video {
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
@@ -1583,25 +1579,6 @@ video {
|
||||
background-color: rgb(254 252 232 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-black {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -2078,6 +2055,12 @@ video {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-colors {
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -2090,12 +2073,6 @@ video {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.duration-150 {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
@@ -2128,6 +2105,11 @@ video {
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-blue-800:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-100:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
@@ -2208,11 +2190,6 @@ video {
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-blue-800:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:from-primary-600:hover {
|
||||
--tw-gradient-from: #d68338 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(214 131 56 / 0) var(--tw-gradient-to-position);
|
||||
@@ -2268,16 +2245,16 @@ video {
|
||||
color: rgb(185 28 28 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-white:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-red-800:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(153 27 27 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-white:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -2408,6 +2385,11 @@ video {
|
||||
border-color: rgb(156 163 175 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(107 114 128 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-600:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-border-opacity));
|
||||
@@ -2448,9 +2430,9 @@ video {
|
||||
border-color: rgb(234 179 8 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-gray-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(107 114 128 / var(--tw-border-opacity));
|
||||
.dark\:bg-blue-600:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-800:is(.dark *) {
|
||||
@@ -2551,11 +2533,6 @@ video {
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-600:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
@@ -2635,6 +2612,11 @@ video {
|
||||
color: rgb(248 113 113 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-500:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-white:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
@@ -2650,11 +2632,6 @@ video {
|
||||
color: rgb(250 204 21 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-red-500:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
|
||||
--tw-placeholder-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
|
||||
@@ -2674,6 +2651,16 @@ video {
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-blue-600:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-blue-700:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-500:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
@@ -2714,16 +2701,6 @@ video {
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-blue-600:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-blue-700:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:from-primary-500:hover:is(.dark *) {
|
||||
--tw-gradient-from: #f49541 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(244 149 65 / 0) var(--tw-gradient-to-position);
|
||||
@@ -2739,6 +2716,11 @@ video {
|
||||
color: rgb(191 219 254 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-blue-400:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(96 165 250 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-gray-200:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
@@ -2749,21 +2731,26 @@ video {
|
||||
color: rgb(248 185 99 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-primary-400:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(246 167 82 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-primary-500:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(244 149 65 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-white:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-red-400:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(248 113 113 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-white:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-blue-500:focus:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
@@ -2979,10 +2966,6 @@ video {
|
||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.md\:p-5 {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
|
||||
Reference in New Issue
Block a user