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:
nessar committed 2026-08-30 19:16:36 +02:00
1 parent 31314f3969
commit 609b3dece9
30 files changed
+798 -310

No files matched your search

+16 -8
View File
@@ -1,13 +1,21 @@
<app-dark-light-mode-button class="modeButton"></app-dark-light-mode-button>
@if (showPlanner()) {
<main class="main">
<h1 style="display: flex; gap: 1rem"><mat-icon>beach_access</mat-icon> {{ title() }}</h1>
<div class="content">
<app-user-input></app-user-input>
<mat-divider [vertical]="true" />
<mat-divider />
<app-calendar-year></app-calendar-year>
<div class="app-shell">
<header class="app-header">
<div class="brand">
<mat-icon aria-hidden="true">beach_access</mat-icon>
<h1>{{ title() }}</h1>
</div>
<app-dark-light-mode-button></app-dark-light-mode-button>
</header>
<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>
+7
View File
@@ -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
View File
@@ -1,6 +1,7 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', pathMatch: 'full', children: [] },
{
path: 'benchmark',
loadComponent: () =>
+55 -63
View File
@@ -1,79 +1,71 @@
.main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
:host {
display: block;
}
.content {
.app-shell {
min-height: 100dvh;
}
.app-header {
display: flex;
flex-direction: row;
align-items: flex-start;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 24px;
gap: 1rem;
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;
}
app-user-input {
display: block;
flex: 3;
min-width: 350px;
margin-right: 16px;
}
app-calendar-year {
display: block;
flex: 7;
min-width: 700px;
margin-left: 16px;
}
@media (max-width: 1024px) {
.content {
flex-direction: column;
align-items: stretch;
padding: 0 16px;
}
.configuration-panel,
.results-panel,
app-user-input,
app-calendar-year {
flex: none;
width: 100%;
display: block;
min-width: 0;
margin: 0;
}
app-user-input {
margin-bottom: 24px;
.configuration-panel {
padding: 1.25rem;
background: var(--ui-panel-bg, #f7f7fa);
border: 1px solid var(--ui-border, #c7c7d0);
border-radius: var(--ui-radius-lg, 1rem);
}
.mat-divider-vertical {
display: none;
}
.mat-divider-horizontal {
display: block !important;
@media (max-width: 75rem) {
.planner-layout {
grid-template-columns: minmax(0, 1fr);
}
}
.modeButton {
position: fixed;
top: 20px;
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;
}
-2
View File
@@ -1,7 +1,6 @@
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { provideNativeDateAdapter } from '@angular/material/core';
import { MatDivider } from '@angular/material/divider';
import { UserInput } from './components/user-input/user-input';
import { CalendarYear } from './components/calendar-year/calendar-year';
import { DarkLightModeButton } from './components/dark-light-mode-button/dark-light-mode-button';
@@ -16,7 +15,6 @@ function isBenchmarkUrl(url: string): boolean {
selector: 'app-root',
imports: [
MatDatepickerModule,
MatDivider,
UserInput,
CalendarYear,
DarkLightModeButton,
@@ -1,4 +1,4 @@
<div class="calendar">
<div class="calendar" role="group" [attr.aria-label]="getMonthName() + ' ' + year">
<h3>{{ getMonthName() }} {{ year }}</h3>
<div>
<div class="row header">
@@ -25,6 +25,7 @@
@if (day === null) {
<p class="day empty"></p>
} @else {
@let dayTooltip = getDayTooltip(day);
<p
[class.past]="day < getNow()"
[class.start]="selectedDates.isStart(day)"
@@ -32,8 +33,11 @@
[class.selected]="selectedDates.isSelected(day)"
[class]="selectedDates.getClasses(day)"
class="day number"
[matTooltip]="getDayTooltip(day)"
[matTooltip]="dayTooltip"
[matTooltipShowDelay]="500"
[attr.aria-label]="
dayTooltip ? day.getDate() + ': ' + dayTooltip : null
"
>
{{ day.getDate() }}
</p>
@@ -69,8 +69,6 @@
.today {
border-color: var(--today-border-color);
border-width: 2px;
width: 26px !important;
height: 22px !important;
border-style: dashed !important;
:has(.dark-mode) {
@@ -88,8 +86,6 @@
.closed-day {
border: 2px solid var(--closed-day-bg);
width: 26px !important;
height: 22px !important;
}
.rtt {
@@ -1,4 +1,4 @@
<button matFab extended (click)="openDialog()">
<mat-icon>settings</mat-icon>
<button mat-stroked-button type="button" (click)="openDialog()">
<mat-icon aria-hidden="true">settings</mat-icon>
{{ tr().settings.title }}
</button>
@@ -0,0 +1,7 @@
:host {
display: inline-flex;
}
button {
min-height: 2.75rem;
}
@@ -32,15 +32,30 @@ const incomplete: PlanningIncompleteOutcome = {
const plan: PlanOutcome = {
kind: 'Plan',
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: [],
preferences: [],
remainingBalances: [],
metrics: {
unplacedAtRisk: 0,
holidayWeekValue: 0,
nonWorkingDayGain: 0,
longestWorkSequence: 0,
nonWorkingDayGain: 4,
longestWorkSequence: 6,
shortSequenceDeficit: 0,
supportingPlacements: 0,
preferenceSatisfaction: [],
@@ -84,7 +99,7 @@ describe('CalendarYear planning outcome boundary', () => {
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 = {
getSelectedDatesForYear: vi.fn(() => new SelectedDates()),
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);
fixture.detectChanges();
await fixture.whenStable();
await new Promise<void>((resolve) => setTimeout(resolve, 0));
fixture.detectChanges();
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 () => {
@@ -3,26 +3,99 @@
<div class="loading-overlay">
<div class="loading-spinner">
<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>
<p>{{ tr().loading }}</p>
</div>
</div>
}
<header class="results-header">
<div class="results-heading">
<h2 id="planning-results-title">{{ tr().planning.title }}</h2>
@if (planningOutcome()?.kind === 'InvalidInput') {
<p class="planning-status" role="alert">{{ tr().planning.invalidInput }}</p>
<p class="planning-status error" role="alert">
<mat-icon aria-hidden="true">error</mat-icon>
{{ tr().planning.invalidInput }}
</p>
} @else if (planningOutcome()?.kind === 'PlanningConflict') {
<p class="planning-status" role="alert">{{ tr().planning.conflict }}</p>
<p class="planning-status error" role="alert">
<mat-icon aria-hidden="true">error</mat-icon>
{{ tr().planning.conflict }}
</p>
} @else if (planningOutcome()?.kind === 'PlanningIncomplete') {
<p class="planning-status" role="status">
<p
class="planning-status"
[class.error]="hasPlannerError()"
[class.warning]="!hasPlannerError()"
[attr.role]="hasPlannerError() ? 'alert' : 'status'"
>
<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" role="status">{{ tr().planning.complete }}</p>
<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="card">
<mat-card class="month-card">
<mat-card-content>
<app-calendar-month
[monthIndex]="month"
@@ -33,3 +106,4 @@
</mat-card>
}
</div>
</div>
@@ -1,40 +1,41 @@
@use '@angular/material' as mat;
:host {
display: block;
min-width: 0;
}
.calendar-year {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
display: grid;
container: calendar-year / inline-size;
gap: 1.25rem;
width: 100%;
max-width: 100%;
min-width: 0;
position: relative;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.9);
inset: 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
border-radius: 4px;
z-index: 1000;
background-color: var(--loading-overlay-bg);
min-height: 18rem;
border-radius: var(--ui-radius-lg, 1rem);
z-index: 1;
background: var(--loading-overlay-bg, rgba(255, 255, 255, 0.92));
}
.loading-spinner {
display: flex;
flex-wrap: wrap;
flex-direction: column;
align-items: center;
gap: 16px;
gap: 1rem;
p {
font-size: 16px;
font-size: 1rem;
font-weight: 500;
color: var(--loading-text-color);
color: var(--loading-text-color, #333);
margin: 0;
}
}
@@ -43,24 +44,183 @@
position: relative;
display: inline-block;
::ng-deep .mat-mdc-progress-spinner {
// Override Material spinner size
width: 100px;
height: 100px;
}
.spinner-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 20px;
font-size: 1.125rem;
font-weight: bold;
color: var(--loading-text-color);
color: var(--loading-text-color, #333);
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;
}
@@ -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 { MatCard, MatCardContent } from '@angular/material/card';
import { CalendarMonth } from '../calendar-month/calendar-month';
@@ -9,10 +9,19 @@ import { RuleService } from '../../services/rule-service';
import { TranslationService } from '../../services/translation-service';
import { PlanOutcome, PlanningOutcome } from '../../services/leave-planner.service';
import { SelectableDayType, VacDay } from '../../services/calendar-service';
import { MatIcon } from '@angular/material/icon';
import { CalendarSettings } from '../calendar-settings/calendar-settings';
@Component({
selector: 'app-calendar-year',
imports: [MatCard, CalendarMonth, MatCardContent, MatProgressSpinner],
imports: [
MatCard,
CalendarMonth,
MatCardContent,
MatProgressSpinner,
MatIcon,
CalendarSettings,
],
templateUrl: './calendar-year.html',
styleUrl: './calendar-year.scss',
})
@@ -31,6 +40,14 @@ export class CalendarYear {
protected loadingProgress = signal(0);
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;
protected hasPlannerError(): boolean {
@@ -127,12 +144,7 @@ export class CalendarYear {
private applyOutcome(outcome: PlanningOutcome): void {
this.planningOutcome.set(outcome);
const plan =
outcome.kind === 'Plan'
? outcome
: outcome.kind === 'PlanningIncomplete'
? outcome.incumbent
: undefined;
const plan = this.displayedPlan();
if (plan) {
this.selectedDates = this.calendarService.selectedDatesForPlan(plan);
this.userInput.remainingVacationDaysSignal.set(this.toLegacyBalances(plan));
@@ -2,13 +2,18 @@ button {
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
transition: all 0.3s ease;
width: 2.75rem;
height: 2.75rem;
min-width: 2.75rem;
min-height: 2.75rem;
transition:
background-color 200ms ease,
color 200ms ease;
&:hover {
background-color: rgba(0, 0, 0, 0);
@@ -1,8 +1,9 @@
<mat-expansion-panel class="rules-panel-container mat-elevation-z2">
<mat-expansion-panel-header>
<mat-panel-title>
<mat-icon>rule</mat-icon>
{{ tr().rules.title }}
<mat-icon aria-hidden="true">rule</mat-icon>
<span>{{ tr().rules.title }}</span>
<span class="rule-count">{{ activeRuleCount }} {{ tr().rules.active }}</span>
</mat-panel-title>
<mat-panel-description> {{ tr().rules.subtitle }} </mat-panel-description>
</mat-expansion-panel-header>
@@ -37,6 +38,22 @@
>
</mat-slide-toggle>
<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
mat-icon-button
[aria-label]="tr().rules.editRule"
+50 -26
View File
@@ -1,20 +1,30 @@
.rules-panel-container {
margin-top: 1rem;
margin-bottom: 1rem;
border-radius: 12px;
margin: 0;
border-radius: var(--ui-radius-md, 0.75rem);
overflow: hidden;
container-type: inline-size;
mat-panel-title {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
gap: 0.5rem;
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 {
display: none !important;
display: none;
}
}
}
@@ -27,10 +37,12 @@
}
.rule-item {
border: 1px solid var(--mat-sys-outline-variant);
border-radius: 8px;
transition: all 0.2s ease;
background: var(--mat-sys-surface-container);
border: 1px solid var(--mat-sys-outline-variant, #c7c7d0);
border-radius: var(--ui-radius-sm, 0.5rem);
transition:
border-color 200ms ease,
background-color 200ms ease;
background: var(--mat-sys-surface-container, #f1f1f5);
&:not(.editing) {
cursor: grab;
@@ -41,11 +53,11 @@
}
&:hover:not(.editing) {
border-color: var(--mat-sys-outline);
border-color: var(--mat-sys-outline, #777680);
}
&.editing {
border: 1px solid var(--mat-sys-primary);
border: 1px solid var(--mat-sys-primary, #005cbb);
}
}
@@ -76,29 +88,31 @@
.priority-statement {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--mat-sys-primary-container);
color: var(--mat-sys-on-primary-container);
background: var(--mat-sys-primary-container, #d7e3ff);
color: var(--mat-sys-on-primary-container, #001b3f);
border-radius: 8px;
font-size: 0.95rem;
margin-top: 8px;
margin-bottom: 8px;
mat-icon {
color: var(--mat-sys-primary);
color: var(--mat-sys-primary, #005cbb);
flex-shrink: 0;
}
}
.rule-drag-grip {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
padding-top: 8px;
margin-bottom: -8px;
color: var(--mat-sys-outline);
color: var(--mat-sys-outline, #777680);
opacity: 0.6;
cursor: grab;
transition: opacity 0.2s;
@@ -114,6 +128,7 @@
.rule-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
@@ -122,24 +137,21 @@
font-weight: 600;
font-size: 1.05rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
.rule-number {
color: var(--mat-sys-primary);
color: var(--mat-sys-primary, #005cbb);
font-weight: 700;
}
}
.rule-actions {
display: flex;
gap: 4px;
opacity: 0.6;
transition: opacity 0.2s;
.rule-item:hover & {
opacity: 1;
}
flex-wrap: wrap;
gap: 0;
margin-inline-start: auto;
}
}
@@ -158,8 +170,8 @@
display: flex;
flex-direction: column;
gap: 16px;
background: var(--mat-sys-surface-container-high);
border-radius: 8px;
background: var(--mat-sys-surface-container-high, #e9e9ee);
border-radius: var(--ui-radius-sm, 0.5rem);
h4 {
margin: 0;
@@ -195,6 +207,7 @@
.editor-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 12px;
margin-top: 8px;
@@ -203,6 +216,7 @@
.add-buttons {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 8px;
margin-top: 8px;
@@ -216,3 +230,13 @@
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);
});
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;
}
protected get activeRuleCount(): number {
return this.rules.filter((rule) => rule.enabled).length;
}
public drop(event: CdkDragDrop<VacationRule[]>) {
if (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) {
this.ruleService.toggleRule(id);
}
+22 -14
View File
@@ -1,5 +1,9 @@
<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">
@for (day of daysOff; track day.id) {
<div class="day-input-wrapper" animate.enter="day-enter" animate.leave="day-leave">
@@ -10,25 +14,27 @@
></app-day-input>
</div>
}
<mat-card class="add-day-card mat-elevation-z2" (click)="addDay()">
<button matFab [aria-label]="tr().userInput.addVacationDay">
<mat-icon>add</mat-icon>
<button mat-stroked-button class="add-day-button" type="button" (click)="addDay()">
<mat-icon aria-hidden="true">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>
<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>
<app-calendar-settings></app-calendar-settings>
</div>
<app-rules-panel></app-rules-panel>
@if (hasRemainingDays()) {
<mat-card class="mat-elevation-z2 warning-card">
<mat-card class="warning-card">
<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">
<span>
{{
@@ -49,4 +55,6 @@
</mat-card-content>
</mat-card>
}
<app-rules-panel></app-rules-panel>
</div>
+52 -37
View File
@@ -1,34 +1,38 @@
.user-input {
display: flex;
flex-direction: column;
margin: 1rem;
label {
font-weight: bold;
margin-bottom: 0.5rem;
gap: 1.25rem;
min-width: 0;
}
.error-message {
color: #ff0000;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.warning-card {
margin-bottom: 1rem;
}
.warning-content {
.section-heading {
display: flex;
align-items: center;
gap: 0.5rem;
flex-direction: column;
gap: 0.375rem;
h2,
p {
margin: 0;
}
h2 {
font-size: 1.5rem;
font-weight: 700;
line-height: 1.2;
text-wrap: balance;
}
p {
max-width: 55ch;
color: var(--ui-text-secondary, #45464f);
line-height: 1.5;
}
}
.listOfDays {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 0.75rem;
}
.day-input-wrapper {
@@ -68,36 +72,47 @@ form {
flex-direction: column;
}
.add-day-card {
display: flex;
flex-direction: row;
align-items: center;
padding: 16px;
gap: 1rem;
margin-bottom: 1rem;
cursor: pointer;
.add-day-button,
.calculate-button {
width: 100%;
min-height: 3rem;
}
.add-day-card h3 {
flex-grow: 1;
margin: 0;
font-weight: normal;
.add-day-button {
display: flex;
justify-content: flex-start;
}
.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;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
gap: 1rem;
align-items: flex-start;
gap: 0.75rem;
mat-icon {
flex-shrink: 0;
}
}
.warning {
display: flex;
flex-direction: column;
gap: 0.25rem;
min-width: 0;
overflow-wrap: break-word;
}
:host {
display: block;
container: user-input / inline-size;
max-width: 100%;
}
@@ -1,7 +1,6 @@
import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core';
import { MatInputModule } from '@angular/material/input';
import { UserInputService } from '../../services/user-input-service';
import { CalendarSettings } from '../calendar-settings/calendar-settings';
import { RulesPanel } from '../rules-panel/rules-panel';
import { MatCard, MatCardContent } from '@angular/material/card';
import { MatIcon } from '@angular/material/icon';
@@ -14,7 +13,6 @@ import { TranslationService } from '../../services/translation-service';
selector: 'app-user-input',
imports: [
MatInputModule,
CalendarSettings,
RulesPanel,
MatCard,
MatCardContent,
@@ -1,15 +1,18 @@
<!-- <mat-list-item role="listitem"> -->
<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
matInput
type="number"
min="0"
step="1"
inputmode="numeric"
[value]="day().numberOfDays"
(input)="onInput($event)"
/>
</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-option></mat-option>
@for (dayType of selectableDayTypes; track $index) {
@@ -19,7 +22,17 @@
}
</mat-select>
</mat-form-field>
<mat-form-field class="dateInput">
<button
mat-icon-button
class="delete-button"
type="button"
[aria-label]="tr().userInput.deleteEntry"
(click)="deleteEntry.emit()"
>
<mat-icon aria-hidden="true">close</mat-icon>
</button>
<div class="date-fields">
<mat-form-field class="dateInput" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ tr().userInput.availableDate }}</mat-label>
<input
matInput
@@ -30,11 +43,10 @@
<mat-datepicker-toggle matIconSuffix [for]="availablePicker"></mat-datepicker-toggle>
<mat-datepicker #availablePicker></mat-datepicker>
</mat-form-field>
<mat-form-field class="dateInput">
<mat-form-field class="dateInput" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ tr().userInput.chooseDate }}</mat-label>
<input
matInput
type="datetime"
[matDatepicker]="picker"
[value]="day().expiryDate"
(dateChange)="addEvent($event)"
@@ -42,8 +54,5 @@
<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>
</div>
</mat-card>
<!-- </mat-list-item> -->
@@ -1,72 +1,76 @@
:host {
display: block;
container-type: inline-size;
container: day-input / inline-size;
width: 100%;
}
.card {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 16px;
gap: 16px;
display: grid;
grid-template-columns: 5.25rem minmax(0, 1fr) 2.75rem;
align-items: start;
gap: 0.75rem;
min-width: 0;
padding: 1rem;
background: var(--ui-card-bg, #f1f1f5);
border: 1px solid var(--ui-border, #c7c7d0);
box-shadow: none;
}
.dateInput {
flex-grow: 1;
.card mat-form-field {
width: 100%;
min-width: 0;
}
.numberInput {
width: 80px;
}
::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none;
}
@container (max-width: 480px) {
.card {
flex-wrap: wrap;
gap: 12px;
padding: 12px;
align-items: center;
}
.numberInput {
width: 70px;
flex-grow: 0;
grid-column: 1;
}
.dayTypeInput {
flex-grow: 1;
width: 0;
min-width: 120px;
grid-column: 2;
}
.dateInput {
width: 100%;
flex-grow: 1;
flex-basis: 100%;
margin-top: 4px;
.delete-button {
grid-column: 3;
grid-row: 1;
min-width: 2.75rem;
min-height: 2.75rem;
color: var(--ui-text-secondary, #45464f);
}
button[matFab] {
width: 40px;
height: 40px;
min-width: 40px;
line-height: 40px;
::ng-deep .mat-mdc-button-touch-target {
width: 40px;
height: 40px;
.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;
}
mat-icon {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
@container day-input (max-width: 24rem) {
.card {
grid-template-columns: 4.75rem minmax(0, 1fr) 2.75rem;
gap: 0.5rem;
padding: 0.75rem;
}
.date-fields {
grid-template-columns: minmax(0, 1fr);
gap: 0.5rem;
}
}
@container day-input (min-width: 52rem) {
.card {
grid-template-columns:
5.25rem minmax(9rem, 1fr) minmax(11rem, 1fr) minmax(11rem, 1fr)
2.75rem;
}
.date-fields {
display: contents;
}
.delete-button {
grid-column: 5;
}
}
@@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DayInput } from './day-input';
import { SelectableDayType, VacDay } from '../../../services/calendar-service';
import { TranslationService } from '../../../services/translation-service';
describe('DayInput', () => {
let component: DayInput;
@@ -18,6 +19,7 @@ describe('DayInput', () => {
await TestBed.configureTestingModule({
imports: [DayInput],
}).compileComponents();
TestBed.inject(TranslationService).setLanguage('en');
fixture = TestBed.createComponent(DayInput);
component = fixture.componentInstance;
@@ -29,6 +31,14 @@ describe('DayInput', () => {
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', () => {
const emitted: VacDay[] = [];
component.dayChange.subscribe((value) => emitted.push(value));
+12
View File
@@ -9,6 +9,8 @@ export interface AppTranslations {
description: string;
};
userInput: {
title: string;
description: string;
addVacationDay: string;
calculate: string;
remainingDaysWarning: string;
@@ -16,6 +18,8 @@ export interface AppTranslations {
deleteEntry: string;
chooseDate: string;
availableDate: string;
days: string;
type: string;
daySuffix: string;
daysSuffix: string;
expires: string;
@@ -49,6 +53,9 @@ export interface AppTranslations {
rules: {
title: string;
subtitle: string;
active: string;
moveRuleUp: string;
moveRuleDown: string;
priorityInfo: string;
ifThenTemplate: string;
timeWindowTemplate: string;
@@ -92,6 +99,11 @@ export interface AppTranslations {
};
};
planning: {
title: string;
daysPlaced: string;
daysGained: string;
longestWorkSequence: string;
legend: string;
invalidInput: string;
conflict: string;
incomplete: string;
+16 -4
View File
@@ -5,13 +5,17 @@
"description": "Optimieren Sie Ihre Urlaubstage basierend auf Feiertagen und Wochenenden!"
},
"userInput": {
"addVacationDay": "Urlaubstag hinzufügen",
"calculate": "Berechnen",
"title": "Urlaub konfigurieren",
"description": "Fügen Sie jedes Urlaubskonto und seinen Gültigkeitszeitraum hinzu.",
"addVacationDay": "Urlaubskonto hinzufügen",
"calculate": "Plan berechnen",
"remainingDaysWarning": "{count} nicht platziert:",
"remainingDaysManualPlace": "Bitte manuell platzieren.",
"deleteEntry": "Eintrag löschen",
"chooseDate": "Datum auswählen",
"chooseDate": "Gültig bis",
"availableDate": "Verfügbar ab",
"days": "Tage",
"type": "Urlaubsart",
"daySuffix": "Tag",
"daysSuffix": "Tage",
"expires": "abl.",
@@ -43,8 +47,11 @@
"selectLanguage": "Sprache auswählen"
},
"rules": {
"title": "Regeln",
"title": "Planungsregeln",
"subtitle": "Urlaubsregeln konfigurieren",
"active": "aktiv",
"moveRuleUp": "Regel nach oben verschieben",
"moveRuleDown": "Regel nach unten verschieben",
"priorityInfo": "Regeln werden von oben nach unten angewendet",
"ifThenTemplate": "Wenn {type} dann {placement}",
"timeWindowTemplate": "Zeitfenster {startDate} bis {endDate}",
@@ -88,6 +95,11 @@
}
},
"planning": {
"title": "Ihr optimiertes Jahr",
"daysPlaced": "Eingeplante Urlaubstage",
"daysGained": "Gewonnene arbeitsfreie Tage",
"longestWorkSequence": "Längste Arbeitsphase",
"legend": "Kalenderlegende",
"invalidInput": "Die Planungsdaten sind ungültig.",
"conflict": "Die Verpflichtungen können nicht gemeinsam erfüllt werden.",
"incomplete": "Die Berechnung wurde vor Abschluss aller Qualitätsstufen beendet.",
+16 -4
View File
@@ -5,13 +5,17 @@
"description": "Optimize your vacation days based on holidays and weekends!"
},
"userInput": {
"addVacationDay": "Add a vacation day",
"calculate": "Calculate",
"title": "Configure your leave",
"description": "Add each leave balance and its validity period.",
"addVacationDay": "Add leave balance",
"calculate": "Calculate plan",
"remainingDaysWarning": "{count} vacation day{plural} not placed:",
"remainingDaysManualPlace": "Please place them manually.",
"deleteEntry": "Delete entry",
"chooseDate": "Choose a date",
"chooseDate": "Expires on",
"availableDate": "Available from",
"days": "Days",
"type": "Leave type",
"daySuffix": "day",
"daysSuffix": "days",
"expires": "exp.",
@@ -43,8 +47,11 @@
"selectLanguage": "Language"
},
"rules": {
"title": "Custom Rules",
"title": "Planning 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.",
"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>.",
@@ -88,6 +95,11 @@
}
},
"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.",
"conflict": "The obligations cannot be satisfied together.",
"incomplete": "Calculation stopped before all quality stages completed.",
+16 -4
View File
@@ -5,13 +5,17 @@
"description": "¡Optimice sus días de vacaciones según los días festivos y los fines de semana!"
},
"userInput": {
"addVacationDay": "Añadir un día de vacaciones",
"calculate": "Calcular",
"title": "Configura tus vacaciones",
"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}:",
"remainingDaysManualPlace": "Por favor, colóquelos manualmente.",
"deleteEntry": "Eliminar entrada",
"chooseDate": "Elija una fecha",
"chooseDate": "Caduca el",
"availableDate": "Disponible desde",
"days": "Días",
"type": "Tipo de permiso",
"daySuffix": "día",
"daysSuffix": "días",
"expires": "exp.",
@@ -43,8 +47,11 @@
"selectLanguage": "Idioma"
},
"rules": {
"title": "Reglas personalizadas",
"title": "Reglas de planificació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.",
"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>.",
@@ -88,6 +95,11 @@
}
},
"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.",
"conflict": "Las obligaciones no se pueden satisfacer juntas.",
"incomplete": "El cálculo se detuvo antes de completar todas las etapas de calidad.",
+16 -4
View File
@@ -5,13 +5,17 @@
"description": "Optimisez vos jours de vacances en fonction des jours fériés et des week-ends !"
},
"userInput": {
"addVacationDay": "Ajouter un jour de vacances",
"calculate": "Calculer",
"title": "Configurez vos congés",
"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}:",
"remainingDaysManualPlace": "Veuillez les placer manuellement.",
"deleteEntry": "Supprimer l’entrée",
"chooseDate": "Choisissez une date",
"chooseDate": "Expire le",
"availableDate": "Disponible à partir du",
"days": "Jours",
"type": "Type de congé",
"daySuffix": "jour",
"daysSuffix": "jours",
"expires": "exp.",
@@ -43,8 +47,11 @@
"selectLanguage": "Langue"
},
"rules": {
"title": "Règles Personnalisées",
"title": "Règles de planification",
"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.",
"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>.",
@@ -88,6 +95,11 @@
}
},
"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.",
"conflict": "Les obligations ne peuvent pas être satisfaites ensemble.",
"incomplete": "Le calcul s’est arrêté avant la fin des critères de qualité.",
+67 -26
View File
@@ -5,6 +5,8 @@
// custom components at https://material.angular.dev/guide/theming
@use '@angular/material' as mat;
@layer reset, base, layout, utilities, blocks, exceptions;
html {
@include mat.theme(
(
@@ -18,7 +20,31 @@ html {
);
}
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
}
}
@layer base {
:root {
--ui-panel-bg: var(--mat-sys-surface-container-low, #f7f7fa);
--ui-card-bg: var(--mat-sys-surface-container, #f1f1f5);
--ui-border: var(--mat-sys-outline-variant, #c7c7d0);
--ui-text-secondary: var(--mat-sys-on-surface-variant, #45464f);
--ui-radius-sm: 0.5rem;
--ui-radius-md: 0.75rem;
--ui-radius-lg: 1rem;
--ui-success-bg: oklch(92% 0.05 145);
--ui-success-text: oklch(31% 0.1 145);
--ui-warning-bg: oklch(93% 0.06 82);
--ui-warning-text: oklch(31% 0.08 75);
--ui-error-bg: oklch(93% 0.05 25);
--ui-error-text: oklch(32% 0.12 25);
--number-bg-hover: #e0e0e0;
--number-color: black;
--number-color-hover: black;
@@ -26,7 +52,6 @@ html {
--week-number-bg: #e0e0e0;
--week-number-bg-hover: #c0c0c0;
--week-number-color: black;
--today-border-color: #1976d2;
--other-month-color: #9e9e9e;
--weekend-bg: #ffca39;
@@ -34,11 +59,18 @@ html {
--rtt-bg: #99ff87;
--cp-bg: #eca0ff;
--other-bg: #ff9b9b;
--loading-overlay-bg: rgba(255, 255, 255, 0.9);
--loading-overlay-bg: rgba(255, 255, 255, 0.92);
--loading-text-color: #333;
}
.dark-mode {
--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;
@@ -46,7 +78,6 @@ html {
--week-number-bg: #424242;
--week-number-bg-hover: #616161;
--week-number-color: white;
--today-border-color: #0b385c;
--other-month-color: #757575;
--weekend-bg: #b17927;
@@ -54,14 +85,27 @@ html {
--rtt-bg: #468149;
--cp-bg: #703e79;
--other-bg: #9b3939;
--loading-overlay-bg: rgba(0, 0, 0, 0.7);
--loading-text-color: #ddd;
--loading-overlay-bg: rgba(18, 18, 20, 0.92);
--loading-text-color: #f0f0f0;
}
html,
body {
min-height: 100%;
}
html {
scrollbar-gutter: stable;
}
body {
// Default the application to a light color theme. This can be changed to
// `dark` to enable the dark color theme, or to `light dark` to defer to the
// user's system settings.
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;
}
@@ -69,26 +113,23 @@ body {
&.light-mode {
color-scheme: light;
}
// Set a default background, font and text colors for the application using
// Angular Material's system-level CSS variables. Learn more about these
// variables at https://material.angular.dev/guide/system-variables
background-color: var(--mat-sys-surface);
color: var(--mat-sys-on-surface);
font: var(--mat-sys-body-medium);
// Reset the user agent margin.
margin: 0;
}
/* You can add global styles to this file, and also import other style files */
html,
body {
height: 100%;
:where(button, a, input, select, textarea, [tabindex]):focus-visible {
outline: 0.1875rem solid var(--mat-sys-primary, #005cbb);
outline-offset: 0.125rem;
}
}
body {
margin: 0;
font-family: Roboto, 'Helvetica Neue', sans-serif;
@layer exceptions {
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}
}