diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index 19da613..e76390d 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -15,6 +15,7 @@
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"@ngrx/signals": "^19.2.1",
+ "@picocss/pico": "^2.1.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"uuid": "^11.1.0"
@@ -2816,6 +2817,12 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@picocss/pico": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@picocss/pico/-/pico-2.1.1.tgz",
+ "integrity": "sha512-kIDugA7Ps4U+2BHxiNHmvgPIQDWPDU4IeU6TNRdvXQM1uZX+FibqDQT2xUOnnO2yq/LUHcwnGlu1hvf4KfXnMg==",
+ "license": "MIT"
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
diff --git a/webapp/package.json b/webapp/package.json
index d1cd31d..4519742 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -17,6 +17,7 @@
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"@ngrx/signals": "^19.2.1",
+ "@picocss/pico": "^2.1.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"uuid": "^11.1.0"
diff --git a/webapp/src/app/components/card/card.html b/webapp/src/app/components/card/card.html
deleted file mode 100644
index 4d82bc6..0000000
--- a/webapp/src/app/components/card/card.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
diff --git a/webapp/src/app/components/card/card.scss b/webapp/src/app/components/card/card.scss
deleted file mode 100644
index 7649468..0000000
--- a/webapp/src/app/components/card/card.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-:host {
- border: 1px solid var(--color-outline);
- border-radius: 1.2rem;
- color: var(--color-on-surface);
- display: inline-grid;
- font-size: 1.8rem;
- grid-template-areas:
- 'head'
- 'body'
- 'foot';
- grid-template-rows: min-content auto min-content;
- padding: 1.6rem;
-}
-header {
- margin-top: 0.4rem;
-}
-footer {
- justify-content: flex-end;
- display: flex;
- flex-direction: row;
- margin-top: 1.2rem;
-}
diff --git a/webapp/src/app/components/card/card.spec.ts b/webapp/src/app/components/card/card.spec.ts
deleted file mode 100644
index 9ec58c2..0000000
--- a/webapp/src/app/components/card/card.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { Card } from './card';
-
-describe('Card', () => {
- let component: Card;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [Card],
- }).compileComponents();
-
- fixture = TestBed.createComponent(Card);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/webapp/src/app/components/card/card.ts b/webapp/src/app/components/card/card.ts
deleted file mode 100644
index 10f8b3f..0000000
--- a/webapp/src/app/components/card/card.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
- selector: 'app-card',
- templateUrl: './card.html',
- styleUrl: './card.scss',
-})
-export class Card {}
diff --git a/webapp/src/app/routes/login/login.html b/webapp/src/app/routes/login/login.html
index 6e0f73b..3df9be5 100644
--- a/webapp/src/app/routes/login/login.html
+++ b/webapp/src/app/routes/login/login.html
@@ -1,8 +1,16 @@
-
-
-
-
-
-
+Aisle17
+
+
+
diff --git a/webapp/src/app/routes/login/login.scss b/webapp/src/app/routes/login/login.scss
index b01a4ba..5f330e0 100644
--- a/webapp/src/app/routes/login/login.scss
+++ b/webapp/src/app/routes/login/login.scss
@@ -1,3 +1,10 @@
:host {
display: grid;
}
+
+form {
+ fieldset,
+ input[type='submit'] {
+ margin-bottom: 0;
+ }
+}
diff --git a/webapp/src/app/routes/login/login.ts b/webapp/src/app/routes/login/login.ts
index 9722c16..b5ac528 100644
--- a/webapp/src/app/routes/login/login.ts
+++ b/webapp/src/app/routes/login/login.ts
@@ -1,16 +1,47 @@
-import { Component, inject } from '@angular/core';
+import { Component, inject, Signal } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
-import { Card } from 'components/card/card';
+import { debounceTime, map } from 'rxjs';
@Component({
selector: 'app-login',
templateUrl: './login.html',
styleUrl: './login.scss',
- imports: [Card, FormsModule, ReactiveFormsModule],
+ imports: [FormsModule, ReactiveFormsModule],
})
export class Login {
readonly form = inject(FormBuilder).group({
- username: ['', Validators.required],
- password: ['', Validators.required],
+ email: ['', Validators.compose([Validators.required, Validators.email])],
+ password: ['', Validators.compose([Validators.required, Validators.minLength(8)])],
});
+
+ private get email() {
+ return this.form.get('email')!;
+ }
+
+ private get password() {
+ return this.form.get('password')!;
+ }
+
+ readonly isEmailInvalid: Signal = toSignal(
+ this.email.statusChanges.pipe(
+ debounceTime(500),
+ map(status => this.email.dirty && status !== 'VALID')
+ ),
+ { initialValue: false }
+ );
+
+ readonly isPasswordInvalid: Signal = toSignal(
+ this.password.statusChanges.pipe(
+ debounceTime(500),
+ map(status => this.email.dirty && status !== 'VALID')
+ ),
+ { initialValue: false }
+ );
+
+ readonly isFormValid: Signal = toSignal(this.form.statusChanges.pipe(map(status => status === 'VALID')), {
+ initialValue: this.form.dirty && this.form.status === 'VALID',
+ });
+
+ onSubmit() {}
}
diff --git a/webapp/src/app/routes/plan/components/meal/meal.html b/webapp/src/app/routes/plan/components/meal/meal.html
index d1f2200..ae9480b 100644
--- a/webapp/src/app/routes/plan/components/meal/meal.html
+++ b/webapp/src/app/routes/plan/components/meal/meal.html
@@ -1,13 +1,5 @@
-
-
+@if (foods!!.length > 0) {
- @if (foods!!.length > 0) {
-
- } @else {
- No foods planned. Click the button below to add some!
- }
-
-
-
+} @else {
+ No foods planned. Click the button below to add some!
+}
diff --git a/webapp/src/app/routes/plan/components/meal/meal.ts b/webapp/src/app/routes/plan/components/meal/meal.ts
index 19d36ed..b799bbd 100644
--- a/webapp/src/app/routes/plan/components/meal/meal.ts
+++ b/webapp/src/app/routes/plan/components/meal/meal.ts
@@ -1,10 +1,8 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { IFood, MealType } from 'models/meal.model';
-import { Card } from 'components/card/card';
@Component({
selector: 'app-meal',
- imports: [Card],
templateUrl: './meal.html',
styleUrl: './meal.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
diff --git a/webapp/src/index.html b/webapp/src/index.html
index 0d5097b..b00fee1 100644
--- a/webapp/src/index.html
+++ b/webapp/src/index.html
@@ -1,5 +1,5 @@
-
+
Webapp
diff --git a/webapp/src/styles.scss b/webapp/src/styles.scss
index 0991b86..519d5fc 100644
--- a/webapp/src/styles.scss
+++ b/webapp/src/styles.scss
@@ -1,45 +1,25 @@
-@import url('./styles/reset.scss');
-@import url('./styles/typography.scss');
-@import url('./styles/components/button.scss');
+@use '@picocss/pico/scss' with (
+ $enable-semantic-container: true,
+ $enable-classes: false,
+ $modules: (
+ // Theme
+ 'themes/default': false,
-:root {
- --color-primary: oklch(0.327 0.057852 184.2083);
- --color-on-primary: white;
- --color-primary-container: oklch(0.5247 0.0849 184.3);
- --color-primary-container-alt: oklch(from var(--color-primary-container) 0.565 c h);
- --color-on-primary-container: white;
+ // Layout
+ 'layout/grid': false,
- --color-secondary: oklch(0.3394 0.0927 273.22);
- --color-on-secondary: white;
- --color-secondary-container: oklch(0.5379 0.0899 276.11);
- --color-on-secondary-container: white;
+ // Content
+ 'content/code': false,
- --color-tertiary: oklch(0.3384 0.0788 60.74);
- --color-on-tertiary: white;
- --color-tertiary-container: oklch(0.5416 0.1017 61.52);
- --color-on-tertiary-container: white;
+ // Forms
+ 'forms/input-color': false,
+ 'forms/input-file': false,
+ )
+);
- --color-error: oklch(0.3413 0.087 34.27);
- --color-on-error: white;
- --color-error-container: oklch(0.5441 0.0989 34.26);
- --color-on-error-container: white;
-
- --color-surface: oklch(0.9816 0.0055 211.04);
- --color-on-surface: oklch(0.1764 0.0095 208.78);
- --color-surface-container: oklch(0.9297 0.0066 208.78);
-
- --color-outline: oklch(0.4386 0.0119 212.57);
- --color-outline-variant: oklch(0.5336 0.0123 204.04);
-
- --color-shadow: oklch(0.3396 0.0743 281.7 / 25%);
- --color-shadow-variant: oklch(0 0 0 / 30%);
-
- --box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 3px, rgba(0, 0, 0, 0.24) 0px 1px 2px;
-
- font-family: system-ui, sans-serif;
- font-size: 62.5%;
- font-weight: 400;
-}
+// Custom Theme
+@use 'styles/theme/styles';
+@use 'styles/theme/schemes';
body {
margin: 0;
diff --git a/webapp/src/styles/components/button.scss b/webapp/src/styles/components/button.scss
deleted file mode 100644
index 9e2e8e4..0000000
--- a/webapp/src/styles/components/button.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-button {
- background-color: var(--color-surface-container);
- border: 1px solid var(--color-outline);
- border-radius: 0.4rem;
- color: var(--color-on-surface-container);
- cursor: pointer;
- font-size: 0.8rem;
- padding: 0.2rem 0.6rem;
-
- &:hover {
- box-shadow: var(--color-shadow) 0px 3px 8px;
- box-shadow: var(--color-shadow) 0px 2px 5px -1px, var(--color-shadow-variant) 0px 1px 3px -1px;
-
- &:active {
- background-color: var(--color-primary-container-alt);
- }
- }
-
- &.primary {
- background-color: var(--color-primary-container);
- color: var(--color-on-primary-container);
- }
-}
diff --git a/webapp/src/styles/reset.scss b/webapp/src/styles/reset.scss
deleted file mode 100644
index 0761796..0000000
--- a/webapp/src/styles/reset.scss
+++ /dev/null
@@ -1,54 +0,0 @@
-/* 1. Use a more-intuitive box-sizing model */
-*, *::before, *::after {
- box-sizing: border-box;
-}
-
-/* 2. Remove default margin */
-* {
- margin: 0;
-}
-
-/* 3. Enable keyword animations */
-@media (prefers-reduced-motion: no-preference) {
- html {
- interpolate-size: allow-keywords;
- }
-}
-
-body {
- /* 4. Add accessible line-height */
- line-height: 1.5;
- /* 5. Improve text rendering */
- -webkit-font-smoothing: antialiased;
-}
-
-/* 6. Improve media defaults */
-img, picture, video, canvas, svg {
- display: block;
- max-width: 100%;
-}
-
-/* 7. Inherit fonts for form controls */
-input, button, textarea, select {
- font: inherit;
-}
-
-/* 8. Avoid text overflows */
-p, h1, h2, h3, h4, h5, h6 {
- overflow-wrap: break-word;
-}
-
-/* 9. Improve line wrapping */
-p {
- text-wrap: pretty;
-}
-h1, h2, h3, h4, h5, h6 {
- text-wrap: balance;
-}
-
-/*
- 10. Create a root stacking context
-*/
-#root, #__next {
- isolation: isolate;
-}
diff --git a/webapp/src/styles/theme/_dark.scss b/webapp/src/styles/theme/_dark.scss
new file mode 100644
index 0000000..e72089d
--- /dev/null
+++ b/webapp/src/styles/theme/_dark.scss
@@ -0,0 +1,144 @@
+@use 'sass:color';
+@use 'sass:map';
+@use '@picocss/pico/scss/settings' as *;
+@use '@picocss/pico/scss/colors' as *;
+@use '@picocss/pico/scss/helpers/functions';
+
+$theme-color: #0074d9;
+
+// Default: Dark theme
+@mixin theme {
+ #{$css-var-prefix}background-color: #{color.mix($slate-950, $slate-900)};
+
+ // Text color
+ #{$css-var-prefix}color: #{$zinc-100};
+
+ // Text selection color
+ #{$css-var-prefix}text-selection-color: #{rgba($theme-color, 0.1875)};
+
+ // Muted colors
+ #{$css-var-prefix}muted-color: #{$zinc-450};
+ #{$css-var-prefix}muted-border-color: #{$slate-850};
+
+ // Primary colors
+ #{$css-var-prefix}primary: #{$theme-color};
+ #{$css-var-prefix}primary-background: var(#{$css-var-prefix}primary);
+ #{$css-var-prefix}primary-border: var(#{$css-var-prefix}primary-background);
+ #{$css-var-prefix}primary-underline: #{rgba($theme-color, 0.5)};
+ #{$css-var-prefix}primary-hover: #{color.scale($theme-color, $lightness: 21%)};
+ #{$css-var-prefix}primary-hover-background: var(#{$css-var-prefix}primary-hover);
+ #{$css-var-prefix}primary-hover-border: var(#{$css-var-prefix}primary-hover-background);
+ #{$css-var-prefix}primary-hover-underline: var(#{$css-var-prefix}primary-hover);
+ #{$css-var-prefix}primary-focus: #{rgba($theme-color, 0.375)};
+ #{$css-var-prefix}primary-inverse: #{$black};
+
+ // Secondary colors
+ #{$css-var-prefix}secondary: #{$zinc-350};
+ #{$css-var-prefix}secondary-background: #{$slate-600};
+ #{$css-var-prefix}secondary-border: var(#{$css-var-prefix}secondary-background);
+ #{$css-var-prefix}secondary-underline: #{rgba($zinc-350, 0.5)};
+ #{$css-var-prefix}secondary-hover: #{$zinc-250};
+ #{$css-var-prefix}secondary-hover-background: #{$slate-550};
+ #{$css-var-prefix}secondary-hover-border: var(#{$css-var-prefix}secondary-hover-background);
+ #{$css-var-prefix}secondary-hover-underline: var(#{$css-var-prefix}secondary-hover);
+ #{$css-var-prefix}secondary-focus: #{rgba($slate-350, 0.25)};
+ #{$css-var-prefix}secondary-inverse: #{$white};
+
+ // Contrast colors
+ #{$css-var-prefix}contrast: #{$slate-100};
+ #{$css-var-prefix}contrast-background: #{$slate-50};
+ #{$css-var-prefix}contrast-border: var(#{$css-var-prefix}contrast-background);
+ #{$css-var-prefix}contrast-underline: #{rgba($slate-100, 0.5)};
+ #{$css-var-prefix}contrast-hover: #{$white};
+ #{$css-var-prefix}contrast-hover-background: #{$white};
+ #{$css-var-prefix}contrast-hover-border: var(#{$css-var-prefix}contrast-hover-background);
+ #{$css-var-prefix}contrast-hover-underline: var(#{$css-var-prefix}contrast-hover);
+ #{$css-var-prefix}contrast-focus: #{rgba($slate-150, 0.25)};
+ #{$css-var-prefix}contrast-inverse: #{$black};
+
+ // Box shadow
+ #{$css-var-prefix}box-shadow: functions.shadow($black);
+
+ // Typography
+ @if map.get($modules, 'content/typography') {
+ // Headings colors
+ #{$css-var-prefix}h1-color: #{$zinc-50};
+ #{$css-var-prefix}h2-color: #{$zinc-100};
+ #{$css-var-prefix}h3-color: #{$zinc-200};
+ #{$css-var-prefix}h4-color: #{$zinc-250};
+ #{$css-var-prefix}h5-color: #{$zinc-300};
+ #{$css-var-prefix}h6-color: #{$zinc-400};
+
+ // Highlighted text ()
+ #{$css-var-prefix}mark-background-color: #{$azure-750};
+ #{$css-var-prefix}mark-color: #{$white};
+
+ // Inserted () & Deleted ()
+ #{$css-var-prefix}ins-color: #{color.mix($jade-450, $zinc-200)};
+ #{$css-var-prefix}del-color: #{color.mix($red-500, $zinc-200)};
+
+ // Blockquote
+ #{$css-var-prefix}blockquote-border-color: var(#{$css-var-prefix}muted-border-color);
+ #{$css-var-prefix}blockquote-footer-color: var(#{$css-var-prefix}muted-color);
+ }
+
+ // Button
+ @if map.get($modules, 'content/button') {
+ // To disable box-shadow, remove the var or set to '0 0 0 rgba(0, 0, 0, 0)'
+ // Don't use, 'none, 'false, 'null', '0', etc.
+ #{$css-var-prefix}button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
+ #{$css-var-prefix}button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
+ }
+
+ // Table
+ @if map.get($modules, 'content/table') {
+ #{$css-var-prefix}table-border-color: var(#{$css-var-prefix}muted-border-color);
+ #{$css-var-prefix}table-row-stripped-background-color: #{rgba($zinc-500, 0.0375)};
+ }
+
+ // Form elements
+ @if map.get($modules, 'forms/basics') {
+ #{$css-var-prefix}form-element-background-color: #{color.mix($slate-900, $slate-850)};
+ #{$css-var-prefix}form-element-selected-background-color: #{$slate-800};
+ #{$css-var-prefix}form-element-border-color: #{$slate-800};
+ #{$css-var-prefix}form-element-color: #{$zinc-100};
+ #{$css-var-prefix}form-element-placeholder-color: #{$zinc-400};
+ #{$css-var-prefix}form-element-active-background-color: #{color.mix($slate-900, $slate-850, 75%)};
+ #{$css-var-prefix}form-element-active-border-color: var(#{$css-var-prefix}primary-border);
+ #{$css-var-prefix}form-element-focus-color: var(#{$css-var-prefix}primary-focus);
+ #{$css-var-prefix}form-element-disabled-background-color: var(#{$css-var-prefix}form-element-background-color);
+ #{$css-var-prefix}form-element-disabled-border-color: var(#{$css-var-prefix}form-element-border-color);
+ #{$css-var-prefix}form-element-disabled-opacity: 0.5;
+ #{$css-var-prefix}form-element-invalid-border-color: #{color.mix($red-500, $slate-600)};
+ #{$css-var-prefix}form-element-invalid-active-border-color: #{color.mix($red-500, $slate-600, 75%)};
+ #{$css-var-prefix}form-element-invalid-focus-color: var(
+ #{$css-var-prefix}form-element-invalid-active-border-color
+ );
+ #{$css-var-prefix}form-element-valid-border-color: #{color.mix($jade-450, $slate-600)};
+ #{$css-var-prefix}form-element-valid-active-border-color: #{color.mix($jade-450, $slate-600, 75%)};
+ #{$css-var-prefix}form-element-valid-focus-color: var(#{$css-var-prefix}form-element-valid-active-border-color);
+
+ // Focus for buttons, radio and select
+ input:is([type='submit'], [type='button'], [type='reset'], [type='checkbox'], [type='radio'], [type='file']) {
+ #{$css-var-prefix}form-element-focus-color: var(#{$css-var-prefix}primary-focus);
+ }
+ }
+
+ // Range (input[type="range"])
+ @if map.get($modules, 'forms/input-range') {
+ #{$css-var-prefix}range-border-color: #{$slate-850};
+ #{$css-var-prefix}range-active-border-color: #{$slate-800};
+ #{$css-var-prefix}range-thumb-border-color: var(#{$css-var-prefix}background-color);
+ #{$css-var-prefix}range-thumb-color: var(#{$css-var-prefix}primary-background);
+ #{$css-var-prefix}range-thumb-active-color: var(#{$css-var-prefix}primary-hover-background);
+ }
+
+ // Form validation icons
+ @if map.get($modules, 'forms/basics') {
+ #{$css-var-prefix}icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{functions.display-rgb(color.mix($jade-450, $slate-600))}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
+ #{$css-var-prefix}icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{functions.display-rgb(color.mix($red-500, $slate-600))}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
+ }
+
+ // Document
+ color-scheme: dark;
+}
diff --git a/webapp/src/styles/theme/_light.scss b/webapp/src/styles/theme/_light.scss
new file mode 100644
index 0000000..6135305
--- /dev/null
+++ b/webapp/src/styles/theme/_light.scss
@@ -0,0 +1,144 @@
+@use 'sass:color';
+@use 'sass:map';
+@use '@picocss/pico/scss/settings' as *;
+@use '@picocss/pico/scss/colors' as *;
+@use '@picocss/pico/scss/helpers/functions';
+
+$theme-color: #001f3f;
+
+// Default: Light theme
+@mixin theme {
+ #{$css-var-prefix}background-color: #{$white};
+
+ // Text color
+ #{$css-var-prefix}color: #{$zinc-900};
+
+ // Text selection color
+ #{$css-var-prefix}text-selection-color: #{rgba($theme-color, 0.25)};
+
+ // Muted colors
+ #{$css-var-prefix}muted-color: #{$zinc-550};
+ #{$css-var-prefix}muted-border-color: #{color.mix($slate-100, $slate-50)};
+
+ // Primary colors
+ #{$css-var-prefix}primary: #{$theme-color};
+ #{$css-var-prefix}primary-background: var(#{$css-var-prefix}primary);
+ #{$css-var-prefix}primary-border: var(#{$css-var-prefix}primary-background);
+ #{$css-var-prefix}primary-underline: #{rgba($theme-color, 0.5)};
+ #{$css-var-prefix}primary-hover: #{color.scale($theme-color, $lightness: -21%)};
+ #{$css-var-prefix}primary-hover-background: #{color.scale($theme-color, $lightness: 21%)};
+ #{$css-var-prefix}primary-hover-border: var(#{$css-var-prefix}primary-hover-background);
+ #{$css-var-prefix}primary-hover-underline: var(#{$css-var-prefix}primary-hover);
+ #{$css-var-prefix}primary-focus: #{rgba($theme-color, 0.375)};
+ #{$css-var-prefix}primary-inverse: #{$black};
+
+ // Secondary colors
+ #{$css-var-prefix}secondary: #{$slate-550};
+ #{$css-var-prefix}secondary-background: #{$slate-600};
+ #{$css-var-prefix}secondary-border: var(#{$css-var-prefix}secondary-background);
+ #{$css-var-prefix}secondary-underline: #{rgba($slate-550, 0.5)};
+ #{$css-var-prefix}secondary-hover: #{$slate-650};
+ #{$css-var-prefix}secondary-hover-background: #{$slate-650};
+ #{$css-var-prefix}secondary-hover-border: var(#{$css-var-prefix}secondary-hover-background);
+ #{$css-var-prefix}secondary-hover-underline: var(#{$css-var-prefix}secondary-hover);
+ #{$css-var-prefix}secondary-focus: #{rgba($slate-550, 0.25)};
+ #{$css-var-prefix}secondary-inverse: #{$white};
+
+ // Contrast colors
+ #{$css-var-prefix}contrast: #{$slate-900};
+ #{$css-var-prefix}contrast-background: #{$slate-900};
+ #{$css-var-prefix}contrast-border: var(#{$css-var-prefix}contrast-background);
+ #{$css-var-prefix}contrast-underline: #{rgba($slate-900, 0.5)};
+ #{$css-var-prefix}contrast-hover: #{$black};
+ #{$css-var-prefix}contrast-hover-background: #{$black};
+ #{$css-var-prefix}contrast-hover-border: var(#{$css-var-prefix}contrast-hover-background);
+ #{$css-var-prefix}contrast-hover-underline: var(#{$css-var-prefix}secondary-hover);
+ #{$css-var-prefix}contrast-focus: #{rgba($slate-550, 0.25)};
+ #{$css-var-prefix}contrast-inverse: #{$white};
+
+ // Box shadow
+ #{$css-var-prefix}box-shadow: functions.shadow($slate-400);
+
+ // Typography
+ @if map.get($modules, 'content/typography') {
+ // Headings colors
+ #{$css-var-prefix}h1-color: #{$zinc-800};
+ #{$css-var-prefix}h2-color: #{$zinc-750};
+ #{$css-var-prefix}h3-color: #{$zinc-700};
+ #{$css-var-prefix}h4-color: #{$zinc-650};
+ #{$css-var-prefix}h5-color: #{$zinc-600};
+ #{$css-var-prefix}h6-color: #{$zinc-550};
+
+ // Highlighted text ()
+ #{$css-var-prefix}mark-background-color: #{color.mix($amber-100, $amber-50)};
+ #{$css-var-prefix}mark-color: #{$zinc-950};
+
+ // Inserted () & Deleted ()
+ #{$css-var-prefix}ins-color: #{color.mix($jade-450, $zinc-750)};
+ #{$css-var-prefix}del-color: #{color.mix($red-500, $zinc-750)};
+
+ // Blockquote
+ #{$css-var-prefix}blockquote-border-color: var(#{$css-var-prefix}muted-border-color);
+ #{$css-var-prefix}blockquote-footer-color: var(#{$css-var-prefix}muted-color);
+ }
+
+ // Button
+ @if map.get($modules, 'content/button') {
+ // To disable box-shadow, remove the var or set to '0 0 0 rgba(0, 0, 0, 0)'
+ // Don't use, 'none, 'false, 'null', '0', etc.
+ #{$css-var-prefix}button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
+ #{$css-var-prefix}button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
+ }
+
+ // Table
+ @if map.get($modules, 'content/table') {
+ #{$css-var-prefix}table-border-color: var(#{$css-var-prefix}muted-border-color);
+ #{$css-var-prefix}table-row-stripped-background-color: #{rgba($zinc-500, 0.0375)};
+ }
+
+ // Form elements
+ @if map.get($modules, 'forms/basics') {
+ #{$css-var-prefix}form-element-background-color: #{color.mix($slate-50, $white, 25%)};
+ #{$css-var-prefix}form-element-selected-background-color: #{$slate-100};
+ #{$css-var-prefix}form-element-border-color: #{$slate-150};
+ #{$css-var-prefix}form-element-color: #{$zinc-850};
+ #{$css-var-prefix}form-element-placeholder-color: var(#{$css-var-prefix}muted-color);
+ #{$css-var-prefix}form-element-active-background-color: #{$white};
+ #{$css-var-prefix}form-element-active-border-color: var(#{$css-var-prefix}primary-border);
+ #{$css-var-prefix}form-element-focus-color: var(#{$css-var-prefix}primary-focus);
+ #{$css-var-prefix}form-element-disabled-background-color: var(#{$css-var-prefix}form-element-background-color);
+ #{$css-var-prefix}form-element-disabled-border-color: var(#{$css-var-prefix}form-element-border-color);
+ #{$css-var-prefix}form-element-disabled-opacity: 0.5;
+ #{$css-var-prefix}form-element-invalid-border-color: #{color.mix($red-500, $zinc-350)};
+ #{$css-var-prefix}form-element-invalid-active-border-color: #{color.mix($red-500, $zinc-350, 75%)};
+ #{$css-var-prefix}form-element-invalid-focus-color: var(
+ #{$css-var-prefix}form-element-invalid-active-border-color
+ );
+ #{$css-var-prefix}form-element-valid-border-color: #{color.mix($jade-450, $zinc-350)};
+ #{$css-var-prefix}form-element-valid-active-border-color: #{color.mix($jade-450, $zinc-350, 75%)};
+ #{$css-var-prefix}form-element-valid-focus-color: var(#{$css-var-prefix}form-element-valid-active-border-color);
+
+ // Focus for buttons, radio and select
+ input:is([type='submit'], [type='button'], [type='reset'], [type='checkbox'], [type='radio'], [type='file']) {
+ #{$css-var-prefix}form-element-focus-color: var(#{$css-var-prefix}primary-focus);
+ }
+ }
+
+ // Range (input[type="range"])
+ @if map.get($modules, 'forms/input-range') {
+ #{$css-var-prefix}range-border-color: #{$slate-100};
+ #{$css-var-prefix}range-active-border-color: #{$slate-200};
+ #{$css-var-prefix}range-thumb-border-color: var(#{$css-var-prefix}background-color);
+ #{$css-var-prefix}range-thumb-color: var(#{$css-var-prefix}primary-background);
+ #{$css-var-prefix}range-thumb-active-color: var(#{$css-var-prefix}primary-hover-background);
+ }
+
+ // Form validation icons
+ @if map.get($modules, 'forms/basics') {
+ #{$css-var-prefix}icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{functions.display-rgb(color.mix($jade-450, $zinc-350))}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
+ #{$css-var-prefix}icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{functions.display-rgb(color.mix($red-500, $zinc-350, 75%))}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
+ }
+
+ // Document
+ color-scheme: light;
+}
diff --git a/webapp/src/styles/theme/_schemes.scss b/webapp/src/styles/theme/_schemes.scss
new file mode 100644
index 0000000..1ce5f5d
--- /dev/null
+++ b/webapp/src/styles/theme/_schemes.scss
@@ -0,0 +1,37 @@
+@use 'sass:map';
+@use '@picocss/pico/scss/settings' as *;
+
+@use './light';
+@use './dark';
+
+/**
+ * Color schemes
+ */
+
+// Light color scheme (Default)
+// Can be forced with data-theme="light"
+[data-theme='light'],
+:root:not([data-theme='dark']) {
+ @include light.theme;
+}
+
+// Dark color scheme (Auto)
+// Automatically enabled if user has Dark mode enabled
+@media only screen and (prefers-color-scheme: dark) {
+ :root:not([data-theme]) {
+ @include dark.theme;
+ }
+}
+
+// Dark color scheme (Forced)
+// Enabled if forced with data-theme="dark"
+[data-theme='dark'] {
+ @include dark.theme;
+}
+
+progress,
+[type='checkbox'],
+[type='radio'],
+[type='range'] {
+ accent-color: var(#{$css-var-prefix}primary);
+}
diff --git a/webapp/src/styles/theme/_styles.scss b/webapp/src/styles/theme/_styles.scss
new file mode 100644
index 0000000..aaf9c99
--- /dev/null
+++ b/webapp/src/styles/theme/_styles.scss
@@ -0,0 +1,187 @@
+@use 'sass:map';
+@use '@picocss/pico/scss/settings' as *;
+@use '@picocss/pico/scss/colors' as *;
+@use '@picocss/pico/scss/helpers/functions';
+
+/**
+ * Styles
+ */
+
+:root {
+ // Typography
+ #{$css-var-prefix}font-family-emoji: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ #{$css-var-prefix}font-family-sans-serif: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
+ var(#{$css-var-prefix}font-family-emoji);
+ #{$css-var-prefix}font-family-monospace: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono',
+ monospace, var(#{$css-var-prefix}font-family-emoji);
+ #{$css-var-prefix}font-family: var(#{$css-var-prefix}font-family-sans-serif);
+ #{$css-var-prefix}line-height: 1.5;
+ #{$css-var-prefix}font-weight: 400;
+ #{$css-var-prefix}font-size: 100%;
+ #{$css-var-prefix}text-underline-offset: 0.1rem;
+
+ // Borders
+ #{$css-var-prefix}border-radius: 0.375rem;
+ #{$css-var-prefix}border-width: 0.0625rem;
+ #{$css-var-prefix}outline-width: 0.125rem;
+
+ // Transitions
+ #{$css-var-prefix}transition: 0.2s ease-in-out;
+
+ // Spacings
+ #{$css-var-prefix}spacing: 1rem;
+
+ // Spacings for typography elements
+ @if map.get($modules, 'content/typography') {
+ & {
+ #{$css-var-prefix}typography-spacing-vertical: 1rem;
+ }
+ }
+
+ // Spacings for body > header, body > main, body > footer, section, article
+ @if map.get($modules, 'layout/landmarks') or
+ map.get($modules, 'layout/section') or
+ map.get($modules, 'components/card') or
+ map.get($modules, 'components/modal')
+ {
+ & {
+ #{$css-var-prefix}block-spacing-vertical: calc(var(#{$css-var-prefix}spacing) * 1.5);
+ #{$css-var-prefix}block-spacing-horizontal: calc(var(#{$css-var-prefix}spacing) * 1.5);
+ }
+ }
+
+ // Spacings for form elements and button
+ @if map.get($modules, 'content/button') or map.get($modules, 'forms/basic') {
+ & {
+ #{$css-var-prefix}form-element-spacing-vertical: 0.75rem;
+ #{$css-var-prefix}form-element-spacing-horizontal: 1rem;
+ }
+ }
+
+ // Font weight for form labels & fieldsets legend
+ @if map.get($modules, 'forms/basic') {
+ & {
+ #{$css-var-prefix}form-label-font-weight: var(#{$css-var-prefix}font-weight);
+ }
+ }
+
+ // Checkboxes icons
+ @if map.get($modules, 'forms/checkbox-radio-switch') {
+ & {
+ #{$css-var-prefix}icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{functions.display-rgb($white)}' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
+ #{$css-var-prefix}icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{functions.display-rgb($white)}' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");
+ }
+ }
+
+ // Chevron icons
+ @if map.get($modules, 'forms/basics') or
+ map.get($modules, 'components/accordion') or
+ map.get($modules, 'components/dropdown')
+ {
+ & {
+ #{$css-var-prefix}icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='#{functions.display-rgb($zinc-400)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+ }
+ }
+
+ // Responsive root font size
+ @if $enable-responsive-typography {
+ @each $key, $values in $breakpoints {
+ @if $values {
+ @media (min-width: map.get($values, 'breakpoint')) {
+ #{$css-var-prefix}font-size: map.get($values, 'root-font-size');
+ }
+ }
+ }
+ }
+}
+
+// Link
+@if map.get($modules, 'content/link') {
+ a {
+ #{$css-var-prefix}text-decoration: underline;
+
+ // Secondary & Contrast
+ @if enable-classes {
+ &.secondary,
+ &.contrast {
+ #{$css-var-prefix}text-decoration: underline;
+ }
+ }
+ }
+}
+
+// Typography
+@if map.get($modules, 'content/typography') {
+ // Small
+ small {
+ #{$css-var-prefix}font-size: 0.875em;
+ }
+
+ // Headings
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ #{$css-var-prefix}font-weight: 700;
+ }
+
+ h1 {
+ #{$css-var-prefix}font-size: 2rem;
+ #{$css-var-prefix}line-height: 1.125;
+ #{$css-var-prefix}typography-spacing-top: 3rem;
+ }
+
+ h2 {
+ #{$css-var-prefix}font-size: 1.75rem;
+ #{$css-var-prefix}line-height: 1.15;
+ #{$css-var-prefix}typography-spacing-top: 2.625rem;
+ }
+
+ h3 {
+ #{$css-var-prefix}font-size: 1.5rem;
+ #{$css-var-prefix}line-height: 1.175;
+ #{$css-var-prefix}typography-spacing-top: 2.25rem;
+ }
+
+ h4 {
+ #{$css-var-prefix}font-size: 1.25rem;
+ #{$css-var-prefix}line-height: 1.2;
+ #{$css-var-prefix}typography-spacing-top: 1.874rem;
+ }
+
+ h5 {
+ #{$css-var-prefix}font-size: 1.125rem;
+ #{$css-var-prefix}line-height: 1.225;
+ #{$css-var-prefix}typography-spacing-top: 1.6875rem;
+ }
+
+ h6 {
+ #{$css-var-prefix}font-size: 1rem;
+ #{$css-var-prefix}line-height: 1.25;
+ #{$css-var-prefix}typography-spacing-top: 1.5rem;
+ }
+}
+
+// Buttons
+button,
+[type='submit'],
+[type='reset'],
+[type='button'],
+[type='file']::file-selector-button,
+[role='button'] {
+ #{$css-var-prefix}font-weight: 700;
+}
+
+// Table
+@if map.get($modules, 'content/table') {
+ thead,
+ tfoot {
+ th,
+ td {
+ #{$css-var-prefix}font-weight: 600;
+ #{$css-var-prefix}border-width: 0.1875rem;
+ }
+ }
+}
diff --git a/webapp/src/styles/typography.scss b/webapp/src/styles/typography.scss
deleted file mode 100644
index b415606..0000000
--- a/webapp/src/styles/typography.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-h1,
-h2,
-h3 {
- font-family: Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif;
- font-weight: 500;
-}
-
-h1 {
- font-size: 5.7rem;
-}
-
-h2 {
- font-size: 4.5rem;
-}
-
-h3 {
- font-size: 3.6rem;
-}