diff --git a/README.md b/README.md index 67c43287a..24ef87794 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,12 @@ docker compose up -d --build --force-recreate ``` The app will be available at http://localhost:80 + + + +## Credits +The following libraries and frameworks are used in this project: + +- [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework for rapidly building custom designs. +- [Flowbite](https://flowbite.com/) - A free and open-source UI component library. +- [Konscious.Security.Cryptography](https://github.com/kmaragon/Konscious.Security.Cryptography) - A .NET library that implements Argon2id, a memory-hard password hashing algorithm. diff --git a/src/AliasVault.WebApp/AliasVault.WebApp.csproj b/src/AliasVault.WebApp/AliasVault.WebApp.csproj index 64d27e7d0..fd74140b7 100644 --- a/src/AliasVault.WebApp/AliasVault.WebApp.csproj +++ b/src/AliasVault.WebApp/AliasVault.WebApp.csproj @@ -22,6 +22,9 @@ + + + diff --git a/src/AliasVault.WebApp/Components/Alias/Alias.razor b/src/AliasVault.WebApp/Components/Alias/Alias.razor new file mode 100644 index 000000000..928b0db52 --- /dev/null +++ b/src/AliasVault.WebApp/Components/Alias/Alias.razor @@ -0,0 +1,47 @@ +@using AliasDb +@inject NavigationManager NavigationManager + +
+
+

Card header

+
+
+

Card body

+
+
+

Card footer

+
+
+ + + + +@code { + [Parameter] public Login Login { get; set; } = new Login(); + private bool showModal = false; + + private void ShowDetails() + { + // Redirect to view page instead for now. + NavigationManager.NavigateTo($"/alias/{Login.Id}"); + //showModal = true; + } + + private void CloseDetails() + { + showModal = false; + } + +} diff --git a/src/AliasVault.WebApp/Components/Breadcrumb.razor b/src/AliasVault.WebApp/Components/Breadcrumb.razor new file mode 100644 index 000000000..b47d6ae39 --- /dev/null +++ b/src/AliasVault.WebApp/Components/Breadcrumb.razor @@ -0,0 +1,35 @@ +@using AliasVault.WebApp.Components.Models +@inherits ComponentBase + +@if (BreadcrumbItems.Any()) +{ + +} + + + +@code { + [Parameter] + public List BreadcrumbItems { get; set; } = new List(); +} diff --git a/src/AliasVault.WebApp/Components/Models/BreadcrumbItem.cs b/src/AliasVault.WebApp/Components/Models/BreadcrumbItem.cs new file mode 100644 index 000000000..01813e22a --- /dev/null +++ b/src/AliasVault.WebApp/Components/Models/BreadcrumbItem.cs @@ -0,0 +1,7 @@ +namespace AliasVault.WebApp.Components.Models; + +public class BreadcrumbItem +{ + public string? DisplayName { get; set; } + public string? Url { get; set; } +} diff --git a/src/AliasVault.WebApp/Components/PageTitleAppend.razor b/src/AliasVault.WebApp/Components/PageTitleAppend.razor new file mode 100644 index 000000000..12e5d1118 --- /dev/null +++ b/src/AliasVault.WebApp/Components/PageTitleAppend.razor @@ -0,0 +1,12 @@ +@PageTitlePrefix AliasVault - @PageTitleSuffix + +@code { + [Parameter] + public string PageTitleSuffix { get; set; } + + public string PageTitlePrefix { get; set; } = ""; + + protected override void OnInitialized() + { + } +} diff --git a/src/AliasVault.WebApp/Layout/Footer.razor b/src/AliasVault.WebApp/Layout/Footer.razor new file mode 100644 index 000000000..6b9a5a09b --- /dev/null +++ b/src/AliasVault.WebApp/Layout/Footer.razor @@ -0,0 +1,11 @@ + diff --git a/src/AliasVault.WebApp/Layout/Footer.razor.css b/src/AliasVault.WebApp/Layout/Footer.razor.css new file mode 100644 index 000000000..881d128a5 --- /dev/null +++ b/src/AliasVault.WebApp/Layout/Footer.razor.css @@ -0,0 +1,83 @@ +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } + + .nav-scrollable { + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/src/AliasVault.WebApp/Layout/MainLayout.razor b/src/AliasVault.WebApp/Layout/MainLayout.razor index dfafffc09..8a8ad4e36 100644 --- a/src/AliasVault.WebApp/Layout/MainLayout.razor +++ b/src/AliasVault.WebApp/Layout/MainLayout.razor @@ -1,17 +1,14 @@ @inherits LayoutComponentBase -
- + -
-
- -
+
-
+
+
@Body -
-
+ + +
+
diff --git a/src/AliasVault.WebApp/Layout/TopMenu.razor b/src/AliasVault.WebApp/Layout/TopMenu.razor new file mode 100644 index 000000000..3cfc97bdb --- /dev/null +++ b/src/AliasVault.WebApp/Layout/TopMenu.razor @@ -0,0 +1,257 @@ +
+ + +
diff --git a/src/AliasVault.WebApp/Layout/TopMenu.razor.css b/src/AliasVault.WebApp/Layout/TopMenu.razor.css new file mode 100644 index 000000000..881d128a5 --- /dev/null +++ b/src/AliasVault.WebApp/Layout/TopMenu.razor.css @@ -0,0 +1,83 @@ +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } + + .nav-scrollable { + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/src/AliasVault.WebApp/Pages/Base/PageBase.cs b/src/AliasVault.WebApp/Pages/Base/PageBase.cs new file mode 100644 index 000000000..4abb93488 --- /dev/null +++ b/src/AliasVault.WebApp/Pages/Base/PageBase.cs @@ -0,0 +1,50 @@ +using AliasVault.WebApp.Components.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace AliasVault.WebApp.Pages.Base; + +/// +/// Base authorize page that all pages that are part of the logged in website should inherit from. +/// All pages that inherit from this class will require the user to be logged in and have a confirmed email. +/// Also, a default set of breadcrumbs is added in the parent OnInitialized method. +/// +public class PageBase : OwningComponentBase +{ + [Inject] + public NavigationManager NavigationManager { get; set; } = null!; + + [Inject] + public IJSRuntime Js { get; set; } = null!; + + /// + /// Contains the breadcrumb items for the page. A default set of breadcrumbs is added in the parent OnInitialized method. + /// + protected List BreadcrumbItems { get; set; } = new List(); + + private bool _parametersInitialSet; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + _parametersInitialSet = false; + + // Add base breadcrumbs + BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Home", Url = NavigationManager.BaseUri }); + + // Detect success messages in query string and add them to the SuccessMessages list + var uri = new Uri(NavigationManager.Uri); + } + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + + // This is needed to prevent the OnParametersSetAsync method from running together with OnInitialized on initial page load. + if (!_parametersInitialSet) + { + _parametersInitialSet = true; + return; + } + } +} diff --git a/src/AliasVault.WebApp/Pages/Home.razor b/src/AliasVault.WebApp/Pages/Home.razor index 9001e0bd2..342c0f678 100644 --- a/src/AliasVault.WebApp/Pages/Home.razor +++ b/src/AliasVault.WebApp/Pages/Home.razor @@ -1,7 +1,29 @@ @page "/" +@using AliasVault.WebApp.Components.Alias +@inherits PageBase Home -

Hello, world!

+
+
+ +

Aliases

+

Find all of your aliases below.

+
+
-Welcome to your new app. +
+ + + + + + +
+ +@code { + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + } +} diff --git a/src/AliasVault.WebApp/Services/AliasService.cs b/src/AliasVault.WebApp/Services/AliasService.cs new file mode 100644 index 000000000..d73361bb2 --- /dev/null +++ b/src/AliasVault.WebApp/Services/AliasService.cs @@ -0,0 +1,129 @@ +using AliasDb; +using AliasGenerators.Identity.Models; +using Microsoft.EntityFrameworkCore; + +namespace AliasVault.WebApp.Services; + +public class AliasService +{ + private Login _alias; + + /// + /// Public constructor which can be called from static async method or directly. + /// + /// + /// + public AliasService(Login aliasObj) + { + _alias = aliasObj; + } + + /// + /// Returns inner event EF object. + /// + /// + public Login Alias() + { + return _alias; + } + + /// + /// Insert new entry into database. + /// + /// + public static async Task InsertAliasAsync(Login aliasObject) + { + using (var dbContext = new AliasDbContext()) + { + var newObject = aliasObject; + newObject.Identity.CreatedAt = DateTime.UtcNow; + newObject.Identity.UpdatedAt = DateTime.UtcNow; + newObject.Passwords.First().CreatedAt = DateTime.UtcNow; + newObject.Passwords.First().UpdatedAt = DateTime.UtcNow; + newObject.CreatedAt = DateTime.UtcNow; + newObject.UpdatedAt = DateTime.UtcNow; + + dbContext.Add(newObject); + await dbContext.SaveChangesAsync(); + + return newObject; + } + } + + /// + /// Update an existing entry to database. + /// + /// + public static async Task UpdateAliasAsync(Login aliasObject) + { + using (var dbContext = new AliasDbContext()) + { + // Load existing record.. + var record = dbContext.Logins.First(x => x.Id == aliasObject.Id); + + // Update properties + record.Identity.FirstName = aliasObject.Identity.FirstName; + record.Identity.LastName = aliasObject.Identity.LastName; + record.Identity.NickName = aliasObject.Identity.NickName; + record.Identity.Gender = aliasObject.Identity.Gender; + record.Identity.BirthDate = aliasObject.Identity.BirthDate; + record.Identity.AddressStreet = aliasObject.Identity.AddressStreet; + record.Identity.AddressCity = aliasObject.Identity.AddressCity; + record.Identity.AddressState = aliasObject.Identity.AddressState; + record.Identity.AddressZipCode = aliasObject.Identity.AddressZipCode; + record.Identity.AddressCountry = aliasObject.Identity.AddressCountry; + record.Identity.Hobbies = aliasObject.Identity.Hobbies; + record.Identity.EmailPrefix = aliasObject.Identity.EmailPrefix; + record.Identity.PhoneMobile = aliasObject.Identity.PhoneMobile; + record.Identity.BankAccountIBAN = aliasObject.Identity.BankAccountIBAN; + record.Identity.UpdatedAt = DateTime.UtcNow; + + // TODO: support multiple passwords. + var password = record.Passwords.First(); + password.Value = aliasObject.Passwords.First().Value; + password.UpdatedAt = DateTime.UtcNow; + + // Update service. + record.Service.Name = aliasObject.Service.Name; + record.Service.Url = aliasObject.Service.Url; + record.Service.Logo = aliasObject.Service.Logo; + record.Service.UpdatedAt = DateTime.UtcNow; + + await dbContext.SaveChangesAsync(); + + return record; + } + } + + /// + /// Load existing entry from database. + /// + /// + public static async Task LoadAliasAsync(Guid aliasId) + { + using (var dbContext = new AliasDbContext()) + { + var aliasObject = await dbContext.Logins + .Include(x => x.Passwords) + .Include(x => x.Identity) + .Include(x => x.Service) + .Where(x => x.Id == aliasId) + .FirstAsync(); + + return aliasObject; + } + } + + /// + /// Removes existing entry from database. + /// + /// + public static async Task DeleteAliasAsync(Login alias) + { + using (var dbContext = new AliasDbContext()) + { + dbContext.Logins.Remove(dbContext.Logins.First(x => x.Id == alias.Id)); + dbContext.SaveChanges(); + } + } +} diff --git a/src/AliasVault.WebApp/_Imports.razor b/src/AliasVault.WebApp/_Imports.razor index 90a4d0142..fa17cf570 100644 --- a/src/AliasVault.WebApp/_Imports.razor +++ b/src/AliasVault.WebApp/_Imports.razor @@ -12,6 +12,8 @@ @using AliasVault.WebApp @using AliasVault.WebApp.Layout @using AliasVault.WebApp.Components +@using AliasVault.WebApp.Components.Models +@using AliasVault.WebApp.Pages.Base @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @using Blazored.LocalStorage diff --git a/src/AliasVault.WebApp/wwwroot/css/tailwind.css b/src/AliasVault.WebApp/wwwroot/css/tailwind.css index 631a5dfcd..33dcd850a 100644 --- a/src/AliasVault.WebApp/wwwroot/css/tailwind.css +++ b/src/AliasVault.WebApp/wwwroot/css/tailwind.css @@ -554,27 +554,97 @@ video { --tw-contain-style: ; } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.invisible { + visibility: hidden; +} + .collapse { visibility: collapse; } +.fixed { + position: fixed; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.z-10 { + z-index: 10; +} + +.z-20 { + z-index: 20; +} + +.z-30 { + z-index: 30; +} + +.z-50 { + z-index: 50; +} + +.col-span-2 { + grid-column: span 2 / span 2; +} + +.col-span-full { + grid-column: 1 / -1; +} + +.mx-3 { + margin-left: 0.75rem; + margin-right: 0.75rem; +} + .mx-auto { margin-left: auto; margin-right: auto; } -.mb-0 { - margin-bottom: 0px; +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.-mb-1 { + margin-bottom: -0.25rem; +} + +.-mt-5 { + margin-top: -1.25rem; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-1\.5 { + margin-bottom: 0.375rem; } .mb-2 { margin-bottom: 0.5rem; } -.mb-3 { - margin-bottom: 0.75rem; -} - .mb-4 { margin-bottom: 1rem; } @@ -587,30 +657,102 @@ video { margin-bottom: 2rem; } +.ml-1 { + margin-left: 0.25rem; +} + .ml-3 { margin-left: 0.75rem; } +.ml-6 { + margin-left: 1.5rem; +} + .ml-auto { margin-left: auto; } +.mr-14 { + margin-right: 3.5rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.mr-2\.5 { + margin-right: 0.625rem; +} + +.mr-3 { + margin-right: 0.75rem; +} + +.mr-4 { + margin-right: 1rem; +} + +.mt-0 { + margin-top: 0px; +} + +.mt-4 { + margin-top: 1rem; +} + .mt-8 { margin-top: 2rem; } +.mb-3 { + margin-bottom: 0.75rem; +} + .block { display: block; } +.inline-block { + display: inline-block; +} + .flex { display: flex; } +.inline-flex { + display: inline-flex; +} + .table { display: table; } +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.h-11 { + height: 2.75rem; +} + +.h-16 { + height: 4rem; +} + +.h-3 { + height: 0.75rem; +} + +.h-32 { + height: 8rem; +} + .h-4 { height: 1rem; } @@ -623,30 +765,102 @@ video { height: 1.5rem; } +.h-7 { + height: 1.75rem; +} + +.h-8 { + height: 2rem; +} + .h-9 { height: 2.25rem; } +.h-full { + height: 100%; +} + .w-1\/2 { width: 50%; } +.w-11 { + width: 2.75rem; +} + +.w-3 { + width: 0.75rem; +} + .w-4 { width: 1rem; } +.w-44 { + width: 11rem; +} + +.w-5 { + width: 1.25rem; +} + +.w-56 { + width: 14rem; +} + +.w-6 { + width: 1.5rem; +} + .w-64 { width: 16rem; } +.w-7 { + width: 1.75rem; +} + +.w-8 { + width: 2rem; +} + .w-full { width: 100%; } +.max-w-screen-2xl { + max-width: 1536px; +} + +.max-w-sm { + max-width: 24rem; +} + .max-w-xl { max-width: 36rem; } +.flex-shrink-0 { + flex-shrink: 0; +} + +.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)); +} + +.list-none { + list-style-type: none; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + .grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } @@ -659,6 +873,10 @@ video { flex-direction: column; } +.flex-wrap { + flex-wrap: wrap; +} + .items-start { align-items: flex-start; } @@ -667,10 +885,34 @@ video { align-items: center; } +.justify-start { + justify-content: flex-start; +} + .justify-center { justify-content: center; } +.justify-between { + justify-content: space-between; +} + +.gap-4 { + gap: 1rem; +} + +.space-x-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.25rem * var(--tw-space-x-reverse)); + margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-x-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1.5rem * var(--tw-space-x-reverse)); + margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse))); +} + .space-y-6 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); @@ -683,10 +925,47 @@ video { margin-bottom: calc(2rem * var(--tw-space-y-reverse)); } +.divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); +} + +.divide-gray-100 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-divide-opacity)); +} + +.self-center { + align-self: center; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.whitespace-nowrap { + white-space: nowrap; +} + .rounded { border-radius: 0.25rem; } +.rounded-full { + border-radius: 9999px; +} + .rounded-lg { border-radius: 0.5rem; } @@ -705,11 +984,38 @@ video { border-width: 1px; } +.border-b { + border-bottom-width: 1px; +} + +.border-t { + border-top-width: 1px; +} + +.border-dashed { + border-style: dashed; +} + +.border-gray-100 { + --tw-border-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-border-opacity)); +} + +.border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); +} + .border-gray-300 { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity)); } +.border-white { + --tw-border-opacity: 1; + border-color: rgb(255 255 255 / var(--tw-border-opacity)); +} + .bg-gray-200 { --tw-bg-opacity: 1; background-color: rgb(229 231 235 / var(--tw-bg-opacity)); @@ -720,11 +1026,36 @@ video { background-color: rgb(249 250 251 / var(--tw-bg-opacity)); } +.bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + +.bg-gray-900 { + --tw-bg-opacity: 1; + background-color: rgb(17 24 39 / var(--tw-bg-opacity)); +} + +.bg-green-400 { + --tw-bg-opacity: 1; + background-color: rgb(74 222 128 / var(--tw-bg-opacity)); +} + .bg-primary-700 { --tw-bg-opacity: 1; background-color: rgb(29 78 216 / var(--tw-bg-opacity)); } +.bg-purple-500 { + --tw-bg-opacity: 1; + background-color: rgb(168 85 247 / var(--tw-bg-opacity)); +} + +.bg-red-600 { + --tw-bg-opacity: 1; + background-color: rgb(220 38 38 / var(--tw-bg-opacity)); +} + .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -742,6 +1073,10 @@ video { padding: 0.625rem; } +.p-4 { + padding: 1rem; +} + .p-6 { padding: 1.5rem; } @@ -766,15 +1101,46 @@ video { padding-right: 1.5rem; } +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } +.py-6 { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} + +.pl-3 { + padding-left: 0.75rem; +} + +.pr-4 { + padding-right: 1rem; +} + .ps-3 { padding-inline-start: 0.75rem; } +.pt-16 { + padding-top: 4rem; +} + +.pt-6 { + padding-top: 1.5rem; +} + .pt-8 { padding-top: 2rem; } @@ -798,14 +1164,32 @@ video { line-height: 1.25rem; } +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + .font-bold { font-weight: 700; } +.font-light { + font-weight: 300; +} + .font-medium { font-weight: 500; } +.font-normal { + font-weight: 400; +} + .font-semibold { font-weight: 600; } @@ -818,11 +1202,21 @@ video { line-height: 2.25rem; } +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + .text-gray-500 { --tw-text-opacity: 1; color: rgb(107 114 128 / var(--tw-text-opacity)); } +.text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + .text-gray-900 { --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); @@ -838,6 +1232,10 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.opacity-0 { + opacity: 0; +} + .shadow { --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); @@ -850,11 +1248,60 @@ video { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.transition-opacity { + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-300 { + transition-duration: 300ms; +} + +.hover\:bg-gray-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + .hover\:bg-primary-800:hover { --tw-bg-opacity: 1; background-color: rgb(30 64 175 / var(--tw-bg-opacity)); } +.hover\:text-gray-900:hover { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.hover\:text-primary-600:hover { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); +} + +.hover\:text-primary-700:hover { + --tw-text-opacity: 1; + color: rgb(29 78 216 / var(--tw-text-opacity)); +} + .hover\:underline:hover { text-decoration-line: underline; } @@ -864,12 +1311,27 @@ video { border-color: rgb(59 130 246 / var(--tw-border-opacity)); } +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + .focus\:ring-4:focus { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); } +.focus\:ring-gray-200:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity)); +} + +.focus\:ring-gray-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity)); +} + .focus\:ring-primary-300:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity)); @@ -880,11 +1342,26 @@ video { --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); } +.group:hover .group-hover\:text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + +.dark\:divide-gray-600:is(.dark *) > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-divide-opacity)); +} + .dark\:border-gray-600:is(.dark *) { --tw-border-opacity: 1; border-color: rgb(75 85 99 / var(--tw-border-opacity)); } +.dark\:border-gray-700:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(55 65 81 / var(--tw-border-opacity)); +} + .dark\:bg-gray-700:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(55 65 81 / var(--tw-bg-opacity)); @@ -909,11 +1386,21 @@ video { --tw-bg-opacity: 0.8; } +.dark\:text-gray-300:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} + .dark\:text-gray-400:is(.dark *) { --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); } +.dark\:text-primary-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(96 165 250 / var(--tw-text-opacity)); +} + .dark\:text-primary-500:is(.dark *) { --tw-text-opacity: 1; color: rgb(59 130 246 / var(--tw-text-opacity)); @@ -938,16 +1425,55 @@ video { --tw-ring-offset-color: #1f2937; } +.dark\:hover\:bg-gray-600:hover:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(75 85 99 / var(--tw-bg-opacity)); +} + +.dark\:hover\:bg-gray-700:hover:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); +} + .dark\:hover\:bg-primary-700:hover:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(29 78 216 / var(--tw-bg-opacity)); } +.dark\:hover\:text-primary-500:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(59 130 246 / 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\:underline:hover:is(.dark *) { + text-decoration-line: underline; +} + .dark\:focus\:border-primary-500:focus:is(.dark *) { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity)); } +.dark\:focus\:text-white:focus:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.dark\:focus\:ring-gray-600:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity)); +} + +.dark\:focus\:ring-gray-700:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity)); +} + .dark\:focus\:ring-primary-500:focus:is(.dark *) { --tw-ring-opacity: 1; --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); @@ -963,15 +1489,37 @@ video { --tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity)); } +.group:hover .dark\:group-hover\:text-gray-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + @media (min-width: 640px) { + .sm\:block { + display: block; + } + + .sm\:flex { + display: flex; + } + .sm\:w-auto { width: auto; } + .sm\:p-6 { + padding: 1.5rem; + } + .sm\:p-8 { padding: 2rem; } + .sm\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } + .sm\:text-sm { font-size: 0.875rem; line-height: 1.25rem; @@ -979,13 +1527,175 @@ video { } @media (min-width: 768px) { + .md\:mb-0 { + margin-bottom: 0px; + } + + .md\:ml-2 { + margin-left: 0.5rem; + } + + .md\:mr-0 { + margin-right: 0px; + } + + .md\:mr-6 { + margin-right: 1.5rem; + } + + .md\:flex { + display: flex; + } + .md\:h-screen { height: 100vh; } + + .md\:w-auto { + width: auto; + } + + .md\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .md\:grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .md\:items-center { + align-items: center; + } + + .md\:justify-between { + justify-content: space-between; + } + + .md\:space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); + } + + .md\:border-0 { + border-width: 0px; + } + + .md\:p-0 { + padding: 0px; + } + + .md\:py-10 { + padding-top: 2.5rem; + padding-bottom: 2.5rem; + } + + .md\:hover\:bg-transparent:hover { + background-color: transparent; + } + + .md\:hover\:text-primary-700:hover { + --tw-text-opacity: 1; + color: rgb(29 78 216 / var(--tw-text-opacity)); + } + + .md\:dark\:hover\:bg-transparent:hover:is(.dark *) { + background-color: transparent; + } } @media (min-width: 1024px) { + .lg\:order-1 { + order: 1; + } + + .lg\:order-2 { + order: 2; + } + .lg\:mb-10 { margin-bottom: 2.5rem; } + + .lg\:mt-0 { + margin-top: 0px; + } + + .lg\:flex { + display: flex; + } + + .lg\:hidden { + display: none; + } + + .lg\:w-auto { + width: auto; + } + + .lg\:flex-row { + flex-direction: row; + } + + .lg\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .lg\:py-0 { + padding-top: 0px; + padding-bottom: 0px; + } + + .lg\:hover\:underline:hover { + text-decoration-line: underline; + } +} + +@media (min-width: 1280px) { + .xl\:col-auto { + grid-column: auto; + } + + .xl\:mb-2 { + margin-bottom: 0.5rem; + } + + .xl\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .xl\:grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .xl\:grid-cols-6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + + .xl\:gap-4 { + gap: 1rem; + } + + .xl\:space-x-8 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(2rem * var(--tw-space-x-reverse)); + margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse))); + } + + .xl\:px-0 { + padding-left: 0px; + padding-right: 0px; + } +} + +@media (min-width: 1536px) { + .\32xl\:col-span-2 { + grid-column: span 2 / span 2; + } + + .\32xl\:px-0 { + padding-left: 0px; + padding-right: 0px; + } }