feat: enhance user input and rules panel components
- Updated dark-light mode button styles for improved responsiveness. - Enhanced rules panel with additional buttons for moving rules up and down, and updated styles for better layout. - Added active rule count display in the rules panel header. - Improved user input component layout with section headings and updated button styles. - Refactored day input component to use grid layout for better alignment and responsiveness. - Added new translations for user input and rules panel features. - Implemented tests for new functionality in rules panel and day input components. - Added application routes test to ensure correct routing configuration.
This commit is contained in:
1 parent
31314f3969
commit
609b3dece9
30 files changed
+870
-382
No files matched your search
+18
-10
@@ -1,13 +1,21 @@
|
|||||||
<app-dark-light-mode-button class="modeButton"></app-dark-light-mode-button>
|
|
||||||
@if (showPlanner()) {
|
@if (showPlanner()) {
|
||||||
<main class="main">
|
<div class="app-shell">
|
||||||
<h1 style="display: flex; gap: 1rem"><mat-icon>beach_access</mat-icon> {{ title() }}</h1>
|
<header class="app-header">
|
||||||
<div class="content">
|
<div class="brand">
|
||||||
<app-user-input></app-user-input>
|
<mat-icon aria-hidden="true">beach_access</mat-icon>
|
||||||
<mat-divider [vertical]="true" />
|
<h1>{{ title() }}</h1>
|
||||||
<mat-divider />
|
</div>
|
||||||
<app-calendar-year></app-calendar-year>
|
<app-dark-light-mode-button></app-dark-light-mode-button>
|
||||||
</div>
|
</header>
|
||||||
</main>
|
|
||||||
|
<main class="planner-layout">
|
||||||
|
<section class="configuration-panel" aria-labelledby="leave-settings-title">
|
||||||
|
<app-user-input></app-user-input>
|
||||||
|
</section>
|
||||||
|
<section class="results-panel" aria-labelledby="planning-results-title">
|
||||||
|
<app-calendar-year></app-calendar-year>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { routes } from './app.routes';
|
||||||
|
|
||||||
|
describe('application routes', () => {
|
||||||
|
it('declares the root URL for the planner shell', () => {
|
||||||
|
expect(routes).toContainEqual({ path: '', pathMatch: 'full', children: [] });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
|
{ path: '', pathMatch: 'full', children: [] },
|
||||||
{
|
{
|
||||||
path: 'benchmark',
|
path: 'benchmark',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
|
|||||||
+57
-65
@@ -1,79 +1,71 @@
|
|||||||
.main {
|
:host {
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.app-shell {
|
||||||
|
min-height: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-wrap: wrap;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
gap: 1rem;
|
||||||
padding: 0 24px;
|
width: min(100%, 120rem);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding: 1rem clamp(1rem, 2vw, 2rem);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-block-end: 1px solid var(--ui-border, #c7c7d0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
color: var(--mat-sys-primary, #005cbb);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.planner-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(23rem, 29rem) minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: 2rem;
|
||||||
|
width: min(100%, 120rem);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding: 1.5rem clamp(1rem, 2vw, 2rem) 3rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
app-user-input {
|
.configuration-panel,
|
||||||
display: block;
|
.results-panel,
|
||||||
flex: 3;
|
app-user-input,
|
||||||
min-width: 350px;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
app-calendar-year {
|
app-calendar-year {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 7;
|
min-width: 0;
|
||||||
min-width: 700px;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
.configuration-panel {
|
||||||
.content {
|
padding: 1.25rem;
|
||||||
flex-direction: column;
|
background: var(--ui-panel-bg, #f7f7fa);
|
||||||
align-items: stretch;
|
border: 1px solid var(--ui-border, #c7c7d0);
|
||||||
padding: 0 16px;
|
border-radius: var(--ui-radius-lg, 1rem);
|
||||||
}
|
|
||||||
|
|
||||||
app-user-input,
|
|
||||||
app-calendar-year {
|
|
||||||
flex: none;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
app-user-input {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-divider-vertical {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-divider-horizontal {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modeButton {
|
@media (max-width: 75rem) {
|
||||||
position: fixed;
|
.planner-layout {
|
||||||
top: 20px;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
right: 20px;
|
}
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-divider-vertical {
|
|
||||||
height: 80vh;
|
|
||||||
width: 1px;
|
|
||||||
margin: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-divider-horizontal {
|
|
||||||
width: 80%;
|
|
||||||
height: 1px;
|
|
||||||
margin: 16px 0;
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
import { provideNativeDateAdapter } from '@angular/material/core';
|
import { provideNativeDateAdapter } from '@angular/material/core';
|
||||||
import { MatDivider } from '@angular/material/divider';
|
|
||||||
import { UserInput } from './components/user-input/user-input';
|
import { UserInput } from './components/user-input/user-input';
|
||||||
import { CalendarYear } from './components/calendar-year/calendar-year';
|
import { CalendarYear } from './components/calendar-year/calendar-year';
|
||||||
import { DarkLightModeButton } from './components/dark-light-mode-button/dark-light-mode-button';
|
import { DarkLightModeButton } from './components/dark-light-mode-button/dark-light-mode-button';
|
||||||
@@ -16,7 +15,6 @@ function isBenchmarkUrl(url: string): boolean {
|
|||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [
|
imports: [
|
||||||
MatDatepickerModule,
|
MatDatepickerModule,
|
||||||
MatDivider,
|
|
||||||
UserInput,
|
UserInput,
|
||||||
CalendarYear,
|
CalendarYear,
|
||||||
DarkLightModeButton,
|
DarkLightModeButton,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="calendar">
|
<div class="calendar" role="group" [attr.aria-label]="getMonthName() + ' ' + year">
|
||||||
<h3>{{ getMonthName() }} {{ year }}</h3>
|
<h3>{{ getMonthName() }} {{ year }}</h3>
|
||||||
<div>
|
<div>
|
||||||
<div class="row header">
|
<div class="row header">
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
@if (day === null) {
|
@if (day === null) {
|
||||||
<p class="day empty"></p>
|
<p class="day empty"></p>
|
||||||
} @else {
|
} @else {
|
||||||
|
@let dayTooltip = getDayTooltip(day);
|
||||||
<p
|
<p
|
||||||
[class.past]="day < getNow()"
|
[class.past]="day < getNow()"
|
||||||
[class.start]="selectedDates.isStart(day)"
|
[class.start]="selectedDates.isStart(day)"
|
||||||
@@ -32,8 +33,11 @@
|
|||||||
[class.selected]="selectedDates.isSelected(day)"
|
[class.selected]="selectedDates.isSelected(day)"
|
||||||
[class]="selectedDates.getClasses(day)"
|
[class]="selectedDates.getClasses(day)"
|
||||||
class="day number"
|
class="day number"
|
||||||
[matTooltip]="getDayTooltip(day)"
|
[matTooltip]="dayTooltip"
|
||||||
[matTooltipShowDelay]="500"
|
[matTooltipShowDelay]="500"
|
||||||
|
[attr.aria-label]="
|
||||||
|
dayTooltip ? day.getDate() + ': ' + dayTooltip : null
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ day.getDate() }}
|
{{ day.getDate() }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -69,8 +69,6 @@
|
|||||||
.today {
|
.today {
|
||||||
border-color: var(--today-border-color);
|
border-color: var(--today-border-color);
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
width: 26px !important;
|
|
||||||
height: 22px !important;
|
|
||||||
border-style: dashed !important;
|
border-style: dashed !important;
|
||||||
|
|
||||||
:has(.dark-mode) {
|
:has(.dark-mode) {
|
||||||
@@ -88,8 +86,6 @@
|
|||||||
|
|
||||||
.closed-day {
|
.closed-day {
|
||||||
border: 2px solid var(--closed-day-bg);
|
border: 2px solid var(--closed-day-bg);
|
||||||
width: 26px !important;
|
|
||||||
height: 22px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rtt {
|
.rtt {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<button matFab extended (click)="openDialog()">
|
<button mat-stroked-button type="button" (click)="openDialog()">
|
||||||
<mat-icon>settings</mat-icon>
|
<mat-icon aria-hidden="true">settings</mat-icon>
|
||||||
{{ tr().settings.title }}
|
{{ tr().settings.title }}
|
||||||
</button>
|
</button>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
}
|
||||||
@@ -32,15 +32,30 @@ const incomplete: PlanningIncompleteOutcome = {
|
|||||||
const plan: PlanOutcome = {
|
const plan: PlanOutcome = {
|
||||||
kind: 'Plan',
|
kind: 'Plan',
|
||||||
horizon: { start: '2024-01-01', end: '2025-01-01' },
|
horizon: { start: '2024-01-01', end: '2025-01-01' },
|
||||||
placements: [],
|
placements: [
|
||||||
|
{
|
||||||
|
date: '2024-05-03',
|
||||||
|
balanceId: 'cp',
|
||||||
|
type: 'cp',
|
||||||
|
ruleIds: [],
|
||||||
|
supporting: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '2024-05-06',
|
||||||
|
balanceId: 'cp',
|
||||||
|
type: 'cp',
|
||||||
|
ruleIds: [],
|
||||||
|
supporting: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
timeOffPeriods: [],
|
timeOffPeriods: [],
|
||||||
preferences: [],
|
preferences: [],
|
||||||
remainingBalances: [],
|
remainingBalances: [],
|
||||||
metrics: {
|
metrics: {
|
||||||
unplacedAtRisk: 0,
|
unplacedAtRisk: 0,
|
||||||
holidayWeekValue: 0,
|
holidayWeekValue: 0,
|
||||||
nonWorkingDayGain: 0,
|
nonWorkingDayGain: 4,
|
||||||
longestWorkSequence: 0,
|
longestWorkSequence: 6,
|
||||||
shortSequenceDeficit: 0,
|
shortSequenceDeficit: 0,
|
||||||
supportingPlacements: 0,
|
supportingPlacements: 0,
|
||||||
preferenceSatisfaction: [],
|
preferenceSatisfaction: [],
|
||||||
@@ -84,7 +99,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
expect(userInput.remainingVacationDaysSignal()).toEqual([]);
|
expect(userInput.remainingVacationDaysSignal()).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps a returned incumbent/result visible only when an outcome supplies one', async () => {
|
it('keeps and summarizes a returned result', async () => {
|
||||||
const calendar = {
|
const calendar = {
|
||||||
getSelectedDatesForYear: vi.fn(() => new SelectedDates()),
|
getSelectedDatesForYear: vi.fn(() => new SelectedDates()),
|
||||||
getDefaultPlanningHorizon: vi.fn(() => ({ start: '2024-01-01', end: '2025-01-01' })),
|
getDefaultPlanningHorizon: vi.fn(() => ({ start: '2024-01-01', end: '2025-01-01' })),
|
||||||
@@ -103,7 +118,13 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
const fixture = TestBed.createComponent(CalendarYear);
|
const fixture = TestBed.createComponent(CalendarYear);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
|
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||||
|
fixture.detectChanges();
|
||||||
expect(userInput.remainingVacationDaysSignal()).toEqual([]);
|
expect(userInput.remainingVacationDaysSignal()).toEqual([]);
|
||||||
|
const summaryValues = Array.from(
|
||||||
|
fixture.nativeElement.querySelectorAll('.plan-summary dd') as NodeListOf<HTMLElement>,
|
||||||
|
).map((element) => element.textContent?.trim());
|
||||||
|
expect(summaryValues).toEqual(['2', '4', '6']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('turns planner rejection into an incomplete error and clears loading', async () => {
|
it('turns planner rejection into an incomplete error and clears loading', async () => {
|
||||||
|
|||||||
@@ -3,33 +3,107 @@
|
|||||||
<div class="loading-overlay">
|
<div class="loading-overlay">
|
||||||
<div class="loading-spinner">
|
<div class="loading-spinner">
|
||||||
<div class="spinner-container">
|
<div class="spinner-container">
|
||||||
<mat-spinner [mode]="'determinate'" [value]="loadingProgress()"></mat-spinner>
|
<mat-spinner
|
||||||
|
[diameter]="72"
|
||||||
|
[mode]="'determinate'"
|
||||||
|
[value]="loadingProgress()"
|
||||||
|
></mat-spinner>
|
||||||
<div class="spinner-text">{{ loadingProgress() }}%</div>
|
<div class="spinner-text">{{ loadingProgress() }}%</div>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ tr().loading }}</p>
|
<p>{{ tr().loading }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (planningOutcome()?.kind === 'InvalidInput') {
|
|
||||||
<p class="planning-status" role="alert">{{ tr().planning.invalidInput }}</p>
|
<header class="results-header">
|
||||||
} @else if (planningOutcome()?.kind === 'PlanningConflict') {
|
<div class="results-heading">
|
||||||
<p class="planning-status" role="alert">{{ tr().planning.conflict }}</p>
|
<h2 id="planning-results-title">{{ tr().planning.title }}</h2>
|
||||||
} @else if (planningOutcome()?.kind === 'PlanningIncomplete') {
|
@if (planningOutcome()?.kind === 'InvalidInput') {
|
||||||
<p class="planning-status" role="status">
|
<p class="planning-status error" role="alert">
|
||||||
{{ hasPlannerError() ? tr().planning.error : tr().planning.incomplete }}
|
<mat-icon aria-hidden="true">error</mat-icon>
|
||||||
</p>
|
{{ tr().planning.invalidInput }}
|
||||||
} @else if (planningOutcome()?.kind === 'Plan') {
|
</p>
|
||||||
<p class="planning-status" role="status">{{ tr().planning.complete }}</p>
|
} @else if (planningOutcome()?.kind === 'PlanningConflict') {
|
||||||
}
|
<p class="planning-status error" role="alert">
|
||||||
@for (month of months; track $index) {
|
<mat-icon aria-hidden="true">error</mat-icon>
|
||||||
<mat-card class="card">
|
{{ tr().planning.conflict }}
|
||||||
<mat-card-content>
|
</p>
|
||||||
<app-calendar-month
|
} @else if (planningOutcome()?.kind === 'PlanningIncomplete') {
|
||||||
[monthIndex]="month"
|
<p
|
||||||
[year]="getYear(month)"
|
class="planning-status"
|
||||||
[selectedDates]="selectedDates"
|
[class.error]="hasPlannerError()"
|
||||||
></app-calendar-month>
|
[class.warning]="!hasPlannerError()"
|
||||||
</mat-card-content>
|
[attr.role]="hasPlannerError() ? 'alert' : 'status'"
|
||||||
</mat-card>
|
>
|
||||||
|
<mat-icon aria-hidden="true">{{
|
||||||
|
hasPlannerError() ? 'error' : 'warning'
|
||||||
|
}}</mat-icon>
|
||||||
|
{{ hasPlannerError() ? tr().planning.error : tr().planning.incomplete }}
|
||||||
|
</p>
|
||||||
|
} @else if (planningOutcome()?.kind === 'Plan') {
|
||||||
|
<p class="planning-status success" role="status">
|
||||||
|
<mat-icon aria-hidden="true">check_circle</mat-icon>
|
||||||
|
{{ tr().planning.complete }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<app-calendar-settings></app-calendar-settings>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
@if (displayedPlan(); as plan) {
|
||||||
|
<dl class="plan-summary">
|
||||||
|
<div>
|
||||||
|
<dt>{{ tr().planning.daysPlaced }}</dt>
|
||||||
|
<dd>{{ plan.placements.length }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>{{ tr().planning.daysGained }}</dt>
|
||||||
|
<dd>{{ plan.metrics.nonWorkingDayGain }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>{{ tr().planning.longestWorkSequence }}</dt>
|
||||||
|
<dd>{{ plan.metrics.longestWorkSequence }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="calendar-legend" role="group" [attr.aria-label]="tr().planning.legend">
|
||||||
|
<strong>{{ tr().planning.legend }}</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="legend-swatch weekend" aria-hidden="true"></span
|
||||||
|
>{{ tr().dayTypes.weekend }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="legend-swatch closed-day" aria-hidden="true"></span
|
||||||
|
>{{ tr().dayTypes.closedDay }}
|
||||||
|
</li>
|
||||||
|
<li><span class="legend-swatch cp" aria-hidden="true"></span>{{ tr().dayTypes.cp }}</li>
|
||||||
|
<li>
|
||||||
|
<span class="legend-swatch rtt" aria-hidden="true"></span>{{ tr().dayTypes.rtt }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="legend-swatch other" aria-hidden="true"></span
|
||||||
|
>{{ tr().dayTypes.other }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="legend-swatch today" aria-hidden="true"></span
|
||||||
|
>{{ tr().dayTypes.today }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="month-grid">
|
||||||
|
@for (month of months; track $index) {
|
||||||
|
<mat-card class="month-card">
|
||||||
|
<mat-card-content>
|
||||||
|
<app-calendar-month
|
||||||
|
[monthIndex]="month"
|
||||||
|
[year]="getYear(month)"
|
||||||
|
[selectedDates]="selectedDates"
|
||||||
|
></app-calendar-month>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,40 +1,41 @@
|
|||||||
@use '@angular/material' as mat;
|
:host {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-year {
|
.calendar-year {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: row;
|
container: calendar-year / inline-size;
|
||||||
flex-wrap: wrap;
|
gap: 1.25rem;
|
||||||
justify-content: space-evenly;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
min-width: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-overlay {
|
.loading-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
inset: 0;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 4px;
|
min-height: 18rem;
|
||||||
z-index: 1000;
|
border-radius: var(--ui-radius-lg, 1rem);
|
||||||
background-color: var(--loading-overlay-bg);
|
z-index: 1;
|
||||||
|
background: var(--loading-overlay-bg, rgba(255, 255, 255, 0.92));
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 1rem;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: 16px;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--loading-text-color);
|
color: var(--loading-text-color, #333);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,24 +44,183 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
::ng-deep .mat-mdc-progress-spinner {
|
|
||||||
// Override Material spinner size
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner-text {
|
.spinner-text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
font-size: 20px;
|
font-size: 1.125rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--loading-text-color);
|
color: var(--loading-text-color, #333);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.results-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-block-end: 1rem;
|
||||||
|
border-block-end: 1px solid var(--ui-border, #c7c7d0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-heading {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.planning-status {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 65ch;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: var(--ui-radius-sm, 0.5rem);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: var(--ui-success-bg, #d8f5df);
|
||||||
|
color: var(--ui-success-text, #174d26);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background: var(--ui-warning-bg, #fff4d6);
|
||||||
|
color: var(--ui-warning-text, #4f3800);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: var(--ui-error-bg, #ffe0e0);
|
||||||
|
color: var(--ui-error-text, #751b1b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 11rem), 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
background: var(--ui-panel-bg, #f7f7fa);
|
||||||
|
border-radius: var(--ui-radius-md, 0.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
color: var(--ui-text-secondary, #45464f);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
order: -1;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem 1rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: var(--ui-panel-bg, #f7f7fa);
|
||||||
|
border-radius: var(--ui-radius-md, 0.75rem);
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
color: var(--ui-text-secondary, #45464f);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-swatch {
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: var(--ui-card-bg, #f1f1f5);
|
||||||
|
|
||||||
|
&.weekend {
|
||||||
|
background: var(--weekend-bg, #ffca39);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.closed-day {
|
||||||
|
border: 0.125rem solid var(--closed-day-bg, #0ee3ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cp {
|
||||||
|
background: var(--cp-bg, #eca0ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rtt {
|
||||||
|
background: var(--rtt-bg, #99ff87);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.other {
|
||||||
|
background: var(--other-bg, #ff9b9b);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.today {
|
||||||
|
border: 0.125rem dashed var(--today-border-color, #1976d2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-card {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, effect, signal, inject, untracked } from '@angular/core';
|
import { Component, computed, effect, signal, inject, untracked } from '@angular/core';
|
||||||
import { UserInputService } from '../../services/user-input-service';
|
import { UserInputService } from '../../services/user-input-service';
|
||||||
import { MatCard, MatCardContent } from '@angular/material/card';
|
import { MatCard, MatCardContent } from '@angular/material/card';
|
||||||
import { CalendarMonth } from '../calendar-month/calendar-month';
|
import { CalendarMonth } from '../calendar-month/calendar-month';
|
||||||
@@ -9,10 +9,19 @@ import { RuleService } from '../../services/rule-service';
|
|||||||
import { TranslationService } from '../../services/translation-service';
|
import { TranslationService } from '../../services/translation-service';
|
||||||
import { PlanOutcome, PlanningOutcome } from '../../services/leave-planner.service';
|
import { PlanOutcome, PlanningOutcome } from '../../services/leave-planner.service';
|
||||||
import { SelectableDayType, VacDay } from '../../services/calendar-service';
|
import { SelectableDayType, VacDay } from '../../services/calendar-service';
|
||||||
|
import { MatIcon } from '@angular/material/icon';
|
||||||
|
import { CalendarSettings } from '../calendar-settings/calendar-settings';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-calendar-year',
|
selector: 'app-calendar-year',
|
||||||
imports: [MatCard, CalendarMonth, MatCardContent, MatProgressSpinner],
|
imports: [
|
||||||
|
MatCard,
|
||||||
|
CalendarMonth,
|
||||||
|
MatCardContent,
|
||||||
|
MatProgressSpinner,
|
||||||
|
MatIcon,
|
||||||
|
CalendarSettings,
|
||||||
|
],
|
||||||
templateUrl: './calendar-year.html',
|
templateUrl: './calendar-year.html',
|
||||||
styleUrl: './calendar-year.scss',
|
styleUrl: './calendar-year.scss',
|
||||||
})
|
})
|
||||||
@@ -31,6 +40,14 @@ export class CalendarYear {
|
|||||||
|
|
||||||
protected loadingProgress = signal(0);
|
protected loadingProgress = signal(0);
|
||||||
protected planningOutcome = signal<PlanningOutcome | null>(null);
|
protected planningOutcome = signal<PlanningOutcome | null>(null);
|
||||||
|
protected displayedPlan = computed(() => {
|
||||||
|
const outcome = this.planningOutcome();
|
||||||
|
return outcome?.kind === 'Plan'
|
||||||
|
? outcome
|
||||||
|
: outcome?.kind === 'PlanningIncomplete'
|
||||||
|
? outcome.incumbent
|
||||||
|
: undefined;
|
||||||
|
});
|
||||||
private calculationVersion = 0;
|
private calculationVersion = 0;
|
||||||
|
|
||||||
protected hasPlannerError(): boolean {
|
protected hasPlannerError(): boolean {
|
||||||
@@ -127,12 +144,7 @@ export class CalendarYear {
|
|||||||
|
|
||||||
private applyOutcome(outcome: PlanningOutcome): void {
|
private applyOutcome(outcome: PlanningOutcome): void {
|
||||||
this.planningOutcome.set(outcome);
|
this.planningOutcome.set(outcome);
|
||||||
const plan =
|
const plan = this.displayedPlan();
|
||||||
outcome.kind === 'Plan'
|
|
||||||
? outcome
|
|
||||||
: outcome.kind === 'PlanningIncomplete'
|
|
||||||
? outcome.incumbent
|
|
||||||
: undefined;
|
|
||||||
if (plan) {
|
if (plan) {
|
||||||
this.selectedDates = this.calendarService.selectedDatesForPlan(plan);
|
this.selectedDates = this.calendarService.selectedDatesForPlan(plan);
|
||||||
this.userInput.remainingVacationDaysSignal.set(this.toLegacyBalances(plan));
|
this.userInput.remainingVacationDaysSignal.set(this.toLegacyBalances(plan));
|
||||||
|
|||||||
@@ -2,13 +2,18 @@ button {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 40px;
|
width: 2.75rem;
|
||||||
height: 40px;
|
height: 2.75rem;
|
||||||
transition: all 0.3s ease;
|
min-width: 2.75rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
transition:
|
||||||
|
background-color 200ms ease,
|
||||||
|
color 200ms ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<mat-expansion-panel class="rules-panel-container mat-elevation-z2">
|
<mat-expansion-panel class="rules-panel-container mat-elevation-z2">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>rule</mat-icon>
|
<mat-icon aria-hidden="true">rule</mat-icon>
|
||||||
{{ tr().rules.title }}
|
<span>{{ tr().rules.title }}</span>
|
||||||
|
<span class="rule-count">{{ activeRuleCount }} {{ tr().rules.active }}</span>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
<mat-panel-description> {{ tr().rules.subtitle }} </mat-panel-description>
|
<mat-panel-description> {{ tr().rules.subtitle }} </mat-panel-description>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
@@ -37,6 +38,22 @@
|
|||||||
>
|
>
|
||||||
</mat-slide-toggle>
|
</mat-slide-toggle>
|
||||||
<div class="rule-actions">
|
<div class="rule-actions">
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
[disabled]="i === 0"
|
||||||
|
[aria-label]="tr().rules.moveRuleUp"
|
||||||
|
(click)="moveRule(i, -1, $event)"
|
||||||
|
>
|
||||||
|
<mat-icon>keyboard_arrow_up</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
[disabled]="i === rules.length - 1"
|
||||||
|
[aria-label]="tr().rules.moveRuleDown"
|
||||||
|
(click)="moveRule(i, 1, $event)"
|
||||||
|
>
|
||||||
|
<mat-icon>keyboard_arrow_down</mat-icon>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
[aria-label]="tr().rules.editRule"
|
[aria-label]="tr().rules.editRule"
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
.rules-panel-container {
|
.rules-panel-container {
|
||||||
margin-top: 1rem;
|
margin: 0;
|
||||||
margin-bottom: 1rem;
|
border-radius: var(--ui-radius-md, 0.75rem);
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
|
|
||||||
mat-panel-title {
|
mat-panel-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 0.5rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 480px) {
|
.rule-count {
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
background: var(--mat-sys-primary-container, #d7e3ff);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--mat-sys-on-primary-container, #001b3f);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 30rem) {
|
||||||
mat-panel-description {
|
mat-panel-description {
|
||||||
display: none !important;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,10 +37,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rule-item {
|
.rule-item {
|
||||||
border: 1px solid var(--mat-sys-outline-variant);
|
border: 1px solid var(--mat-sys-outline-variant, #c7c7d0);
|
||||||
border-radius: 8px;
|
border-radius: var(--ui-radius-sm, 0.5rem);
|
||||||
transition: all 0.2s ease;
|
transition:
|
||||||
background: var(--mat-sys-surface-container);
|
border-color 200ms ease,
|
||||||
|
background-color 200ms ease;
|
||||||
|
background: var(--mat-sys-surface-container, #f1f1f5);
|
||||||
|
|
||||||
&:not(.editing) {
|
&:not(.editing) {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
@@ -41,11 +53,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(.editing) {
|
&:hover:not(.editing) {
|
||||||
border-color: var(--mat-sys-outline);
|
border-color: var(--mat-sys-outline, #777680);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.editing {
|
&.editing {
|
||||||
border: 1px solid var(--mat-sys-primary);
|
border: 1px solid var(--mat-sys-primary, #005cbb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,29 +88,31 @@
|
|||||||
|
|
||||||
.priority-statement {
|
.priority-statement {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: var(--mat-sys-primary-container);
|
background: var(--mat-sys-primary-container, #d7e3ff);
|
||||||
color: var(--mat-sys-on-primary-container);
|
color: var(--mat-sys-on-primary-container, #001b3f);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
mat-icon {
|
mat-icon {
|
||||||
color: var(--mat-sys-primary);
|
color: var(--mat-sys-primary, #005cbb);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-drag-grip {
|
.rule-drag-grip {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
margin-bottom: -8px;
|
margin-bottom: -8px;
|
||||||
color: var(--mat-sys-outline);
|
color: var(--mat-sys-outline, #777680);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
@@ -114,6 +128,7 @@
|
|||||||
|
|
||||||
.rule-header {
|
.rule-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -122,24 +137,21 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|
||||||
.rule-number {
|
.rule-number {
|
||||||
color: var(--mat-sys-primary);
|
color: var(--mat-sys-primary, #005cbb);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-actions {
|
.rule-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
flex-wrap: wrap;
|
||||||
opacity: 0.6;
|
gap: 0;
|
||||||
transition: opacity 0.2s;
|
margin-inline-start: auto;
|
||||||
|
|
||||||
.rule-item:hover & {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,8 +170,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
background: var(--mat-sys-surface-container-high);
|
background: var(--mat-sys-surface-container-high, #e9e9ee);
|
||||||
border-radius: 8px;
|
border-radius: var(--ui-radius-sm, 0.5rem);
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -195,6 +207,7 @@
|
|||||||
|
|
||||||
.editor-actions {
|
.editor-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@@ -203,6 +216,7 @@
|
|||||||
|
|
||||||
.add-buttons {
|
.add-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@@ -216,3 +230,13 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container (max-width: 30rem) {
|
||||||
|
.rule-summary {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,4 +105,10 @@ describe('RulesPanel', () => {
|
|||||||
|
|
||||||
expect(component.rules.find((r) => r.id === targetRule.id)?.enabled).toBe(!initialState);
|
expect(component.rules.find((r) => r.id === targetRule.id)?.enabled).toBe(!initialState);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should move a rule with the keyboard controls', () => {
|
||||||
|
const secondRuleId = component.rules[1].id;
|
||||||
|
component.moveRule(1, -1, { stopPropagation: () => {} } as Event);
|
||||||
|
expect(component.rules[0].id).toBe(secondRuleId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -140,12 +140,23 @@ export class RulesPanel {
|
|||||||
return this.ruleService.rules;
|
return this.ruleService.rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get activeRuleCount(): number {
|
||||||
|
return this.rules.filter((rule) => rule.enabled).length;
|
||||||
|
}
|
||||||
|
|
||||||
public drop(event: CdkDragDrop<VacationRule[]>) {
|
public drop(event: CdkDragDrop<VacationRule[]>) {
|
||||||
if (event.previousIndex !== event.currentIndex) {
|
if (event.previousIndex !== event.currentIndex) {
|
||||||
this.ruleService.reorderRules(event.previousIndex, event.currentIndex);
|
this.ruleService.reorderRules(event.previousIndex, event.currentIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public moveRule(index: number, offset: -1 | 1, event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const destination = index + offset;
|
||||||
|
if (destination >= 0 && destination < this.rules.length)
|
||||||
|
this.ruleService.reorderRules(index, destination);
|
||||||
|
}
|
||||||
|
|
||||||
public toggleRule(id: string) {
|
public toggleRule(id: string) {
|
||||||
this.ruleService.toggleRule(id);
|
this.ruleService.toggleRule(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<div class="user-input">
|
<div class="user-input">
|
||||||
<p>{{ tr().app.description }}</p>
|
<header class="section-heading">
|
||||||
|
<h2 id="leave-settings-title">{{ tr().userInput.title }}</h2>
|
||||||
|
<p>{{ tr().userInput.description }}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="listOfDays">
|
<div class="listOfDays">
|
||||||
@for (day of daysOff; track day.id) {
|
@for (day of daysOff; track day.id) {
|
||||||
<div class="day-input-wrapper" animate.enter="day-enter" animate.leave="day-leave">
|
<div class="day-input-wrapper" animate.enter="day-enter" animate.leave="day-leave">
|
||||||
@@ -10,25 +14,27 @@
|
|||||||
></app-day-input>
|
></app-day-input>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<mat-card class="add-day-card mat-elevation-z2" (click)="addDay()">
|
<button mat-stroked-button class="add-day-button" type="button" (click)="addDay()">
|
||||||
<button matFab [aria-label]="tr().userInput.addVacationDay">
|
<mat-icon aria-hidden="true">add</mat-icon>
|
||||||
<mat-icon>add</mat-icon>
|
{{ tr().userInput.addVacationDay }}
|
||||||
</button>
|
|
||||||
<h3>{{ tr().userInput.addVacationDay }}</h3>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button matFab extended (click)="triggerComputation()">
|
|
||||||
<mat-icon>calculate</mat-icon>
|
|
||||||
{{ tr().userInput.calculate }}
|
|
||||||
</button>
|
</button>
|
||||||
<app-calendar-settings></app-calendar-settings>
|
|
||||||
</div>
|
</div>
|
||||||
<app-rules-panel></app-rules-panel>
|
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
class="calculate-button"
|
||||||
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
(click)="triggerComputation()"
|
||||||
|
>
|
||||||
|
<mat-icon aria-hidden="true">calculate</mat-icon>
|
||||||
|
{{ tr().userInput.calculate }}
|
||||||
|
</button>
|
||||||
|
|
||||||
@if (hasRemainingDays()) {
|
@if (hasRemainingDays()) {
|
||||||
<mat-card class="mat-elevation-z2 warning-card">
|
<mat-card class="warning-card">
|
||||||
<mat-card-content class="warning-content">
|
<mat-card-content class="warning-content">
|
||||||
<mat-icon color="warn">warning</mat-icon>
|
<mat-icon color="warn" aria-hidden="true">warning</mat-icon>
|
||||||
<div class="warning">
|
<div class="warning">
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
@@ -49,4 +55,6 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<app-rules-panel></app-rules-panel>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,34 +1,38 @@
|
|||||||
.user-input {
|
.user-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 1rem;
|
gap: 1.25rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
.section-heading {
|
||||||
font-weight: bold;
|
display: flex;
|
||||||
margin-bottom: 0.5rem;
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
h2 {
|
||||||
color: #ff0000;
|
font-size: 1.5rem;
|
||||||
font-size: 0.875rem;
|
font-weight: 700;
|
||||||
margin-top: 0.25rem;
|
line-height: 1.2;
|
||||||
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-card {
|
p {
|
||||||
margin-bottom: 1rem;
|
max-width: 55ch;
|
||||||
}
|
color: var(--ui-text-secondary, #45464f);
|
||||||
|
line-height: 1.5;
|
||||||
.warning-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.listOfDays {
|
.listOfDays {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-input-wrapper {
|
.day-input-wrapper {
|
||||||
@@ -68,36 +72,47 @@ form {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-day-card {
|
.add-day-button,
|
||||||
display: flex;
|
.calculate-button {
|
||||||
flex-direction: row;
|
width: 100%;
|
||||||
align-items: center;
|
min-height: 3rem;
|
||||||
padding: 16px;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-day-card h3 {
|
.add-day-button {
|
||||||
flex-grow: 1;
|
display: flex;
|
||||||
margin: 0;
|
justify-content: flex-start;
|
||||||
font-weight: normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.calculate-button {
|
||||||
|
min-height: 3.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-card {
|
||||||
|
background: var(--ui-warning-bg, #fff4d6);
|
||||||
|
color: var(--ui-warning-text, #4f3800);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
gap: 0.75rem;
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
mat-icon {
|
||||||
gap: 1rem;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
|
display: block;
|
||||||
|
container: user-input / inline-size;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { UserInputService } from '../../services/user-input-service';
|
import { UserInputService } from '../../services/user-input-service';
|
||||||
import { CalendarSettings } from '../calendar-settings/calendar-settings';
|
|
||||||
import { RulesPanel } from '../rules-panel/rules-panel';
|
import { RulesPanel } from '../rules-panel/rules-panel';
|
||||||
import { MatCard, MatCardContent } from '@angular/material/card';
|
import { MatCard, MatCardContent } from '@angular/material/card';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
@@ -14,7 +13,6 @@ import { TranslationService } from '../../services/translation-service';
|
|||||||
selector: 'app-user-input',
|
selector: 'app-user-input',
|
||||||
imports: [
|
imports: [
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
CalendarSettings,
|
|
||||||
RulesPanel,
|
RulesPanel,
|
||||||
MatCard,
|
MatCard,
|
||||||
MatCardContent,
|
MatCardContent,
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
<!-- <mat-list-item role="listitem"> -->
|
|
||||||
<mat-card class="card">
|
<mat-card class="card">
|
||||||
<mat-form-field class="numberInput">
|
<mat-form-field class="numberInput" appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>{{ tr().userInput.days }}</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
|
step="1"
|
||||||
|
inputmode="numeric"
|
||||||
[value]="day().numberOfDays"
|
[value]="day().numberOfDays"
|
||||||
(input)="onInput($event)"
|
(input)="onInput($event)"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="dayTypeInput">
|
<mat-form-field class="dayTypeInput" appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>{{ tr().userInput.type }}</mat-label>
|
||||||
<mat-select (valueChange)="onTypeChange($event)" [value]="day().type">
|
<mat-select (valueChange)="onTypeChange($event)" [value]="day().type">
|
||||||
<mat-option></mat-option>
|
<mat-option></mat-option>
|
||||||
@for (dayType of selectableDayTypes; track $index) {
|
@for (dayType of selectableDayTypes; track $index) {
|
||||||
@@ -19,31 +22,37 @@
|
|||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="dateInput">
|
<button
|
||||||
<mat-label>{{ tr().userInput.availableDate }}</mat-label>
|
mat-icon-button
|
||||||
<input
|
class="delete-button"
|
||||||
matInput
|
type="button"
|
||||||
[matDatepicker]="availablePicker"
|
[aria-label]="tr().userInput.deleteEntry"
|
||||||
[value]="day().availableDate"
|
(click)="deleteEntry.emit()"
|
||||||
(dateChange)="addAvailabilityEvent($event)"
|
>
|
||||||
/>
|
<mat-icon aria-hidden="true">close</mat-icon>
|
||||||
<mat-datepicker-toggle matIconSuffix [for]="availablePicker"></mat-datepicker-toggle>
|
|
||||||
<mat-datepicker #availablePicker></mat-datepicker>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field class="dateInput">
|
|
||||||
<mat-label>{{ tr().userInput.chooseDate }}</mat-label>
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
type="datetime"
|
|
||||||
[matDatepicker]="picker"
|
|
||||||
[value]="day().expiryDate"
|
|
||||||
(dateChange)="addEvent($event)"
|
|
||||||
/>
|
|
||||||
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
|
||||||
<mat-datepicker #picker></mat-datepicker>
|
|
||||||
</mat-form-field>
|
|
||||||
<button matFab [aria-label]="tr().userInput.deleteEntry" (click)="deleteEntry.emit()">
|
|
||||||
<mat-icon>close</mat-icon>
|
|
||||||
</button>
|
</button>
|
||||||
|
<div class="date-fields">
|
||||||
|
<mat-form-field class="dateInput" appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>{{ tr().userInput.availableDate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[matDatepicker]="availablePicker"
|
||||||
|
[value]="day().availableDate"
|
||||||
|
(dateChange)="addAvailabilityEvent($event)"
|
||||||
|
/>
|
||||||
|
<mat-datepicker-toggle matIconSuffix [for]="availablePicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #availablePicker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class="dateInput" appearance="outline" subscriptSizing="dynamic">
|
||||||
|
<mat-label>{{ tr().userInput.chooseDate }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[matDatepicker]="picker"
|
||||||
|
[value]="day().expiryDate"
|
||||||
|
(dateChange)="addEvent($event)"
|
||||||
|
/>
|
||||||
|
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #picker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<!-- </mat-list-item> -->
|
|
||||||
@@ -1,72 +1,76 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
container-type: inline-size;
|
container: day-input / inline-size;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: row;
|
grid-template-columns: 5.25rem minmax(0, 1fr) 2.75rem;
|
||||||
align-items: center;
|
align-items: start;
|
||||||
justify-content: space-between;
|
gap: 0.75rem;
|
||||||
padding: 16px;
|
min-width: 0;
|
||||||
gap: 16px;
|
padding: 1rem;
|
||||||
|
background: var(--ui-card-bg, #f1f1f5);
|
||||||
|
border: 1px solid var(--ui-border, #c7c7d0);
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dateInput {
|
.card mat-form-field {
|
||||||
flex-grow: 1;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.numberInput {
|
.numberInput {
|
||||||
width: 80px;
|
grid-column: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
.dayTypeInput {
|
||||||
display: none;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 480px) {
|
.delete-button {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1;
|
||||||
|
min-width: 2.75rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
color: var(--ui-text-secondary, #45464f);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-fields {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 2;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container day-input (max-width: 24rem) {
|
||||||
.card {
|
.card {
|
||||||
flex-wrap: wrap;
|
grid-template-columns: 4.75rem minmax(0, 1fr) 2.75rem;
|
||||||
gap: 12px;
|
gap: 0.5rem;
|
||||||
padding: 12px;
|
padding: 0.75rem;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.numberInput {
|
.date-fields {
|
||||||
width: 70px;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
flex-grow: 0;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dayTypeInput {
|
@container day-input (min-width: 52rem) {
|
||||||
flex-grow: 1;
|
.card {
|
||||||
width: 0;
|
grid-template-columns:
|
||||||
min-width: 120px;
|
5.25rem minmax(9rem, 1fr) minmax(11rem, 1fr) minmax(11rem, 1fr)
|
||||||
|
2.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dateInput {
|
.date-fields {
|
||||||
width: 100%;
|
display: contents;
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 100%;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button[matFab] {
|
.delete-button {
|
||||||
width: 40px;
|
grid-column: 5;
|
||||||
height: 40px;
|
|
||||||
min-width: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
|
|
||||||
::ng-deep .mat-mdc-button-touch-target {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-icon {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { DayInput } from './day-input';
|
import { DayInput } from './day-input';
|
||||||
import { SelectableDayType, VacDay } from '../../../services/calendar-service';
|
import { SelectableDayType, VacDay } from '../../../services/calendar-service';
|
||||||
|
import { TranslationService } from '../../../services/translation-service';
|
||||||
|
|
||||||
describe('DayInput', () => {
|
describe('DayInput', () => {
|
||||||
let component: DayInput;
|
let component: DayInput;
|
||||||
@@ -18,6 +19,7 @@ describe('DayInput', () => {
|
|||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [DayInput],
|
imports: [DayInput],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
TestBed.inject(TranslationService).setLanguage('en');
|
||||||
|
|
||||||
fixture = TestBed.createComponent(DayInput);
|
fixture = TestBed.createComponent(DayInput);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
@@ -29,6 +31,14 @@ describe('DayInput', () => {
|
|||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('labels every balance field', () => {
|
||||||
|
const text = fixture.nativeElement.textContent as string;
|
||||||
|
expect(text).toContain('Days');
|
||||||
|
expect(text).toContain('Leave type');
|
||||||
|
expect(text).toContain('Available from');
|
||||||
|
expect(text).toContain('Expires on');
|
||||||
|
});
|
||||||
|
|
||||||
it('emits an availability-date change without mutating the input value', () => {
|
it('emits an availability-date change without mutating the input value', () => {
|
||||||
const emitted: VacDay[] = [];
|
const emitted: VacDay[] = [];
|
||||||
component.dayChange.subscribe((value) => emitted.push(value));
|
component.dayChange.subscribe((value) => emitted.push(value));
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export interface AppTranslations {
|
|||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
userInput: {
|
userInput: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
addVacationDay: string;
|
addVacationDay: string;
|
||||||
calculate: string;
|
calculate: string;
|
||||||
remainingDaysWarning: string;
|
remainingDaysWarning: string;
|
||||||
@@ -16,6 +18,8 @@ export interface AppTranslations {
|
|||||||
deleteEntry: string;
|
deleteEntry: string;
|
||||||
chooseDate: string;
|
chooseDate: string;
|
||||||
availableDate: string;
|
availableDate: string;
|
||||||
|
days: string;
|
||||||
|
type: string;
|
||||||
daySuffix: string;
|
daySuffix: string;
|
||||||
daysSuffix: string;
|
daysSuffix: string;
|
||||||
expires: string;
|
expires: string;
|
||||||
@@ -49,6 +53,9 @@ export interface AppTranslations {
|
|||||||
rules: {
|
rules: {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
active: string;
|
||||||
|
moveRuleUp: string;
|
||||||
|
moveRuleDown: string;
|
||||||
priorityInfo: string;
|
priorityInfo: string;
|
||||||
ifThenTemplate: string;
|
ifThenTemplate: string;
|
||||||
timeWindowTemplate: string;
|
timeWindowTemplate: string;
|
||||||
@@ -92,6 +99,11 @@ export interface AppTranslations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
planning: {
|
planning: {
|
||||||
|
title: string;
|
||||||
|
daysPlaced: string;
|
||||||
|
daysGained: string;
|
||||||
|
longestWorkSequence: string;
|
||||||
|
legend: string;
|
||||||
invalidInput: string;
|
invalidInput: string;
|
||||||
conflict: string;
|
conflict: string;
|
||||||
incomplete: string;
|
incomplete: string;
|
||||||
|
|||||||
+16
-4
@@ -5,13 +5,17 @@
|
|||||||
"description": "Optimieren Sie Ihre Urlaubstage basierend auf Feiertagen und Wochenenden!"
|
"description": "Optimieren Sie Ihre Urlaubstage basierend auf Feiertagen und Wochenenden!"
|
||||||
},
|
},
|
||||||
"userInput": {
|
"userInput": {
|
||||||
"addVacationDay": "Urlaubstag hinzufügen",
|
"title": "Urlaub konfigurieren",
|
||||||
"calculate": "Berechnen",
|
"description": "Fügen Sie jedes Urlaubskonto und seinen Gültigkeitszeitraum hinzu.",
|
||||||
|
"addVacationDay": "Urlaubskonto hinzufügen",
|
||||||
|
"calculate": "Plan berechnen",
|
||||||
"remainingDaysWarning": "{count} nicht platziert:",
|
"remainingDaysWarning": "{count} nicht platziert:",
|
||||||
"remainingDaysManualPlace": "Bitte manuell platzieren.",
|
"remainingDaysManualPlace": "Bitte manuell platzieren.",
|
||||||
"deleteEntry": "Eintrag löschen",
|
"deleteEntry": "Eintrag löschen",
|
||||||
"chooseDate": "Datum auswählen",
|
"chooseDate": "Gültig bis",
|
||||||
"availableDate": "Verfügbar ab",
|
"availableDate": "Verfügbar ab",
|
||||||
|
"days": "Tage",
|
||||||
|
"type": "Urlaubsart",
|
||||||
"daySuffix": "Tag",
|
"daySuffix": "Tag",
|
||||||
"daysSuffix": "Tage",
|
"daysSuffix": "Tage",
|
||||||
"expires": "abl.",
|
"expires": "abl.",
|
||||||
@@ -43,8 +47,11 @@
|
|||||||
"selectLanguage": "Sprache auswählen"
|
"selectLanguage": "Sprache auswählen"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"title": "Regeln",
|
"title": "Planungsregeln",
|
||||||
"subtitle": "Urlaubsregeln konfigurieren",
|
"subtitle": "Urlaubsregeln konfigurieren",
|
||||||
|
"active": "aktiv",
|
||||||
|
"moveRuleUp": "Regel nach oben verschieben",
|
||||||
|
"moveRuleDown": "Regel nach unten verschieben",
|
||||||
"priorityInfo": "Regeln werden von oben nach unten angewendet",
|
"priorityInfo": "Regeln werden von oben nach unten angewendet",
|
||||||
"ifThenTemplate": "Wenn {type} dann {placement}",
|
"ifThenTemplate": "Wenn {type} dann {placement}",
|
||||||
"timeWindowTemplate": "Zeitfenster {startDate} bis {endDate}",
|
"timeWindowTemplate": "Zeitfenster {startDate} bis {endDate}",
|
||||||
@@ -88,6 +95,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"planning": {
|
"planning": {
|
||||||
|
"title": "Ihr optimiertes Jahr",
|
||||||
|
"daysPlaced": "Eingeplante Urlaubstage",
|
||||||
|
"daysGained": "Gewonnene arbeitsfreie Tage",
|
||||||
|
"longestWorkSequence": "Längste Arbeitsphase",
|
||||||
|
"legend": "Kalenderlegende",
|
||||||
"invalidInput": "Die Planungsdaten sind ungültig.",
|
"invalidInput": "Die Planungsdaten sind ungültig.",
|
||||||
"conflict": "Die Verpflichtungen können nicht gemeinsam erfüllt werden.",
|
"conflict": "Die Verpflichtungen können nicht gemeinsam erfüllt werden.",
|
||||||
"incomplete": "Die Berechnung wurde vor Abschluss aller Qualitätsstufen beendet.",
|
"incomplete": "Die Berechnung wurde vor Abschluss aller Qualitätsstufen beendet.",
|
||||||
|
|||||||
+16
-4
@@ -5,13 +5,17 @@
|
|||||||
"description": "Optimize your vacation days based on holidays and weekends!"
|
"description": "Optimize your vacation days based on holidays and weekends!"
|
||||||
},
|
},
|
||||||
"userInput": {
|
"userInput": {
|
||||||
"addVacationDay": "Add a vacation day",
|
"title": "Configure your leave",
|
||||||
"calculate": "Calculate",
|
"description": "Add each leave balance and its validity period.",
|
||||||
|
"addVacationDay": "Add leave balance",
|
||||||
|
"calculate": "Calculate plan",
|
||||||
"remainingDaysWarning": "{count} vacation day{plural} not placed:",
|
"remainingDaysWarning": "{count} vacation day{plural} not placed:",
|
||||||
"remainingDaysManualPlace": "Please place them manually.",
|
"remainingDaysManualPlace": "Please place them manually.",
|
||||||
"deleteEntry": "Delete entry",
|
"deleteEntry": "Delete entry",
|
||||||
"chooseDate": "Choose a date",
|
"chooseDate": "Expires on",
|
||||||
"availableDate": "Available from",
|
"availableDate": "Available from",
|
||||||
|
"days": "Days",
|
||||||
|
"type": "Leave type",
|
||||||
"daySuffix": "day",
|
"daySuffix": "day",
|
||||||
"daysSuffix": "days",
|
"daysSuffix": "days",
|
||||||
"expires": "exp.",
|
"expires": "exp.",
|
||||||
@@ -43,8 +47,11 @@
|
|||||||
"selectLanguage": "Language"
|
"selectLanguage": "Language"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"title": "Custom Rules",
|
"title": "Planning rules",
|
||||||
"subtitle": "Manage optimization rules",
|
"subtitle": "Manage optimization rules",
|
||||||
|
"active": "active",
|
||||||
|
"moveRuleUp": "Move rule up",
|
||||||
|
"moveRuleDown": "Move rule down",
|
||||||
"priorityInfo": "The order of rules defines their execution priority (from top to bottom). Drag and drop to reorder.",
|
"priorityInfo": "The order of rules defines their execution priority (from top to bottom). Drag and drop to reorder.",
|
||||||
"ifThenTemplate": "If a holiday falls on a <strong>{day}</strong>, place <strong>{placeDays}</strong> day(s) <strong>{placeType}</strong> <strong>{placement}</strong> and get <strong>{rewardDays}</strong> day(s) <strong>{rewardType}</strong> as a bonus.",
|
"ifThenTemplate": "If a holiday falls on a <strong>{day}</strong>, place <strong>{placeDays}</strong> day(s) <strong>{placeType}</strong> <strong>{placement}</strong> and get <strong>{rewardDays}</strong> day(s) <strong>{rewardType}</strong> as a bonus.",
|
||||||
"timeWindowTemplate": "Place <strong>{placeDays}</strong> day(s) <strong>{placeType}</strong> between <strong>{startDate}</strong> and <strong>{endDate}</strong>.",
|
"timeWindowTemplate": "Place <strong>{placeDays}</strong> day(s) <strong>{placeType}</strong> between <strong>{startDate}</strong> and <strong>{endDate}</strong>.",
|
||||||
@@ -88,6 +95,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"planning": {
|
"planning": {
|
||||||
|
"title": "Your optimized year",
|
||||||
|
"daysPlaced": "Leave days placed",
|
||||||
|
"daysGained": "Non-working days gained",
|
||||||
|
"longestWorkSequence": "Longest work sequence",
|
||||||
|
"legend": "Calendar legend",
|
||||||
"invalidInput": "The planning input is invalid.",
|
"invalidInput": "The planning input is invalid.",
|
||||||
"conflict": "The obligations cannot be satisfied together.",
|
"conflict": "The obligations cannot be satisfied together.",
|
||||||
"incomplete": "Calculation stopped before all quality stages completed.",
|
"incomplete": "Calculation stopped before all quality stages completed.",
|
||||||
|
|||||||
+16
-4
@@ -5,13 +5,17 @@
|
|||||||
"description": "¡Optimice sus días de vacaciones según los días festivos y los fines de semana!"
|
"description": "¡Optimice sus días de vacaciones según los días festivos y los fines de semana!"
|
||||||
},
|
},
|
||||||
"userInput": {
|
"userInput": {
|
||||||
"addVacationDay": "Añadir un día de vacaciones",
|
"title": "Configura tus vacaciones",
|
||||||
"calculate": "Calcular",
|
"description": "Añade cada saldo de vacaciones y su periodo de validez.",
|
||||||
|
"addVacationDay": "Añadir saldo de vacaciones",
|
||||||
|
"calculate": "Calcular plan",
|
||||||
"remainingDaysWarning": "{count} día{plural} de vacaciones no colocado{plural}:",
|
"remainingDaysWarning": "{count} día{plural} de vacaciones no colocado{plural}:",
|
||||||
"remainingDaysManualPlace": "Por favor, colóquelos manualmente.",
|
"remainingDaysManualPlace": "Por favor, colóquelos manualmente.",
|
||||||
"deleteEntry": "Eliminar entrada",
|
"deleteEntry": "Eliminar entrada",
|
||||||
"chooseDate": "Elija una fecha",
|
"chooseDate": "Caduca el",
|
||||||
"availableDate": "Disponible desde",
|
"availableDate": "Disponible desde",
|
||||||
|
"days": "Días",
|
||||||
|
"type": "Tipo de permiso",
|
||||||
"daySuffix": "día",
|
"daySuffix": "día",
|
||||||
"daysSuffix": "días",
|
"daysSuffix": "días",
|
||||||
"expires": "exp.",
|
"expires": "exp.",
|
||||||
@@ -43,8 +47,11 @@
|
|||||||
"selectLanguage": "Idioma"
|
"selectLanguage": "Idioma"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"title": "Reglas personalizadas",
|
"title": "Reglas de planificación",
|
||||||
"subtitle": "Gestionar reglas de optimización",
|
"subtitle": "Gestionar reglas de optimización",
|
||||||
|
"active": "activas",
|
||||||
|
"moveRuleUp": "Subir regla",
|
||||||
|
"moveRuleDown": "Bajar regla",
|
||||||
"priorityInfo": "El orden de las reglas define su prioridad de ejecución (de arriba a abajo). Arrastre y suelte para reorganizar.",
|
"priorityInfo": "El orden de las reglas define su prioridad de ejecución (de arriba a abajo). Arrastre y suelte para reorganizar.",
|
||||||
"ifThenTemplate": "Si un día festivo cae en <strong>{day}</strong>, colocar <strong>{placeDays}</strong> día(s) <strong>{placeType}</strong> <strong>{placement}</strong> y obtener <strong>{rewardDays}</strong> día(s) <strong>{rewardType}</strong> de bonificación.",
|
"ifThenTemplate": "Si un día festivo cae en <strong>{day}</strong>, colocar <strong>{placeDays}</strong> día(s) <strong>{placeType}</strong> <strong>{placement}</strong> y obtener <strong>{rewardDays}</strong> día(s) <strong>{rewardType}</strong> de bonificación.",
|
||||||
"timeWindowTemplate": "Colocar <strong>{placeDays}</strong> día(s) <strong>{placeType}</strong> entre el <strong>{startDate}</strong> y el <strong>{endDate}</strong>.",
|
"timeWindowTemplate": "Colocar <strong>{placeDays}</strong> día(s) <strong>{placeType}</strong> entre el <strong>{startDate}</strong> y el <strong>{endDate}</strong>.",
|
||||||
@@ -88,6 +95,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"planning": {
|
"planning": {
|
||||||
|
"title": "Tu año optimizado",
|
||||||
|
"daysPlaced": "Días de vacaciones asignados",
|
||||||
|
"daysGained": "Días no laborables ganados",
|
||||||
|
"longestWorkSequence": "Período de trabajo más largo",
|
||||||
|
"legend": "Leyenda del calendario",
|
||||||
"invalidInput": "Los datos de planificación no son válidos.",
|
"invalidInput": "Los datos de planificación no son válidos.",
|
||||||
"conflict": "Las obligaciones no se pueden satisfacer juntas.",
|
"conflict": "Las obligaciones no se pueden satisfacer juntas.",
|
||||||
"incomplete": "El cálculo se detuvo antes de completar todas las etapas de calidad.",
|
"incomplete": "El cálculo se detuvo antes de completar todas las etapas de calidad.",
|
||||||
|
|||||||
+16
-4
@@ -5,13 +5,17 @@
|
|||||||
"description": "Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !"
|
"description": "Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !"
|
||||||
},
|
},
|
||||||
"userInput": {
|
"userInput": {
|
||||||
"addVacationDay": "Ajouter un jour de vacances",
|
"title": "Configurez vos congés",
|
||||||
"calculate": "Calculer",
|
"description": "Ajoutez chaque solde de congés et sa période de validité.",
|
||||||
|
"addVacationDay": "Ajouter un solde de congés",
|
||||||
|
"calculate": "Calculer le planning",
|
||||||
"remainingDaysWarning": "{count} jour{plural} de vacances non placé{plural}:",
|
"remainingDaysWarning": "{count} jour{plural} de vacances non placé{plural}:",
|
||||||
"remainingDaysManualPlace": "Veuillez les placer manuellement.",
|
"remainingDaysManualPlace": "Veuillez les placer manuellement.",
|
||||||
"deleteEntry": "Supprimer l’entrée",
|
"deleteEntry": "Supprimer l’entrée",
|
||||||
"chooseDate": "Choisissez une date",
|
"chooseDate": "Expire le",
|
||||||
"availableDate": "Disponible à partir du",
|
"availableDate": "Disponible à partir du",
|
||||||
|
"days": "Jours",
|
||||||
|
"type": "Type de congé",
|
||||||
"daySuffix": "jour",
|
"daySuffix": "jour",
|
||||||
"daysSuffix": "jours",
|
"daysSuffix": "jours",
|
||||||
"expires": "exp.",
|
"expires": "exp.",
|
||||||
@@ -43,8 +47,11 @@
|
|||||||
"selectLanguage": "Langue"
|
"selectLanguage": "Langue"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"title": "Règles Personnalisées",
|
"title": "Règles de planification",
|
||||||
"subtitle": "Gérer les règles d'optimisation",
|
"subtitle": "Gérer les règles d'optimisation",
|
||||||
|
"active": "actives",
|
||||||
|
"moveRuleUp": "Monter la règle",
|
||||||
|
"moveRuleDown": "Descendre la règle",
|
||||||
"priorityInfo": "L'ordre des règles définit leur priorité d'exécution (de haut en bas). Glissez-déposez pour réorganiser.",
|
"priorityInfo": "L'ordre des règles définit leur priorité d'exécution (de haut en bas). Glissez-déposez pour réorganiser.",
|
||||||
"ifThenTemplate": "Si un jour férié tombe un <strong>{day}</strong>, placer <strong>{placeDays}</strong> jour(s) <strong>{placeType}</strong> <strong>{placement}</strong> et obtenir <strong>{rewardDays}</strong> jour(s) <strong>{rewardType}</strong> en bonus.",
|
"ifThenTemplate": "Si un jour férié tombe un <strong>{day}</strong>, placer <strong>{placeDays}</strong> jour(s) <strong>{placeType}</strong> <strong>{placement}</strong> et obtenir <strong>{rewardDays}</strong> jour(s) <strong>{rewardType}</strong> en bonus.",
|
||||||
"timeWindowTemplate": "Placer <strong>{placeDays}</strong> jour(s) <strong>{placeType}</strong> entre le <strong>{startDate}</strong> et le <strong>{endDate}</strong>.",
|
"timeWindowTemplate": "Placer <strong>{placeDays}</strong> jour(s) <strong>{placeType}</strong> entre le <strong>{startDate}</strong> et le <strong>{endDate}</strong>.",
|
||||||
@@ -88,6 +95,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"planning": {
|
"planning": {
|
||||||
|
"title": "Votre année optimisée",
|
||||||
|
"daysPlaced": "Jours de congé placés",
|
||||||
|
"daysGained": "Jours non travaillés gagnés",
|
||||||
|
"longestWorkSequence": "Plus longue période travaillée",
|
||||||
|
"legend": "Légende du calendrier",
|
||||||
"invalidInput": "Les données de planification sont invalides.",
|
"invalidInput": "Les données de planification sont invalides.",
|
||||||
"conflict": "Les obligations ne peuvent pas être satisfaites ensemble.",
|
"conflict": "Les obligations ne peuvent pas être satisfaites ensemble.",
|
||||||
"incomplete": "Le calcul s’est arrêté avant la fin des critères de qualité.",
|
"incomplete": "Le calcul s’est arrêté avant la fin des critères de qualité.",
|
||||||
|
|||||||
+103
-62
@@ -5,6 +5,8 @@
|
|||||||
// custom components at https://material.angular.dev/guide/theming
|
// custom components at https://material.angular.dev/guide/theming
|
||||||
@use '@angular/material' as mat;
|
@use '@angular/material' as mat;
|
||||||
|
|
||||||
|
@layer reset, base, layout, utilities, blocks, exceptions;
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@include mat.theme(
|
@include mat.theme(
|
||||||
(
|
(
|
||||||
@@ -18,77 +20,116 @@ html {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
@layer reset {
|
||||||
--number-bg-hover: #e0e0e0;
|
*,
|
||||||
--number-color: black;
|
*::before,
|
||||||
--number-color-hover: black;
|
*::after {
|
||||||
--past-color: #b0b0b0;
|
box-sizing: border-box;
|
||||||
--week-number-bg: #e0e0e0;
|
}
|
||||||
--week-number-bg-hover: #c0c0c0;
|
|
||||||
--week-number-color: black;
|
|
||||||
|
|
||||||
--today-border-color: #1976d2;
|
|
||||||
--other-month-color: #9e9e9e;
|
|
||||||
--weekend-bg: #ffca39;
|
|
||||||
--closed-day-bg: #0ee3ff;
|
|
||||||
--rtt-bg: #99ff87;
|
|
||||||
--cp-bg: #eca0ff;
|
|
||||||
--other-bg: #ff9b9b;
|
|
||||||
--loading-overlay-bg: rgba(255, 255, 255, 0.9);
|
|
||||||
--loading-text-color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode {
|
@layer base {
|
||||||
--number-bg-hover: #424242;
|
:root {
|
||||||
--number-color: white;
|
--ui-panel-bg: var(--mat-sys-surface-container-low, #f7f7fa);
|
||||||
--number-color-hover: white;
|
--ui-card-bg: var(--mat-sys-surface-container, #f1f1f5);
|
||||||
--past-color: #757575;
|
--ui-border: var(--mat-sys-outline-variant, #c7c7d0);
|
||||||
--week-number-bg: #424242;
|
--ui-text-secondary: var(--mat-sys-on-surface-variant, #45464f);
|
||||||
--week-number-bg-hover: #616161;
|
--ui-radius-sm: 0.5rem;
|
||||||
--week-number-color: white;
|
--ui-radius-md: 0.75rem;
|
||||||
|
--ui-radius-lg: 1rem;
|
||||||
|
|
||||||
--today-border-color: #0b385c;
|
--ui-success-bg: oklch(92% 0.05 145);
|
||||||
--other-month-color: #757575;
|
--ui-success-text: oklch(31% 0.1 145);
|
||||||
--weekend-bg: #b17927;
|
--ui-warning-bg: oklch(93% 0.06 82);
|
||||||
--closed-day-bg: #098a9b;
|
--ui-warning-text: oklch(31% 0.08 75);
|
||||||
--rtt-bg: #468149;
|
--ui-error-bg: oklch(93% 0.05 25);
|
||||||
--cp-bg: #703e79;
|
--ui-error-text: oklch(32% 0.12 25);
|
||||||
--other-bg: #9b3939;
|
|
||||||
--loading-overlay-bg: rgba(0, 0, 0, 0.7);
|
|
||||||
--loading-text-color: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
--number-bg-hover: #e0e0e0;
|
||||||
// Default the application to a light color theme. This can be changed to
|
--number-color: black;
|
||||||
// `dark` to enable the dark color theme, or to `light dark` to defer to the
|
--number-color-hover: black;
|
||||||
// user's system settings.
|
--past-color: #b0b0b0;
|
||||||
&.dark-mode {
|
--week-number-bg: #e0e0e0;
|
||||||
color-scheme: dark;
|
--week-number-bg-hover: #c0c0c0;
|
||||||
|
--week-number-color: black;
|
||||||
|
--today-border-color: #1976d2;
|
||||||
|
--other-month-color: #9e9e9e;
|
||||||
|
--weekend-bg: #ffca39;
|
||||||
|
--closed-day-bg: #0ee3ff;
|
||||||
|
--rtt-bg: #99ff87;
|
||||||
|
--cp-bg: #eca0ff;
|
||||||
|
--other-bg: #ff9b9b;
|
||||||
|
--loading-overlay-bg: rgba(255, 255, 255, 0.92);
|
||||||
|
--loading-text-color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.light-mode {
|
.dark-mode {
|
||||||
color-scheme: light;
|
--ui-success-bg: oklch(30% 0.06 145);
|
||||||
|
--ui-success-text: oklch(90% 0.07 145);
|
||||||
|
--ui-warning-bg: oklch(31% 0.06 82);
|
||||||
|
--ui-warning-text: oklch(91% 0.07 82);
|
||||||
|
--ui-error-bg: oklch(30% 0.07 25);
|
||||||
|
--ui-error-text: oklch(90% 0.07 25);
|
||||||
|
|
||||||
|
--number-bg-hover: #424242;
|
||||||
|
--number-color: white;
|
||||||
|
--number-color-hover: white;
|
||||||
|
--past-color: #757575;
|
||||||
|
--week-number-bg: #424242;
|
||||||
|
--week-number-bg-hover: #616161;
|
||||||
|
--week-number-color: white;
|
||||||
|
--today-border-color: #0b385c;
|
||||||
|
--other-month-color: #757575;
|
||||||
|
--weekend-bg: #b17927;
|
||||||
|
--closed-day-bg: #098a9b;
|
||||||
|
--rtt-bg: #468149;
|
||||||
|
--cp-bg: #703e79;
|
||||||
|
--other-bg: #9b3939;
|
||||||
|
--loading-overlay-bg: rgba(18, 18, 20, 0.92);
|
||||||
|
--loading-text-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a default background, font and text colors for the application using
|
html,
|
||||||
// Angular Material's system-level CSS variables. Learn more about these
|
body {
|
||||||
// variables at https://material.angular.dev/guide/system-variables
|
min-height: 100%;
|
||||||
background-color: var(--mat-sys-surface);
|
}
|
||||||
color: var(--mat-sys-on-surface);
|
|
||||||
font: var(--mat-sys-body-medium);
|
|
||||||
|
|
||||||
// Reset the user agent margin.
|
html {
|
||||||
margin: 0;
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--mat-sys-surface, #fff);
|
||||||
|
color: var(--mat-sys-on-surface, #1c1b1f);
|
||||||
|
font: var(--mat-sys-body-medium, 400 1rem/1.5 Roboto, sans-serif);
|
||||||
|
font-family: Roboto, 'Helvetica Neue', sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
&.dark-mode {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.light-mode {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(button, a, input, select, textarea, [tabindex]):focus-visible {
|
||||||
|
outline: 0.1875rem solid var(--mat-sys-primary, #005cbb);
|
||||||
|
outline-offset: 0.125rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* You can add global styles to this file, and also import other style files */
|
@layer exceptions {
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
html,
|
*,
|
||||||
body {
|
*::before,
|
||||||
height: 100%;
|
*::after {
|
||||||
}
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
body {
|
scroll-behavior: auto !important;
|
||||||
margin: 0;
|
transition-duration: 0.01ms !important;
|
||||||
font-family: Roboto, 'Helvetica Neue', sans-serif;
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user