Improve login and initial user setup screen appearance (#1336)

This commit is contained in:
Aditya Chandel
2025-10-13 13:02:27 -06:00
committed by GitHub
parent 165d7ef7ad
commit 34d195bbbe
26 changed files with 1136 additions and 219 deletions

View File

@@ -94,7 +94,7 @@
top: 50%;
left: 10px;
transform: translateY(-50%);
color: var(--text-color-secondary);
color: var(--text-secondary-color);
font-size: 1.0rem;
pointer-events: none;
}

View File

@@ -82,7 +82,7 @@
flex-direction: column;
overflow: hidden;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid var(--p-content-border-color);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);

View File

@@ -124,7 +124,7 @@
flex-direction: column;
overflow: hidden;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid var(--p-content-border-color);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
@media (max-width: 768px) {

View File

@@ -1,14 +1,15 @@
<div class="ml-2 md:ml-5 relative w-full max-w-[500px]">
<!-- Desktop view -->
<p-iconfield class="w-full sm:flex">
<p-inputicon styleClass="pi pi-search"/>
<div class="book-searcher-container">
<p-iconfield class="search-iconfield" [class.focused]="isSearchFocused" [class.has-results]="isDropdownOpen">
<p-inputicon class="pi pi-search"/>
<input
type="text"
pInputText
[(ngModel)]="searchQuery"
(input)="onSearchInputChange()"
(focus)="isSearchFocused = true"
(blur)="onSearchBlur()"
placeholder="Title, Author, Series, Genre, or ISBN..."
class="w-full pr-10"
class="search-input"
/>
@if (searchQuery) {
<p-button
@@ -16,35 +17,45 @@
(click)="clearSearch()"
[text]="true"
[rounded]="true"
class="absolute right-2 top-1/2 -translate-y-1/2 hidden lg:inline-flex"
class="clear-search-btn"
aria-label="Clear Search">
</p-button>
}
</p-iconfield>
<!-- Search results below small screen search input -->
@if (books.length > 0 || searchQuery) {
<div
class="search-dropdown layout-menu w-[300px] sm:w-[600px]"
class="search-dropdown layout-menu"
[class.show]="(books.length > 0)"
>
@if (books.length > 0) {
@for (book of books; track book) {
<div class="search-dropdown-item flex flex-col" (click)="onBookClick(book)">
<div class="flex flex-row w-full p-2">
<div class="search-dropdown-item" (click)="onBookClick(book)">
<div class="search-item-content">
<img
[attr.src]="urlHelper.getThumbnailUrl(book.id, book.metadata?.coverUpdatedOn)"
alt="Book Cover"
class="search-book-cover"
/>
<div class="search-book-details">
<div class="flex items-center">
<p class="search-book-name text font-medium truncate">
<div class="search-book-title-container">
<p class="search-book-name">
{{ book.metadata?.title | slice: 0:70 }}
</p>
</div>
@if (book.metadata?.authors?.length ?? 0 > 0) {
<p class="italic text-gray-300">by: {{ getAuthorNames(book.metadata?.authors) }}</p>
<div class="search-book-meta-line">
@if (book.metadata?.authors?.length ?? 0 > 0) {
<p class="search-book-authors">by {{ getAuthorNames(book.metadata?.authors) }}</p>
}
@if (getPublishedYear(book.metadata?.publishedDate)) {
<span class="metadata-badge year-badge">{{ getPublishedYear(book.metadata?.publishedDate) }}</span>
}
</div>
@if (getSeriesInfo(book.metadata?.seriesName, book.metadata?.seriesNumber)) {
<div class="search-book-series metadata-badge series-badge">
<i class="pi pi-book"></i>
<span>{{ getSeriesInfo(book.metadata?.seriesName, book.metadata?.seriesNumber) }}</span>
</div>
}
</div>
</div>
@@ -52,7 +63,7 @@
</div>
}
} @else {
<div class="search-dropdown-item px-4 py-2">
<div class="search-dropdown-item no-results">
<span>No results found</span>
</div>
}

View File

@@ -1,65 +1,265 @@
.search-icon {
position: absolute;
top: 50%;
left: 10px;
transform: translateY(-50%);
color: var(--text-color-secondary);
font-size: 1.0rem;
pointer-events: none;
.book-searcher-container {
margin-left: 0.5rem;
position: relative;
width: 100%;
@media (min-width: 768px) {
margin-left: 1.25rem;
max-width: 315px;
transition: max-width 0.3s ease;
}
&:has(.search-iconfield.focused),
&:has(.search-iconfield.has-results) {
@media (min-width: 768px) {
max-width: 100%;
}
}
}
.search-iconfield {
width: 100%;
@media (min-width: 640px) {
display: flex;
}
@media (min-width: 768px) {
transition: all 0.3s ease;
}
}
.search-input {
width: 100%;
padding-left: 30px;
padding-right: 2.5rem;
height: 2.5rem;
line-height: 2.5rem;
font-size: 1rem;
}
.clear-btn {
color: var(--text-color-secondary);
.clear-search-btn {
position: absolute;
right: 10px;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
display: none;
@media (min-width: 1024px) {
display: inline-flex;
}
}
.search-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
background-color: var(--code-background);
width: 300px;
background-color: var(--card-background);
border: 1px solid var(--border-color);
border-radius: var(--card-border);
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 10px 15px -3px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-height: 500px;
max-height: 70vh;
overflow-y: auto;
opacity: 0;
height: 0;
transition: opacity 0.3s ease, height 0.3s ease;
transform: translateY(-10px);
transition: opacity 0.2s ease, transform 0.2s ease, height 0.2s ease;
@media (min-width: 640px) {
width: 100%;
max-width: 600px;
}
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary-color);
}
}
.search-dropdown.show {
opacity: 1;
height: auto;
transform: translateY(0);
}
.search-dropdown-item {
display: flex;
align-items: center;
flex-direction: column;
cursor: pointer;
transition: background-color 0.3s;
transition: background-color 0.2s ease;
&:first-child {
border-radius: 8px 8px 0 0;
}
&:last-child {
border-radius: 0 0 8px 8px;
::ng-deep .p-divider {
display: none;
}
}
&:hover {
background-color: var(--surface-hover);
}
&.no-results {
padding: 2rem 1rem;
text-align: center;
color: var(--text-secondary-color);
font-style: italic;
}
::ng-deep .p-divider {
margin: 0;
&::before {
border-top-color: var(--border-color);
opacity: 0.5;
}
}
}
.search-item-content {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 1rem;
width: 100%;
padding: 0.75rem 1rem;
}
.search-book-cover {
width: 35px;
height: 47px;
width: 50px;
height: 67px;
min-width: 50px;
object-fit: cover;
max-width: 100px;
max-height: 150px;
border-radius: 4px;
margin-right: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: var(--surface-ground);
}
.search-book-details {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.search-book-title-container {
display: flex;
align-items: flex-start;
}
.search-book-name {
font-weight: 600;
font-size: 0.95rem;
line-height: 1.4;
color: var(--text-color);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
margin: 0;
}
.search-book-meta-line {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.search-book-authors {
font-size: 0.9rem;
font-style: italic;
color: var(--text-secondary-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
flex: 1;
min-width: 0;
}
.search-book-year {
font-size: 0.8rem;
color: var(--text-secondary-color);
background-color: var(--surface-ground);
padding: 0.125rem 0.5rem;
border-radius: 4px;
white-space: nowrap;
font-weight: 500;
}
.search-book-series {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8rem;
color: var(--primary-color);
background-color: rgba(var(--primary-color-rgb, 59, 130, 246), 0.1);
padding: 0.075rem 0.125rem;
border-radius: 4px;
width: fit-content;
i {
font-size: 0.75rem;
}
span {
font-weight: 500;
}
}
.search-book-metadata {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.metadata-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.8rem;
padding: 0.1rem 0.25rem;
border-radius: 4px;
background-color: var(--surface-hover);
color: var(--text-secondary-color);
white-space: nowrap;
i {
font-size: 0.7rem;
}
&.series-badge {
background-color: rgba(59, 130, 246, 0.1);
color: rgb(59, 130, 246);
}
&.year-badge {
background-color: rgba(34, 197, 94, 0.1);
color: rgb(34, 197, 94);
}
}

View File

@@ -1,5 +1,5 @@
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import {BehaviorSubject, of, Subject, Subscription} from 'rxjs';
import {BehaviorSubject, of, Subscription} from 'rxjs';
import {catchError, switchMap} from 'rxjs/operators';
import {Book} from '../../model/book.model';
import {FormsModule} from '@angular/forms';
@@ -34,6 +34,7 @@ export class BookSearcherComponent implements OnInit, OnDestroy {
books: Book[] = [];
#searchSubject = new BehaviorSubject<string>('');
#subscription!: Subscription;
isSearchFocused = false;
private bookService = inject(BookService);
private router = inject(Router);
@@ -41,24 +42,16 @@ export class BookSearcherComponent implements OnInit, OnDestroy {
private headerFilter = new HeaderFilter(this.#searchSubject.asObservable());
ngOnInit(): void {
this.initializeSearch();
}
initializeSearch(): void {
this.#subscription = this.bookService.bookState$.pipe(
switchMap(bookState => this.headerFilter.filter(bookState)),
catchError((error) => {
console.error('Error while searching books:', error);
return of({books: [], loaded: true, error: null});
})
catchError(() => of({books: [], loaded: true, error: null}))
).subscribe({
next: (filteredState) => {
const term = this.searchQuery.trim();
this.books = term.length >= 2
? (filteredState.books || []).slice(0, 50)
: [];
},
error: (error) => console.error('Subscription error:', error)
}
});
}
@@ -66,6 +59,20 @@ export class BookSearcherComponent implements OnInit, OnDestroy {
return authors?.join(', ') || 'Unknown Author';
}
getPublishedYear(publishedDate: string | undefined): string | null {
if (!publishedDate) return null;
const year = publishedDate.split('-')[0];
return year && year.length === 4 ? year : null;
}
getSeriesInfo(seriesName: string | undefined, seriesNumber: number | null | undefined): string | null {
if (!seriesName) return null;
if (seriesNumber) {
return `${seriesName} #${seriesNumber}`;
}
return seriesName;
}
onSearchInputChange(): void {
this.#searchSubject.next(this.searchQuery.trim());
}
@@ -82,6 +89,18 @@ export class BookSearcherComponent implements OnInit, OnDestroy {
this.books = [];
}
get isDropdownOpen(): boolean {
return this.books.length > 0;
}
onSearchBlur(): void {
setTimeout(() => {
if (!this.isDropdownOpen) {
this.isSearchFocused = false;
}
}, 200);
}
ngOnDestroy(): void {
if (this.#subscription) {
this.#subscription.unsubscribe();

View File

@@ -1,5 +1,5 @@
@if (filteredBooks$ | async; as books) {
<div class="rounded-xl overflow-hidden">
<div class="rounded-xl overflow-hidden border-[1px] border-solid border-[var(--p-content-border-color)]">
<p-tabs [value]="tab" lazy="true" scrollable>
<p-tablist>
<p-tab value="view">

View File

@@ -102,7 +102,6 @@ export interface BookMetadata {
providerBookId?: string;
thumbnailUrl?: string | null;
reviews?: BookReview[];
titleLocked?: boolean;
subtitleLocked?: boolean;
publisherLocked?: boolean;
@@ -134,7 +133,6 @@ export interface BookMetadata {
tagsLocked?: boolean;
coverLocked?: boolean;
reviewsLocked?: boolean;
[key: string]: any;
}
@@ -203,7 +201,6 @@ export interface BookSetting {
epubSettings?: EpubViewerSetting;
cbxSettings?: CbxViewerSetting;
newPdfSettings?: NewPdfReaderSetting;
[key: string]: any;
}

View File

@@ -62,7 +62,7 @@
padding: 3rem;
background: var(--p-surface-900);
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid var(--p-content-border-color);
backdrop-filter: blur(20px);
position: relative;
overflow: hidden;

View File

@@ -1,4 +1,4 @@
<div class="rounded-xl overflow-hidden">
<div class="rounded-xl overflow-hidden border-[1px] border-solid border-[var(--p-content-border-color)]">
<p-tabs [value]="tab" lazy="true" scrollable>
<p-tablist>
<p-tab value="view">

View File

@@ -58,7 +58,7 @@
p {
margin-top: 1rem;
color: var(--text-color-secondary);
color: var(--text-secondary-color);
}
}
@@ -67,7 +67,7 @@
flex-direction: column;
align-items: center;
padding: 3rem;
color: var(--text-color-secondary);
color: var(--text-secondary-color);
background: var(--surface-card);
border-radius: 8px;
border: 1px solid var(--surface-border);

View File

@@ -9,7 +9,7 @@
}
.description {
color: var(--text-color-secondary);
color: var(--text-secondary-color);
margin-bottom: 1.5rem;
}
@@ -89,7 +89,7 @@
margin-bottom: 0.5rem;
.count {
color: var(--text-color-secondary);
color: var(--text-secondary-color);
font-size: 0.875rem;
}
}
@@ -107,7 +107,7 @@
small {
display: block;
margin-top: 0.5rem;
color: var(--text-color-secondary);
color: var(--text-secondary-color);
}
}
}

View File

@@ -12,7 +12,7 @@
padding: 25px;
margin-bottom: 30px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid var(--p-content-border-color);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: visible;
}
@@ -48,7 +48,7 @@
align-items: center;
margin-top: 8px;
font-size: 0.95rem;
color: var(--text-color-secondary, #cccccc);
color: var(--text-secondary-color);
flex-wrap: wrap;
.total-item {
@@ -67,7 +67,7 @@
}
.total-label {
color: var(--text-color-secondary, #cccccc);
color: var(--text-secondary-color);
font-size: 0.9rem;
white-space: nowrap;
}
@@ -309,7 +309,7 @@
}
label {
color: var(--text-color-secondary, #cccccc);
color: var(--text-secondary-color);
font-size: 0.9rem;
cursor: pointer;
user-select: none;
@@ -426,7 +426,7 @@
border-radius: 8px;
padding: 15px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid var(--p-content-border-color);
transition: all 0.3s ease;
&:hover {
@@ -472,7 +472,7 @@
.chart-description {
text-align: center;
color: var(--text-color-secondary, #cccccc);
color: var(--text-secondary-color);
font-size: 0.9rem;
margin-bottom: 15px;
line-height: 1.4;
@@ -546,7 +546,7 @@
.no-data-message {
text-align: center;
padding: 50px;
color: var(--text-color-secondary, #cccccc);
color: var(--text-secondary-color);
font-size: 16px;
}
@@ -625,7 +625,7 @@
}
.insight-description {
color: var(--text-color-secondary, #cccccc);
color: var(--text-secondary-color);
font-size: 0.85rem;
line-height: 1.4;
margin: 0;
@@ -702,7 +702,7 @@
}
.insight-description {
color: var(--text-color-secondary, #cccccc);
color: var(--text-secondary-color);
font-size: 0.85rem;
line-height: 1.4;
margin: 0;

View File

@@ -1,37 +1,100 @@
<div class="change-password-container">
<div class="change-password-wrapper">
<p-card class="change-password-card p-fluid">
<div class="change-password-card-wrapper">
<div class="change-password-card-border">
<div class="change-password-card">
<div class="change-password-header">
<div class="logo-wrapper">
<div class="icon-circle">
<i class="pi pi-key"></i>
</div>
</div>
<h1 class="change-password-title">Change Password</h1>
<p class="change-password-subtitle">Update your password to keep your account secure</p>
</div>
<ng-template pTemplate="content">
<form #changePasswordForm="ngForm" (ngSubmit)="changePassword()">
<div class="p-field">
<label for="currentPassword">Current Password</label>
<p-password id="currentPassword" [(ngModel)]="currentPassword" name="currentPassword" [feedback]="false" required fluid></p-password>
<form class="change-password-form" #changePasswordForm="ngForm" (ngSubmit)="changePassword()">
<div class="form-field">
<label for="currentPassword" class="form-label">
<i class="pi pi-lock"></i>
<span>Current Password</span>
</label>
<p-password
id="currentPassword"
name="currentPassword"
required
[(ngModel)]="currentPassword"
[feedback]="false"
placeholder="Enter your current password"
[toggleMask]="true"
[fluid]="true"
autocomplete="current-password"
></p-password>
</div>
<div class="p-field">
<label for="newPassword">New Password</label>
<p-password id="newPassword" [(ngModel)]="newPassword" name="newPassword" [feedback]="false" required fluid></p-password>
<div class="form-field">
<label for="newPassword" class="form-label">
<i class="pi pi-lock-open"></i>
<span>New Password</span>
</label>
<p-password
id="newPassword"
name="newPassword"
required
[(ngModel)]="newPassword"
[feedback]="false"
placeholder="Enter your new password"
[toggleMask]="true"
[fluid]="true"
autocomplete="new-password"
></p-password>
</div>
<div class="p-field">
<label for="confirmNewPassword">Confirm New Password</label>
<p-password id="confirmNewPassword" [(ngModel)]="confirmNewPassword" name="confirmNewPassword" [feedback]="false" required fluid></p-password>
<div class="form-field">
<label for="confirmNewPassword" class="form-label">
<i class="pi pi-check-circle"></i>
<span>Confirm New Password</span>
</label>
<p-password
id="confirmNewPassword"
name="confirmNewPassword"
required
[(ngModel)]="confirmNewPassword"
[feedback]="false"
placeholder="Confirm your new password"
[toggleMask]="true"
[fluid]="true"
autocomplete="new-password"
></p-password>
@if (!passwordsMatch && confirmNewPassword) {
<p-message severity="error" text="New passwords do not match."></p-message>
<small class="field-error">
<i class="pi pi-exclamation-circle"></i>
New passwords do not match
</small>
}
</div>
<p-button type="submit" label="Change Password" icon="pi pi-key" class="p-fluid"></p-button>
<p-button
fluid
type="submit"
label="Update Password"
icon="pi pi-check"
[disabled]="!changePasswordForm.valid || !passwordsMatch"
class="change-password-button"
/>
@if (errorMessage) {
<p-message severity="error" text="{{ errorMessage }}"></p-message>
<div class="message-container error-message">
<p-message severity="error">{{ errorMessage }}</p-message>
</div>
}
@if (successMessage) {
<p-message severity="success" text="{{ successMessage }}"></p-message>
<div class="message-container success-message">
<p-message severity="success">{{ successMessage }}</p-message>
</div>
}
</form>
</ng-template>
</p-card>
</div>
</div>
</div>
</div>

View File

@@ -1,28 +1,90 @@
@use '../../styles/auth-shared' as auth;
.change-password-container {
@apply flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-900 via-cyan-600 to-blue-400 relative;
@include auth.auth-container;
}
.change-password-wrapper {
@apply flex flex-col items-center gap-6 absolute top-1/4 transform -translate-y-1/4;
}
.change-password-card-wrapper {
@include auth.auth-card-wrapper(500px, -6rem);
}
.change-password-card-border {
@include auth.auth-card-border;
}
.change-password-card {
@include auth.auth-card;
}
.app-title {
@apply text-5xl font-extrabold text-white drop-shadow-lg;
text-shadow: 0px 4px 6px rgba(0, 0, 0, 0.3);
}
.change-password-card {
@apply w-96 shadow-lg rounded-xl bg-opacity-90 backdrop-blur-lg;
.change-password-header {
@include auth.auth-header;
}
.logo-wrapper {
@include auth.auth-logo;
}
.icon-circle {
@include auth.auth-icon-circle;
}
.change-password-title {
@include auth.auth-title;
}
.change-password-subtitle {
@include auth.auth-subtitle;
}
form {
@apply flex flex-col gap-4;
}
.change-password-form {
@include auth.auth-form;
}
.p-field {
@apply flex flex-col gap-2;
}
.form-field {
@include auth.form-field;
}
.form-label {
@include auth.form-label;
}
.field-error {
@include auth.field-error;
}
p-button {
@apply mt-4;
}
.change-password-button {
@include auth.auth-button;
}
.message-container {
@include auth.message-container;
}
.error-message {
@include auth.error-message;
}
.success-message {
@include auth.success-message;
}

View File

@@ -1,11 +1,10 @@
import {Component, inject} from '@angular/core';
import {Button} from 'primeng/button';
import {Card} from 'primeng/card';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Message} from 'primeng/message';
import {Password} from 'primeng/password';
import {MessageService, PrimeTemplate} from 'primeng/api';
import {MessageService} from 'primeng/api';
import {UserService} from '../../../features/settings/user-management/user.service';
import {AuthService} from '../../service/auth.service';
@@ -14,13 +13,11 @@ import {AuthService} from '../../service/auth.service';
standalone: true,
imports: [
Button,
Card,
FormsModule,
Message,
Password,
PrimeTemplate,
ReactiveFormsModule
],
],
templateUrl: './change-password.component.html',
styleUrl: './change-password.component.scss'
})

View File

@@ -58,7 +58,7 @@
.hash-group {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid var(--p-content-border-color);
border-radius: 8px;
padding: 1rem;
margin: 0.5rem 0;
@@ -120,7 +120,7 @@
padding: 0.75rem;
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid var(--p-content-border-color);
transition: all 0.2s ease;
margin-left: 1rem;
}

View File

@@ -15,7 +15,7 @@
width: 100%;
padding: 8px 12px;
font-size: 16px;
border: 1px solid var(--text-color-secondary);
border: 1px solid var(--text-secondary-color);
border-radius: 4px;
margin-bottom: 15px;
box-sizing: border-box;
@@ -27,6 +27,6 @@
}
.icon-search::placeholder {
color: var(--text-color-secondary);
color: var(--text-secondary-color);
opacity: 1;
}

View File

@@ -1,31 +1,30 @@
<div class="flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden"
style="background: linear-gradient(60deg, var(--primary-color) 0%, #1e3a8a 100%);">
<div class="flex flex-col items-center justify-center -mt-48 w-[90%] sm:w-auto sm:min-w-[400px] md:min-w-[500px]">
<div class="w-full" style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
<div class="w-full bg-surface-900 pt-8 pb-14 px-8 sm:px-12" style="border-radius: 53px">
<div class="text-center mb-8">
<svg class="mb-8 w-14 h-14 mx-auto" viewBox="0 0 126 126" fill="var(--primary-color)" xmlns="http://www.w3.org/2000/svg">
<path
d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
<path
d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z"
fill="white"/>
</svg>
<div class="text-3xl font-medium mb-4">Welcome to Booklore</div>
<span class="font-medium">Sign in to continue</span>
<div class="login-container">
<div class="login-card-wrapper">
<div class="login-card-border">
<div class="login-card">
<div class="login-header">
<div class="logo-wrapper">
<svg class="logo-icon" viewBox="0 0 126 126" fill="var(--primary-color)" xmlns="http://www.w3.org/2000/svg">
<path
d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
<path
d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z"
fill="white"/>
</svg>
</div>
<h1 class="login-title">Welcome Back</h1>
<p class="login-subtitle">Sign in to continue your journey</p>
</div>
@if (showOidcBypassInfo) {
<div class="mb-4 p-3 rounded-md bg-yellow-900/30 border border-yellow-600/50 w-full">
<div class="flex flex-col items-center">
<div class="flex items-center gap-2 text-yellow-200 text-sm mb-2">
<div class="oidc-warning">
<div class="warning-content">
<div class="warning-header">
<i class="pi pi-exclamation-triangle"></i>
<span class="font-medium">OIDC Authentication Issues Detected</span>
<span>OIDC Authentication Issues</span>
</div>
<p class="text-xs text-yellow-300 mb-3 text-center">
{{ oidcBypassMessage }}
</p>
<div class="flex gap-4">
<p class="warning-message">{{ oidcBypassMessage }}</p>
<div class="warning-actions">
<p-button
size="small"
severity="warn"
@@ -45,9 +44,12 @@
</div>
}
<form class="flex flex-col gap-4 w-full" #loginForm="ngForm" (ngSubmit)="login()">
<div class="flex flex-col gap-2">
<label for="username" class="text-lg font-medium">Username</label>
<form class="login-form" #loginForm="ngForm" (ngSubmit)="login()">
<div class="form-field">
<label for="username" class="form-label">
<i class="pi pi-user"></i>
<span>Username</span>
</label>
<input
fluid
pInputText
@@ -56,13 +58,16 @@
required
[(ngModel)]="username"
placeholder="Enter your username"
class="w-full"
class="form-input"
autocomplete="username"
/>
</div>
<div class="flex flex-col gap-2">
<label for="password" class="text-lg font-medium">Password</label>
<div class="form-field">
<label for="password" class="form-label">
<i class="pi pi-lock"></i>
<span>Password</span>
</label>
<p-password
id="password"
name="password"
@@ -71,7 +76,6 @@
[feedback]="false"
placeholder="Enter your password"
[toggleMask]="true"
class="mb-4"
[fluid]="true"
autocomplete="current-password"
></p-password>
@@ -80,47 +84,47 @@
<p-button
fluid
type="submit"
label="Login"
label="Sign In"
icon="pi pi-sign-in"
[disabled]="!loginForm.valid || !username || !password"
class="w-full">
class="login-button">
</p-button>
@if (errorMessage) {
<p-message severity="error">{{ errorMessage }}</p-message>
<div class="error-message">
<p-message severity="error">{{ errorMessage }}</p-message>
</div>
}
@if (oidcEnabled && !isOidcBypassed) {
<div class="flex items-center gap-2 my-2">
<hr class="flex-grow" style="border: none; border-top: 1px solid var(--border-color);"/>
<span class="text-gray-300">or</span>
<hr class="flex-grow" style="border: none; border-top: 1px solid var(--border-color);"/>
<div class="divider">
<span>or continue with</span>
</div>
<p-button
fluid
severity="info"
[label]="isOidcLoginInProgress ? 'Redirecting to ' + oidcName + '...' : 'Login with ' + oidcName"
severity="secondary"
[label]="isOidcLoginInProgress ? 'Redirecting to ' + oidcName + '...' : oidcName"
[icon]="isOidcLoginInProgress ? 'pi pi-spin pi-spinner' : 'pi pi-id-card'"
[disabled]="isOidcLoginInProgress"
(click)="loginWithOidc()"
class="w-full bg-gray-800 text-white hover:bg-gray-700"/>
class="oidc-button"/>
<div class="text-center">
<div class="help-text">
<button
type="button"
class="text-xs text-gray-400 hover:text-gray-300 underline"
class="text-link"
(click)="bypassOidc()">
Having trouble with {{ oidcName }}? Use local login only
Having trouble with {{ oidcName }}? Use local login
</button>
</div>
}
@if (oidcEnabled && isOidcBypassed) {
<div class="text-center mt-4">
<div class="help-text">
<button
type="button"
class="text-xs text-blue-400 hover:text-blue-300 underline"
class="text-link primary"
(click)="enableOidc()">
Re-enable {{ oidcName }} login
</button>

View File

@@ -0,0 +1,169 @@
@use '../../styles/auth-shared' as auth;
.login-container {
@include auth.auth-container;
}
.login-card-wrapper {
@include auth.auth-card-wrapper(460px, -8rem);
}
.login-card-border {
@include auth.auth-card-border;
}
.login-card {
@include auth.auth-card;
}
.login-header {
@include auth.auth-header;
}
.logo-wrapper {
@include auth.auth-logo;
}
.logo-icon {
@include auth.auth-logo-icon;
}
.login-title {
@include auth.auth-title;
}
.login-subtitle {
@include auth.auth-subtitle;
font-size: 1rem;
}
.oidc-warning {
margin-bottom: 1.5rem;
padding: 1rem;
border-radius: 0.75rem;
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
animation: fadeIn 0.3s ease-out;
}
.warning-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.warning-header {
display: flex;
align-items: center;
gap: 0.5rem;
color: #fbbf24;
font-size: 0.875rem;
font-weight: 600;
i {
font-size: 1rem;
}
}
.warning-message {
font-size: 0.75rem;
color: #fcd34d;
line-height: 1.5;
margin: 0;
}
.warning-actions {
display: flex;
gap: 0.75rem;
justify-content: center;
}
.login-form {
@include auth.auth-form(1.5rem);
}
.form-field {
@include auth.form-field;
}
.form-label {
@include auth.form-label;
}
.form-input {
@include auth.form-input;
}
.login-button {
@include auth.auth-button;
margin-top: 0.5rem;
}
.error-message {
@include auth.error-message;
}
.divider {
display: flex;
align-items: center;
text-align: center;
margin: 1.5rem 0;
&::before,
&::after {
content: '';
flex: 1;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
span {
padding: 0 1rem;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.5);
font-weight: 500;
}
}
.oidc-button {
::ng-deep button {
height: 2.75rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
&:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
}
}
}
.help-text {
text-align: center;
margin-top: 0.5rem;
}
.text-link {
background: none;
border: none;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.5);
text-decoration: underline;
cursor: pointer;
transition: color 0.2s ease;
padding: 0;
&:hover {
color: rgba(255, 255, 255, 0.8);
}
&.primary {
color: var(--primary-color);
&:hover {
color: #60a5fa;
}
}
}

View File

@@ -1,42 +1,50 @@
<div class="flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden"
style="background: linear-gradient(60deg, var(--primary-color) 0%, #1e3a8a 100%);">
<div class="flex flex-col items-center justify-center -mt-48">
<div style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
<div class="w-full bg-surface-900 py-16 px-8 sm:px-16" style="border-radius: 53px">
<div class="text-center mb-8">
<svg class="mb-8 w-16 h-16 mx-auto" viewBox="0 0 126 126" fill="var(--primary-color)" xmlns="http://www.w3.org/2000/svg">
<path
d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
<path
d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z"
fill="white"/>
</svg>
<div class="text-3xl font-medium mb-4">Welcome to Booklore</div>
<span class="text-xl">Setup Initial Admin Account</span>
<div class="setup-container">
<div class="setup-card-wrapper">
<div class="setup-card-border">
<div class="setup-card">
<div class="setup-header">
<div class="logo-wrapper">
<svg class="logo-icon" viewBox="0 0 126 126" fill="var(--primary-color)" xmlns="http://www.w3.org/2000/svg">
<path
d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
<path
d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z"
fill="white"/>
</svg>
</div>
<h1 class="setup-title">Welcome to Booklore</h1>
<p class="setup-subtitle">Setup your initial admin account to get started</p>
</div>
<form class="flex flex-col gap-4" [formGroup]="setupForm" (ngSubmit)="onSubmit()">
<div class="flex flex-col gap-2">
<label for="username" class="text-lg font-medium">Username</label>
<form class="setup-form" [formGroup]="setupForm" (ngSubmit)="onSubmit()">
<div class="form-field">
<label for="username" class="form-label">
<i class="pi pi-user"></i>
<span>Username</span>
</label>
<input
pInputText
id="username"
name="username"
required
formControlName="username"
placeholder="Enter your username"
class="w-full md:w-[30rem]"
placeholder="Choose a username"
class="form-input"
autocomplete="username"
/>
@if (setupForm.get('username')?.invalid && setupForm.get('username')?.touched) {
<small class="text-red-500">
Username is required.
<small class="field-error">
<i class="pi pi-exclamation-circle"></i>
Username is required
</small>
}
</div>
<div class="flex flex-col gap-2">
<label for="name" class="text-lg font-medium">Name</label>
<div class="form-field">
<label for="name" class="form-label">
<i class="pi pi-id-card"></i>
<span>Full Name</span>
</label>
<input
pInputText
id="name"
@@ -44,68 +52,85 @@
required
formControlName="name"
placeholder="Enter your full name"
class="w-full md:w-[30rem]"
class="form-input"
autocomplete="name"
/>
@if (setupForm.get('name')?.invalid && setupForm.get('name')?.touched) {
<small class="text-red-500">
Name is required.
<small class="field-error">
<i class="pi pi-exclamation-circle"></i>
Name is required
</small>
}
</div>
<div class="flex flex-col gap-2">
<label for="email" class="text-lg font-medium">Email</label>
<div class="form-field">
<label for="email" class="form-label">
<i class="pi pi-envelope"></i>
<span>Email Address</span>
</label>
<input
pInputText
id="email"
name="email"
type="email"
required
formControlName="email"
placeholder="Enter your email"
class="w-full md:w-[30rem]"
placeholder="Enter your email address"
class="form-input"
autocomplete="email"
/>
@if (setupForm.get('email')?.invalid && setupForm.get('email')?.touched) {
<small class="text-red-500">
Valid email is required.
<small class="field-error">
<i class="pi pi-exclamation-circle"></i>
Valid email is required
</small>
}
</div>
<div class="flex flex-col gap-2">
<label for="password" class="text-lg font-medium">Password</label>
<div class="form-field">
<label for="password" class="form-label">
<i class="pi pi-lock"></i>
<span>Password</span>
</label>
<input
pInputText
id="password"
name="password"
type="password"
required
formControlName="password"
placeholder="Enter new password"
class="w-full md:w-[30rem]"
placeholder="Create a secure password"
class="form-input"
autocomplete="new-password"
/>
@if (setupForm.get('password')?.invalid && setupForm.get('password')?.touched) {
<small class="text-red-500">
Minimum 6 characters required.
<small class="field-error">
<i class="pi pi-exclamation-circle"></i>
Minimum 6 characters required
</small>
}
</div>
<div class="flex flex-col gap-3 mt-4">
<p-button
fluid
type="submit"
[label]="loading ? 'Creating' : 'Create Admin Account'"
[disabled]="loading || setupForm.invalid"
icon="pi pi-user-plus"
>
</p-button>
<p-button
fluid
type="submit"
[label]="loading ? 'Creating Account...' : 'Create Admin Account'"
[icon]="loading ? 'pi pi-spin pi-spinner' : 'pi pi-user-plus'"
[disabled]="loading || setupForm.invalid"
class="setup-button"
/>
@if (error) {
<p-message severity="error" text="{{ error }}"></p-message>
}
@if (success) {
<p-message severity="success" text="Admin account created! Redirecting…"></p-message>
}
</div>
@if (error) {
<div class="message-container error-message">
<p-message severity="error">{{ error }}</p-message>
</div>
}
@if (success) {
<div class="message-container success-message">
<p-message severity="success">Admin account created successfully! Redirecting...</p-message>
</div>
}
</form>
</div>
</div>

View File

@@ -0,0 +1,73 @@
@use '../../styles/auth-shared' as auth;
.setup-container {
@include auth.auth-container;
}
.setup-card-wrapper {
@include auth.auth-card-wrapper(540px, -6rem);
}
.setup-card-border {
@include auth.auth-card-border;
}
.setup-card {
@include auth.auth-card;
}
.setup-header {
@include auth.auth-header;
}
.logo-wrapper {
@include auth.auth-logo;
}
.logo-icon {
@include auth.auth-logo-icon;
}
.setup-title {
@include auth.auth-title;
}
.setup-subtitle {
@include auth.auth-subtitle;
}
.setup-form {
@include auth.auth-form;
}
.form-field {
@include auth.form-field;
}
.form-label {
@include auth.form-label;
}
.form-input {
@include auth.form-input;
}
.field-error {
@include auth.field-error;
}
.setup-button {
@include auth.auth-button;
}
.message-container {
@include auth.message-container;
}
.error-message {
@include auth.error-message;
}
.success-message {
@include auth.success-message;
}

View File

@@ -0,0 +1,287 @@
@mixin auth-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
min-width: 100vw;
overflow: hidden;
background: linear-gradient(135deg, var(--primary-color) 0%, #1e3a8a 50%, #0f172a 100%);
position: relative;
&::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%);
animation: float 20s ease-in-out infinite;
}
}
@mixin auth-card-wrapper($max-width: 500px, $margin-top: -6rem) {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: $margin-top;
width: 90%;
max-width: $max-width;
position: relative;
z-index: 1;
animation: slideUp 0.6s ease-out;
@media (max-width: 640px) {
width: 95%;
margin-top: calc($margin-top / 2);
}
}
@mixin auth-card-border {
width: 100%;
border-radius: 2rem;
padding: 2px;
background: linear-gradient(135deg, var(--primary-color) 0%, rgba(59, 130, 246, 0.3) 50%, transparent 100%);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
transition: all 0.3s ease;
&:hover {
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.5);
transform: translateY(-2px);
}
}
@mixin auth-card {
width: 100%;
background: rgba(17, 24, 39, 0.95);
backdrop-filter: blur(20px);
border-radius: calc(2rem - 2px);
padding: 3rem 2.5rem;
@media (max-width: 640px) {
padding: 2rem 1.5rem;
}
}
@mixin auth-header {
text-align: center;
margin-bottom: 2.5rem;
}
@mixin auth-logo {
margin-bottom: 1.5rem;
animation: pulse 2s ease-in-out infinite;
}
@mixin auth-logo-icon {
width: 4rem;
height: 4rem;
margin: 0 auto;
filter: drop-shadow(0 4px 12px rgba(59, 130, 246, 0.4));
transition: transform 0.3s ease;
&:hover {
transform: scale(1.1) rotate(5deg);
}
}
@mixin auth-icon-circle {
width: 4rem;
height: 4rem;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-color) 0%, #2563eb 100%);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
transition: transform 0.3s ease;
i {
font-size: 1.75rem;
color: white;
}
&:hover {
transform: scale(1.1) rotate(5deg);
}
}
@mixin auth-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, #ffffff 0%, var(--primary-color) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@media (max-width: 640px) {
font-size: 1.75rem;
}
}
@mixin auth-subtitle {
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 400;
line-height: 1.5;
max-width: 400px;
margin: 0 auto;
}
@mixin auth-form($gap: 1.25rem) {
display: flex;
flex-direction: column;
gap: $gap;
}
@mixin form-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
@mixin form-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.95rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
i {
color: var(--primary-color);
font-size: 0.875rem;
}
}
@mixin form-input {
transition: all 0.3s ease;
&:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
}
@mixin field-error {
display: flex;
align-items: center;
gap: 0.375rem;
color: #ef4444;
font-size: 0.8rem;
margin-top: -0.25rem;
animation: fadeIn 0.3s ease-out;
i {
font-size: 0.75rem;
}
}
@mixin auth-button {
margin-top: 0.75rem;
::ng-deep button {
height: 3rem;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
background: linear-gradient(135deg, var(--primary-color) 0%, #2563eb 100%);
border: none;
&:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.4);
}
&:active:not(:disabled) {
transform: translateY(0);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
}
@mixin message-container {
animation: fadeIn 0.3s ease-out;
}
@mixin error-message {
animation: shake 0.4s ease-in-out, fadeIn 0.3s ease-out;
}
@mixin success-message {
animation: successPulse 0.5s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes float {
0%, 100% {
transform: translate(0, 0) rotate(0deg);
}
33% {
transform: translate(30px, -30px) rotate(120deg);
}
66% {
transform: translate(-20px, 20px) rotate(240deg);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
25% {
transform: translateX(-10px);
}
75% {
transform: translateX(10px);
}
}
@keyframes successPulse {
0% {
opacity: 0;
transform: scale(0.95);
}
50% {
transform: scale(1.02);
}
100% {
opacity: 1;
transform: scale(1);
}
}

View File

@@ -1,32 +1,41 @@
@use "variables";
* {
box-sizing: border-box;
box-sizing: border-box;
}
html {
height: 100%;
font-size: variables.$scale;
height: 100%;
font-size: variables.$scale;
}
body {
font-family: var(--font-family);
color: var(--text-color);
background-color: var(--ground-background);
margin: 0;
padding: 0;
min-height: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: var(--font-family), serif;
color: var(--text-color);
background-color: var(--ground-background);
margin: 0;
padding: 0;
min-height: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--glow-image: url(https://github.com/booklore-app/booklore-docs/blob/master/static/img/cdn/bg.png?raw=true), radial-gradient(50% 50% at center -25px, var(--p-primary-color) 0%, #000000 100%);
--glow-blend: hard-light, color-dodge;
}
a {
text-decoration: none;
color: var(--primary-color);
text-decoration: none;
color: var(--primary-color);
}
.layout-wrapper {
min-height: 100dvh;
min-height: 100dvh;
scroll-behavior: smooth;
background-color: var(--ground-background);
background-blend-mode: var(--glow-blend);
background-image: var(--glow-image);
background-position: top;
background-repeat: no-repeat;
background-size: auto 20rem;
}
p {

View File

@@ -9,7 +9,7 @@
top: 0;
width: 100%;
padding: 0 1rem;
background-color: var(--card-background);
background-color: color-mix(in srgb, var(--card-background) 25%, transparent);
border: none;
border-bottom: 1px solid var(--p-content-border-color);
transition: left variables.$transitionDuration;

View File

@@ -8,6 +8,7 @@
/* Content & Text */
--text-color: var(--p-surface-0);
--text-color-secondary: var(--p-surface-300);
--text-secondary-color: var(--p-surface-400);
--high-contrast-text-color: var(--p-surface-0);
--p-text-muted-color: var(--p-surface-400);