summaryrefslogtreecommitdiffstats
path: root/apps/web-shared/src
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2022-06-01 22:10:32 +0200
committerivarlovlie <git@ivarlovlie.no>2022-06-01 22:10:32 +0200
commita640703f2da8815dc26ad1600a6f206be1624379 (patch)
treedbda195fb5783d16487e557e06471cf848b75427 /apps/web-shared/src
downloadgreatoffice-a640703f2da8815dc26ad1600a6f206be1624379.tar.xz
greatoffice-a640703f2da8815dc26ad1600a6f206be1624379.zip
feat: Initial after clean slate
Diffstat (limited to 'apps/web-shared/src')
-rw-r--r--apps/web-shared/src/components/alert.svelte66
-rw-r--r--apps/web-shared/src/components/button.svelte116
-rw-r--r--apps/web-shared/src/components/chip.svelte50
-rw-r--r--apps/web-shared/src/components/details.svelte35
-rw-r--r--apps/web-shared/src/components/dropdown.svelte374
-rw-r--r--apps/web-shared/src/components/form/index.ts5
-rw-r--r--apps/web-shared/src/components/form/textarea.svelte48
-rw-r--r--apps/web-shared/src/components/icon.svelte87
-rw-r--r--apps/web-shared/src/components/menu/index.ts9
-rw-r--r--apps/web-shared/src/components/menu/item.svelte8
-rw-r--r--apps/web-shared/src/components/menu/menu.svelte54
-rw-r--r--apps/web-shared/src/components/menu/separator.svelte2
-rw-r--r--apps/web-shared/src/components/modal.svelte66
-rw-r--r--apps/web-shared/src/components/pre-header.svelte37
-rw-r--r--apps/web-shared/src/components/stopwatch.svelte161
-rw-r--r--apps/web-shared/src/components/table/index.ts15
-rw-r--r--apps/web-shared/src/components/table/paginator.svelte101
-rw-r--r--apps/web-shared/src/components/table/table.svelte3
-rw-r--r--apps/web-shared/src/components/table/tbody.svelte3
-rw-r--r--apps/web-shared/src/components/table/tcell.svelte23
-rw-r--r--apps/web-shared/src/components/table/thead.svelte10
-rw-r--r--apps/web-shared/src/components/table/trow.svelte6
-rw-r--r--apps/web-shared/src/components/tile.svelte4
-rw-r--r--apps/web-shared/src/lib/api/internal-fetch.ts170
-rw-r--r--apps/web-shared/src/lib/api/root.ts6
-rw-r--r--apps/web-shared/src/lib/api/time-entry.ts86
-rw-r--r--apps/web-shared/src/lib/api/user.ts47
-rw-r--r--apps/web-shared/src/lib/colors.ts47
-rw-r--r--apps/web-shared/src/lib/configuration.ts60
-rw-r--r--apps/web-shared/src/lib/helpers.ts489
-rw-r--r--apps/web-shared/src/lib/models/CreateAccountPayload.ts4
-rw-r--r--apps/web-shared/src/lib/models/ErrorResult.ts4
-rw-r--r--apps/web-shared/src/lib/models/IInternalFetchRequest.ts6
-rw-r--r--apps/web-shared/src/lib/models/IInternalFetchResponse.ts6
-rw-r--r--apps/web-shared/src/lib/models/ISession.ts7
-rw-r--r--apps/web-shared/src/lib/models/IValidationResult.ts31
-rw-r--r--apps/web-shared/src/lib/models/LoginPayload.ts4
-rw-r--r--apps/web-shared/src/lib/models/TimeCategoryDto.ts9
-rw-r--r--apps/web-shared/src/lib/models/TimeEntryDto.ts13
-rw-r--r--apps/web-shared/src/lib/models/TimeEntryQuery.ts27
-rw-r--r--apps/web-shared/src/lib/models/TimeLabelDto.ts8
-rw-r--r--apps/web-shared/src/lib/models/TimeQueryDto.ts29
-rw-r--r--apps/web-shared/src/lib/models/UnwrappedEntryDateTime.ts9
-rw-r--r--apps/web-shared/src/lib/models/UpdateProfilePayload.ts4
-rw-r--r--apps/web-shared/src/lib/persistent-store.ts102
-rw-r--r--apps/web-shared/src/lib/session.ts62
-rw-r--r--apps/web-shared/src/styles/_base.scss48
-rw-r--r--apps/web-shared/src/styles/base/_accessibility.scss17
-rw-r--r--apps/web-shared/src/styles/base/_breakpoints.scss15
-rw-r--r--apps/web-shared/src/styles/base/_buttons.scss24
-rw-r--r--apps/web-shared/src/styles/base/_colors.scss6
-rw-r--r--apps/web-shared/src/styles/base/_forms.scss22
-rw-r--r--apps/web-shared/src/styles/base/_grid-layout.scss261
-rw-r--r--apps/web-shared/src/styles/base/_icons.scss62
-rw-r--r--apps/web-shared/src/styles/base/_mixins.scss151
-rw-r--r--apps/web-shared/src/styles/base/_reset.scss83
-rw-r--r--apps/web-shared/src/styles/base/_shared-styles.scss34
-rw-r--r--apps/web-shared/src/styles/base/_spacing.scss20
-rw-r--r--apps/web-shared/src/styles/base/_typography.scss185
-rw-r--r--apps/web-shared/src/styles/base/_util.scss1738
-rw-r--r--apps/web-shared/src/styles/base/_visibility.scss23
-rw-r--r--apps/web-shared/src/styles/base/_z-index.scss6
-rw-r--r--apps/web-shared/src/styles/components/alert.scss69
-rw-r--r--apps/web-shared/src/styles/components/autocomplete.scss76
-rw-r--r--apps/web-shared/src/styles/components/btn-states.scss51
-rw-r--r--apps/web-shared/src/styles/components/chip.scss117
-rw-r--r--apps/web-shared/src/styles/components/circle-loader.scss315
-rw-r--r--apps/web-shared/src/styles/components/custom-checkbox.scss131
-rw-r--r--apps/web-shared/src/styles/components/custom-select.scss158
-rw-r--r--apps/web-shared/src/styles/components/details.scss57
-rw-r--r--apps/web-shared/src/styles/components/dropdown.scss98
-rw-r--r--apps/web-shared/src/styles/components/form-validator.scss18
-rw-r--r--apps/web-shared/src/styles/components/interactive-table.scss156
-rw-r--r--apps/web-shared/src/styles/components/list.scss195
-rw-r--r--apps/web-shared/src/styles/components/menu-bar.scss139
-rw-r--r--apps/web-shared/src/styles/components/menu.scss81
-rw-r--r--apps/web-shared/src/styles/components/modal.scss105
-rw-r--r--apps/web-shared/src/styles/components/pagination.scss77
-rw-r--r--apps/web-shared/src/styles/components/popover.scss38
-rw-r--r--apps/web-shared/src/styles/components/pre-header.scss46
-rw-r--r--apps/web-shared/src/styles/components/radios-checkboxes.scss134
-rw-r--r--apps/web-shared/src/styles/components/select-autocomplete.scss173
-rw-r--r--apps/web-shared/src/styles/components/tabbed-navigation.scss133
-rw-r--r--apps/web-shared/src/styles/components/table.scss147
-rw-r--r--apps/web-shared/src/styles/components/user-menu.scss81
-rw-r--r--apps/web-shared/src/styles/custom-style/_buttons.scss111
-rw-r--r--apps/web-shared/src/styles/custom-style/_colors.scss119
-rw-r--r--apps/web-shared/src/styles/custom-style/_forms.scss56
-rw-r--r--apps/web-shared/src/styles/custom-style/_icons.scss19
-rw-r--r--apps/web-shared/src/styles/custom-style/_shared-styles.scss59
-rw-r--r--apps/web-shared/src/styles/custom-style/_spacing.scss49
-rw-r--r--apps/web-shared/src/styles/custom-style/_typography.scss92
-rw-r--r--apps/web-shared/src/styles/custom-style/_util.scss41
93 files changed, 8349 insertions, 0 deletions
diff --git a/apps/web-shared/src/components/alert.svelte b/apps/web-shared/src/components/alert.svelte
new file mode 100644
index 0000000..4771f78
--- /dev/null
+++ b/apps/web-shared/src/components/alert.svelte
@@ -0,0 +1,66 @@
+<script>
+ import {afterUpdate} from "svelte";
+
+ export let title = "";
+ export let message = "";
+ export let type = "info";
+ export let visible = true;
+ export let closeable = false;
+
+ afterUpdate(() => {
+ if (type === "default") {
+ type = "primary";
+ }
+ });
+</script>
+
+<div class="alert alert--{type} padding-sm radius-md"
+ class:alert--is-visible={visible}
+ role="alert">
+ <div class="flex justify-between">
+ <div class="flex flex-row items-center">
+ <svg class="icon icon--sm alert__icon margin-right-xxs"
+ viewBox="0 0 24 24"
+ aria-hidden="true">
+ <path d="M12,0C5.383,0,0,5.383,0,12s5.383,12,12,12s12-5.383,12-12S18.617,0,12,0z M14.658,18.284 c-0.661,0.26-2.952,1.354-4.272,0.191c-0.394-0.346-0.59-0.785-0.59-1.318c0-0.998,0.328-1.868,0.919-3.957 c0.104-0.395,0.231-0.907,0.231-1.313c0-0.701-0.266-0.887-0.987-0.887c-0.352,0-0.742,0.125-1.095,0.257l0.195-0.799 c0.787-0.32,1.775-0.71,2.621-0.71c1.269,0,2.203,0.633,2.203,1.837c0,0.347-0.06,0.955-0.186,1.375l-0.73,2.582 c-0.151,0.522-0.424,1.673-0.001,2.014c0.416,0.337,1.401,0.158,1.887-0.071L14.658,18.284z M13.452,8c-0.828,0-1.5-0.672-1.5-1.5 s0.672-1.5,1.5-1.5s1.5,0.672,1.5,1.5S14.28,8,13.452,8z"></path>
+ </svg>
+ {#if title}
+ <p class="text-sm">
+ <strong class="error-title">{title}</strong>
+ </p>
+ {:else if message}
+ <div class="text-component text-sm break-word">
+ {@html message}
+ </div>
+ {/if}
+ </div>
+ {#if closeable}
+ <button class="reset alert__close-btn"
+ on:click={() => visible = false}>
+ <svg class="icon"
+ viewBox="0 0 20 20"
+ fill="none"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2">
+ <title>Close alert</title>
+ <line x1="3"
+ y1="3"
+ x2="17"
+ y2="17"/>
+ <line x1="17"
+ y1="3"
+ x2="3"
+ y2="17"/>
+ </svg>
+ </button>
+ {/if}
+ </div>
+
+ {#if message && title}
+ <div class="text-component text-sm break-word padding-top-xs">
+ {@html message}
+ </div>
+ {/if}
+</div>
diff --git a/apps/web-shared/src/components/button.svelte b/apps/web-shared/src/components/button.svelte
new file mode 100644
index 0000000..5eaf19f
--- /dev/null
+++ b/apps/web-shared/src/components/button.svelte
@@ -0,0 +1,116 @@
+<script lang="ts">
+ import Icon from "$shared/components/icon.svelte";
+
+ export let text = "";
+ export let title = "";
+ export let href = "";
+ export let variant: "primary"|"secondary"|"subtle" = "primary";
+ export let type: "button"|"submit"|"reset" = "button";
+ export let disabled = false;
+ export let loading = false;
+ export let icon = "";
+ export let icon_right_aligned = false;
+ export let icon_width = false;
+ export let icon_height = false;
+ export let id;
+ export let tabindex;
+ export let style;
+
+ $: shared_props = {
+ type: type,
+ id: id || null,
+ title: title || null,
+ disabled: disabled || null,
+ tabindex: tabindex || null,
+ style: style || null,
+ "aria-controls": ($$restProps["aria-controls"] ?? "") || null,
+ class: [variant === "reset" ? "reset" : `btn btn--${variant} btn--preserve-width ${loading ? "btn--state-b" : ""}`, $$restProps.class ?? ""].filter(Boolean).join(" "),
+ };
+</script>
+
+<template>
+ {#if href && !disabled}
+ <a {href}
+ {...shared_props}
+ on:click>
+ <span class="btn__content-a">
+ {#if icon !== ""}
+ {#if icon_right_aligned}
+ {text}
+ <Icon class="{text ? 'margin-left-xxxs': ''}"
+ width={icon_width}
+ height={icon_height}
+ name={icon}/>
+ {:else}
+ <Icon class="{text ? 'margin-left-xxxs': ''}"
+ width={icon_width}
+ height={icon_height}
+ name={icon}/>
+ {text}
+ {/if}
+ {:else}
+ {text}
+ {/if}
+ </span>
+ {#if variant !== "reset" && loading}
+ <span class="btn__content-b">
+ <svg class="icon icon--is-spinning"
+ aria-hidden="true"
+ viewBox="0 0 16 16">
+ <title>Loading</title>
+ <g stroke-width="1"
+ fill="currentColor"
+ stroke="currentColor">
+ <path d="M.5,8a7.5,7.5,0,1,1,1.91,5"
+ fill="none"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"/>
+ </g>
+ </svg>
+ </span>
+ {/if}
+ </a>
+ {:else}
+ <button {...shared_props}
+ on:click>
+ <span class="btn__content-a">
+ {#if icon !== ""}
+ {#if icon_right_aligned}
+ {text}
+ <Icon class="{text ? 'margin-left-xxxs': ''}"
+ width={icon_width}
+ height={icon_height}
+ name={icon}/>
+ {:else}
+ <Icon class="{text ? 'margin-left-xxxs': ''}"
+ width={icon_width}
+ height={icon_height}
+ name={icon}/>
+ {text}
+ {/if}
+ {:else}
+ {text}
+ {/if}
+ </span>
+ {#if variant !== "reset" && loading}
+ <span class="btn__content-b">
+ <svg class="icon icon--is-spinning"
+ aria-hidden="true"
+ viewBox="0 0 16 16">
+ <title>Loading</title>
+ <g stroke-width="1"
+ fill="currentColor"
+ stroke="currentColor">
+ <path d="M.5,8a7.5,7.5,0,1,1,1.91,5"
+ fill="none"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"/>
+ </g>
+ </svg>
+ </span>
+ {/if}
+ </button>
+ {/if}
+</template>
diff --git a/apps/web-shared/src/components/chip.svelte b/apps/web-shared/src/components/chip.svelte
new file mode 100644
index 0000000..7fbb445
--- /dev/null
+++ b/apps/web-shared/src/components/chip.svelte
@@ -0,0 +1,50 @@
+<script>
+ import {IconNames} from "$shared/lib/configuration";
+ import {createEventDispatcher} from "svelte";
+ import Button from "./button.svelte";
+
+ const dispatch = createEventDispatcher();
+ export let removable = false;
+ export let clickable = false;
+ export let text = "";
+ export let id = "";
+ export let color = "";
+ export let tabindex = "";
+
+ function handle_remove() {
+ if (removable) {
+ dispatch("remove", {
+ id
+ });
+ }
+ }
+
+ function handle_click() {
+ if (clickable) {
+ dispatch("clicked", {
+ id
+ });
+ }
+ }
+</script>
+
+<div class="chip break-word text-sm justify-between justify-start@md {clickable ? 'chip--interactive' : ''}"
+ on:click={handle_click}
+ id={id}
+ style={color !== "" ? `background-color: ${color}15; border: 1px solid ${color}; color: ${color}` : ""}
+ {tabindex}
+>
+ <span class="chip__label">{text}</span>
+
+ {#if removable}
+ <Button class="chip__btn"
+ variant="reset"
+ style="{color !== '' ? `background-color: ${color}45;` : ''}"
+ {tabindex}
+ icon="{IconNames.x}"
+ icon_width="initial"
+ icon_height="initial"
+ on:click={handle_remove}
+ />
+ {/if}
+</div>
diff --git a/apps/web-shared/src/components/details.svelte b/apps/web-shared/src/components/details.svelte
new file mode 100644
index 0000000..6ccacb0
--- /dev/null
+++ b/apps/web-shared/src/components/details.svelte
@@ -0,0 +1,35 @@
+<script>
+ import {random_string} from "$shared/lib/helpers";
+
+ let open = false;
+ export let summary;
+ const id = "details-" + random_string(4);
+
+ function on_toggle(event) {
+ open = event.target.open;
+ }
+</script>
+
+<details class="details margin-bottom-sm"
+ on:toggle={on_toggle}
+ id={id}>
+ <summary class="details__summary"
+ aria-controls={id}
+ aria-expanded={open}>
+ <span class="flex items-center">
+ <svg
+ class="icon icon--xxs margin-right-xxxs"
+ aria-hidden="true"
+ viewBox="0 0 12 12">
+ <path
+ d="M2.783.088A.5.5,0,0,0,2,.5v11a.5.5,0,0,0,.268.442A.49.49,0,0,0,2.5,12a.5.5,0,0,0,.283-.088l8-5.5a.5.5,0,0,0,0-.824Z"/>
+ </svg>
+ <span>{summary}</span>
+ </span>
+ </summary>
+ <div
+ class="details__content text-component margin-top-xs"
+ aria-hidden={!open}>
+ <slot/>
+ </div>
+</details>
diff --git a/apps/web-shared/src/components/dropdown.svelte b/apps/web-shared/src/components/dropdown.svelte
new file mode 100644
index 0000000..b5068a7
--- /dev/null
+++ b/apps/web-shared/src/components/dropdown.svelte
@@ -0,0 +1,374 @@
+<script lang="ts">
+ // @ts-ignore
+ import {go, highlight} from "fuzzysort";
+ import {element_has_focus, random_string} from "$shared/lib/helpers";
+ import Button from "$shared/components/button.svelte";
+ import Chip from "$shared/components/chip.svelte";
+
+ export let name;
+ export let id;
+ export let maxlength;
+ export let placeholder = "Search";
+ export let entries = [];
+ export let createable = false;
+ export let loading = false;
+ export let multiple = false;
+ export let noResultsText;
+ export let errorText;
+ export let label;
+ export let on_create_async = ({name: string}) => {
+ };
+
+ export const reset = () => methods.reset();
+ export const select = (id: string) => methods.select_entry(id);
+ export const deselect = (id: string) => methods.deselect_entry(id);
+
+ const INTERNAL_ID = "__dropdown-" + random_string(5);
+
+ let entriesUlNode;
+ let searchInputNode;
+ let searchResults = [];
+ let searchValue = "";
+ let showCreationHint = false;
+ let showDropdown = false;
+ let lastKeydownCode = "";
+ let mouseIsOverDropdown = false;
+ let mouseIsOverComponent = false;
+
+ $: hasSelection = entries.some((c) => c.selected === true);
+ $: if (searchValue.trim()) {
+ showCreationHint = createable && entries.every((c) => search.normalise_value(c.name) !== search.normalise_value(searchValue));
+ } else {
+ showCreationHint = false;
+ entries = methods.get_sorted_array(entries);
+ }
+
+ const search = {
+ normalise_value(value: string): string {
+ if (!value) {
+ return "";
+ }
+ return value.toString().trim().toLowerCase();
+ },
+ do() {
+ const query = search.normalise_value(searchValue);
+ if (!query.trim()) {
+ searchResults = [];
+ return;
+ }
+
+ const options = {
+ limit: 10,
+ allowTypo: true,
+ threshold: -10000,
+ key: "name",
+ };
+ searchResults = go(query, entries, options);
+ showDropdown = true;
+ },
+ on_input_focusout() {
+ if (lastKeydownCode !== "Tab" && (mouseIsOverDropdown || lastKeydownCode === "ArrowDown")) {
+ return;
+ }
+ const selected = entries.find((c) => c.selected === true);
+ if (selected && !multiple) {
+ searchValue = selected.name;
+ }
+ showDropdown = false;
+ }
+ };
+
+ const methods = {
+ reset(focus_input = false) {
+ searchValue = "";
+ const copy = entries;
+ for (const entry of copy) {
+ entry.selected = false;
+ }
+ entries = methods.get_sorted_array(copy);
+ if (focus_input) {
+ searchInputNode?.focus();
+ showDropdown = true;
+ } else {
+ showDropdown = false;
+ }
+ },
+ async create_entry(name) {
+ if (!name || !createable || loading) {
+ console.log("Not sending creation event due to failed preconditions", {name, createable, loading});
+ return;
+ }
+ try {
+ await on_create_async({name});
+ searchValue = "";
+ loading = false;
+ } catch (e) {
+ console.error(e);
+ }
+ },
+ select_entry(entry_id) {
+ if (!entry_id || loading) {
+ console.log("Not selecting entry due to failed preconditions", {
+ entry_id,
+ loading,
+ });
+ return;
+ }
+
+ const copy = entries;
+ let selected;
+ for (const entry of entries) {
+ if (entry.id === entry_id) {
+ entry.selected = true;
+ selected = entry;
+ if (multiple) {
+ searchValue = "";
+ } else {
+ searchValue = entry.name;
+ }
+ } else if (!multiple) {
+ entry.selected = false;
+ }
+ }
+ entries = methods.get_sorted_array(copy);
+ searchInputNode?.focus();
+ searchResults = [];
+ },
+ deselect_entry(entry_id) {
+ if (!entry_id || loading) {
+ console.log("Not deselecting entry due to failed preconditions", {
+ entry_id,
+ loading,
+ });
+ return;
+ }
+ console.log("Deselecting entry", entry_id);
+
+ const copy = entries;
+ let deselected;
+
+ for (const entry of copy) {
+ if (entry.id === entry_id) {
+ entry.selected = false;
+ deselected = entry;
+ }
+ }
+
+ entries = methods.get_sorted_array(copy);
+ searchInputNode?.focus();
+ },
+ get_sorted_array(entries: Array<DropdownEntry>): Array<DropdownEntry> {
+ if (!entries) {
+ return;
+ }
+ if (entries.length < 1) {
+ return [];
+ }
+ if (searchValue) {
+ return entries;
+ }
+ return (entries as any).sort((a, b) => {
+ search.normalise_value(a.name).localeCompare(search.normalise_value(b.name));
+ });
+ },
+ };
+
+ const windowEvents = {
+ on_mousemove(event) {
+ mouseIsOverDropdown = (event.target?.closest("#" + INTERNAL_ID + " .autocomplete__results") != null ?? false);
+ mouseIsOverComponent = (event.target?.closest("#" + INTERNAL_ID) != null ?? false);
+ },
+ on_click(event) {
+ if (showDropdown && !mouseIsOverDropdown && !mouseIsOverComponent && event.target.id !== id && event.target?.htmlFor !== id) {
+ showDropdown = false;
+ }
+ },
+ on_keydown(event) {
+ lastKeydownCode = event.code;
+ const enterPressed = event.code === "Enter";
+ const backspacePressed = event.code === "Backspace";
+ const arrowUpPressed = event.code === "ArrowUp";
+ const spacePressed = event.code === "Space";
+ const arrowDownPressed = event.code === "ArrowDown";
+ const searchInputHasFocus = element_has_focus(searchInputNode);
+ const focusedEntry = entriesUlNode?.querySelector("li:focus");
+
+ if (showDropdown && (enterPressed || arrowDownPressed)) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ if (searchInputHasFocus && backspacePressed && !searchValue && entries.length > 0) {
+ if (entries.filter(c => c.selected === true).at(-1)?.id ?? false) {
+ methods.deselect_entry(entries.filter(c => c.selected === true).at(-1)?.id ?? "");
+ }
+ return;
+ }
+
+ if (searchInputHasFocus) {
+ if (enterPressed && showCreationHint) {
+ methods.create_entry(searchValue.trim());
+ return;
+ }
+
+ if (arrowDownPressed) {
+ const firstEntry = entriesUlNode.querySelector("li:first-of-type");
+ if (firstEntry) {
+ firstEntry.focus();
+ }
+ return;
+ }
+ }
+
+ if (focusedEntry && (arrowUpPressed || arrowDownPressed)) {
+ if (arrowDownPressed && focusedEntry.nextElementSibling) {
+ focusedEntry.nextElementSibling.focus();
+ } else if (arrowUpPressed && focusedEntry.previousElementSibling) {
+ focusedEntry.previousElementSibling.focus();
+ }
+ return;
+ }
+
+ if (focusedEntry && (spacePressed || enterPressed)) {
+ methods.select_entry(focusedEntry.dataset.id);
+ return;
+ }
+
+ if (lastKeydownCode === "Tab" && !searchInputHasFocus) {
+ showDropdown = false;
+ }
+ },
+ on_touchend(event) {
+ windowEvents.on_mousemove(event);
+ }
+ };
+
+ interface DropdownEntry {
+ name: string,
+ id: string,
+ }
+</script>
+
+<svelte:window
+ on:keydown={windowEvents.on_keydown}
+ on:mousemove={windowEvents.on_mousemove}
+ on:touchend={windowEvents.on_touchend}
+ on:click={windowEvents.on_click}
+/>
+
+{#if label}
+ <label for="{id}"
+ class="form-label margin-bottom-xxs">{label}</label>
+{/if}
+
+<div class="autocomplete position-relative select-auto"
+ class:cursor-wait={loading}
+ class:autocomplete--results-visible={showDropdown}
+ class:select-auto--selection-done={searchValue}
+ id={INTERNAL_ID}
+>
+ <!-- input -->
+ <div class="select-auto__input-wrapper form-control"
+ class:multiple={multiple === true}
+ class:has-selection={hasSelection}>
+ {#if multiple === true && hasSelection}
+ {#each entries.filter((c) => c.selected === true) as entry}
+ <Chip id={entry.id}
+ removable={true}
+ tabindex="-1"
+ on:remove={() => methods.deselect_entry(entry.id)}
+ text={entry.name}/>
+ {/each}
+ {/if}
+ <input
+ class="reset width-100%"
+ style="outline:none;"
+ type="text"
+ {name}
+ {id}
+ {maxlength}
+ {placeholder}
+ bind:value={searchValue}
+ bind:this={searchInputNode}
+ on:input={() => search.do()}
+ on:click={() => (showDropdown = true)}
+ on:focus={() => (showDropdown = true)}
+ on:blur={search.on_input_focusout}
+ autocomplete="off"
+ />
+ <div class="select-auto__input-icon-wrapper">
+ <!-- arrow icon -->
+ <svg class="icon"
+ viewBox="0 0 16 16">
+ <title>Open selection</title>
+ <polyline points="1 5 8 12 15 5"
+ fill="none"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"/>
+ </svg>
+
+ <!-- close X icon -->
+ <button class="reset select-auto__input-btn"
+ type="button"
+ on:click={() => reset(true)}>
+ <svg class="icon"
+ viewBox="0 0 16 16">
+ <title>Reset selection</title>
+ <path
+ d="M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0Zm3.707,10.293a1,1,0,1,1-1.414,1.414L8,9.414,5.707,11.707a1,1,0,0,1-1.414-1.414L6.586,8,4.293,5.707A1,1,0,0,1,5.707,4.293L8,6.586l2.293-2.293a1,1,0,1,1,1.414,1.414L9.414,8Z"
+ />
+ </svg>
+ </button>
+ </div>
+ </div>
+
+ {#if errorText}
+ <small class="color-error">{errorText}</small>
+ {/if}
+
+ <!-- dropdown -->
+ <div class="autocomplete__results select-auto__results">
+ <ul bind:this={entriesUlNode}
+ on:keydown={(event) => event.code.startsWith("Arrow") && event.preventDefault()}
+ tabindex="-1"
+ class="autocomplete__list">
+ {#if searchResults.length > 0}
+ {#each searchResults.filter((c) => !c.selected) as result}
+ <li class="select-auto__option padding-y-xs padding-x-sm"
+ data-id={result.obj.id}
+ on:click={(e) => methods.select_entry(e.target.dataset.id)}
+ tabindex="-1">
+ {@html highlight(result, (open = '<span class="font-semibold">'), (close = "</span>"))}
+ </li>
+ {/each}
+ {:else if entries.length > 0}
+ {#each entries.filter((c) => !c.selected) as entry}
+ <li class="select-auto__option padding-y-xs padding-x-sm"
+ data-id={entry.id}
+ on:click={(e) => methods.select_entry(e.target.dataset.id)}
+ tabindex="-1">
+ {entry.name}
+ </li>
+ {/each}
+ {:else}
+ <li class="select-auto__option text-center padding-y-xs padding-x-sm pointer-events-none"
+ tabindex="-1">
+ {noResultsText}
+ </li>
+ {/if}
+ </ul>
+ {#if showCreationHint}
+ <div class="width-100% border-top border-bg-lighter padding-xxxs">
+ <Button variant="reset"
+ type="button"
+ class="width-100%"
+ text="Press enter or click to create {searchValue.trim()}"
+ title="Press enter or click here to create {searchValue.trim()}"
+ loading={loading}
+ on:click={() => methods.create_entry(searchValue.trim())}/>
+ </div>
+ {/if}
+ </div>
+</div>
diff --git a/apps/web-shared/src/components/form/index.ts b/apps/web-shared/src/components/form/index.ts
new file mode 100644
index 0000000..08769bd
--- /dev/null
+++ b/apps/web-shared/src/components/form/index.ts
@@ -0,0 +1,5 @@
+import Textarea from "./textarea.svelte";
+
+export {
+ Textarea
+};
diff --git a/apps/web-shared/src/components/form/textarea.svelte b/apps/web-shared/src/components/form/textarea.svelte
new file mode 100644
index 0000000..b313d2e
--- /dev/null
+++ b/apps/web-shared/src/components/form/textarea.svelte
@@ -0,0 +1,48 @@
+<script lang="ts">
+ export let id;
+ export let disabled = false;
+ export let loading = false;
+ export let rows = 2;
+ export let cols = 0;
+ export let name;
+ export let placeholder;
+ export let value;
+ export let label;
+ export let errorText;
+
+ $: shared_props = {
+ rows: rows || null,
+ cols: cols || null,
+ name: name || null,
+ id: id || null,
+ disabled: disabled || null,
+ class: [`form-control ${loading ? "c-disabled loading" : ""}`, $$restProps.class ?? ""].filter(Boolean).join(" "),
+ };
+
+ let textarea;
+ let scrollHeight = 0;
+
+ $:if (textarea) {
+ scrollHeight = textarea.scrollHeight;
+ }
+
+ function on_input(event) {
+ event.target.style.height = "auto";
+ event.target.style.height = (this.scrollHeight) + "px";
+ }
+</script>
+
+{#if label}
+ <label for="{id}"
+ class="form-label margin-bottom-xxs">{label}</label>
+{/if}
+<textarea {...shared_props}
+ {placeholder}
+ style="overflow-y:hidden;min-height:calc(1.5em + .75rem + 2px);{scrollHeight ? 'height:{scrollHeight}px' : ''};"
+ bind:value={value}
+ bind:this={textarea}
+ on:input={on_input}
+></textarea>
+{#if errorText}
+ <small class="color-error">{errorText}</small>
+{/if}
diff --git a/apps/web-shared/src/components/icon.svelte b/apps/web-shared/src/components/icon.svelte
new file mode 100644
index 0000000..144b45d
--- /dev/null
+++ b/apps/web-shared/src/components/icon.svelte
@@ -0,0 +1,87 @@
+<script>
+ import {IconNames} from "$shared/lib/configuration";
+
+ const icons = [
+ {
+ box: 16,
+ name: IconNames.verticalDots,
+ svg: `<path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>`,
+ },
+ {
+ box: 16,
+ name: IconNames.clock,
+ svg: `<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/><path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>`,
+ },
+ {
+ box: 21,
+ name: IconNames.trash,
+ svg: `<g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" transform="translate(3 2)"><path d="m2.5 2.5h10v12c0 1.1045695-.8954305 2-2 2h-6c-1.1045695 0-2-.8954305-2-2zm5-2c1.0543618 0 1.91816512.81587779 1.99451426 1.85073766l.00548574.14926234h-4c0-1.1045695.8954305-2 2-2z"/><path d="m.5 2.5h14"/><path d="m5.5 5.5v8"/><path d="m9.5 5.5v8"/></g>`,
+ },
+ {
+ box: 21,
+ name: IconNames.pencilSquare,
+ svg: `
+ <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" transform="translate(3 3)"><path d="m14 1c.8284271.82842712.8284271 2.17157288 0 3l-9.5 9.5-4 1 1-3.9436508 9.5038371-9.55252193c.7829896-.78700064 2.0312313-.82943964 2.864366-.12506788z"/><path d="m6.5 14.5h8"/><path d="m12.5 3.5 1 1"/></g>
+ `,
+ },
+ {
+ box: 16,
+ name: IconNames.x,
+ svg: `<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>`,
+ },
+ {
+ box: 16,
+ name: IconNames.funnel,
+ svg: `<path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>`,
+ },
+ {
+ box: 16,
+ name: IconNames.funnelFilled,
+ svg: `<path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2z"/>`,
+ },
+ {
+ box: 16,
+ name: IconNames.github,
+ svg: `
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
+ `
+ },
+ {
+ box: 21,
+ name: IconNames.refresh,
+ svg: `<g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" transform="translate(2 2)"><path d="m4.5 1.5c-2.4138473 1.37729434-4 4.02194088-4 7 0 4.418278 3.581722 8 8 8s8-3.581722 8-8-3.581722-8-8-8"/><path d="m4.5 5.5v-4h-4"/></g> `
+ },
+ {
+ box: 21,
+ name: IconNames.resetHard,
+ svg: `<g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" transform="matrix(0 1 1 0 2.5 2.5)"><path d="m13 11 3 3v-6c0-3.36502327-2.0776-6.24479706-5.0200433-7.42656457-.9209869-.36989409-1.92670197-.57343543-2.9799567-.57343543-4.418278 0-8 3.581722-8 8s3.581722 8 8 8c1.48966767 0 3.4724708-.3698516 5.0913668-1.5380762" transform="matrix(-1 0 0 -1 16 16)"/><path d="m5 5 6 6"/><path d="m11 5-6 6"/></g>`
+ },
+ {
+ box: 21,
+ name: IconNames.arrowUp,
+ svg: `<g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" transform="translate(6 3)"><path d="m8.5 4.5-4-4-4.029 4"/><path d="m4.5.5v13"/></g>`
+ },
+ {
+ box: 21,
+ name: IconNames.arrowDown,
+ svg: `<g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" transform="translate(6 4)"><path d="m.5 9.499 4 4.001 4-4.001"/><path d="m4.5.5v13" transform="matrix(-1 0 0 -1 9 14)"/></g>`
+ },
+ {
+ box: 21,
+ name: IconNames.chevronDown,
+ svg: `<path d="m8.5.5-4 4-4-4" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" transform="translate(6 8)"/>`
+ }
+ ];
+
+ export let name;
+ export let fill = false;
+ export let width = "1rem";
+ export let height = "1rem";
+ const displayIcon = icons.find((e) => e.name === name);
+</script>
+
+<svg class="icon {$$restProps.class ?? ''}"
+ style="width: {width}; height:{height}; fill: currentColor;"
+ viewBox="0 0 {displayIcon.box} {displayIcon.box}">
+ {@html displayIcon.svg}
+</svg>
diff --git a/apps/web-shared/src/components/menu/index.ts b/apps/web-shared/src/components/menu/index.ts
new file mode 100644
index 0000000..8eb7938
--- /dev/null
+++ b/apps/web-shared/src/components/menu/index.ts
@@ -0,0 +1,9 @@
+import Menu from "./menu.svelte";
+import MenuItem from "./item.svelte";
+import MenuItemSeparator from "./separator.svelte";
+
+export {
+ Menu,
+ MenuItem,
+ MenuItemSeparator
+};
diff --git a/apps/web-shared/src/components/menu/item.svelte b/apps/web-shared/src/components/menu/item.svelte
new file mode 100644
index 0000000..aeb0f99
--- /dev/null
+++ b/apps/web-shared/src/components/menu/item.svelte
@@ -0,0 +1,8 @@
+<script lang="ts">
+ export let danger = false;
+</script>
+<li role="menuitem" on:click>
+ <span class="menu__content {danger ? 'bg-error-lighter@hover color-white@hover' : ''}">
+ <slot/>
+ </span>
+</li>
diff --git a/apps/web-shared/src/components/menu/menu.svelte b/apps/web-shared/src/components/menu/menu.svelte
new file mode 100644
index 0000000..33b1160
--- /dev/null
+++ b/apps/web-shared/src/components/menu/menu.svelte
@@ -0,0 +1,54 @@
+<script lang="ts">
+ import {random_string} from "$shared/lib/helpers";
+
+ export let id = "__menu_" + random_string(3);
+ export let trigger: HTMLElement;
+ export let show = false;
+
+ let windowInnerWidth = 0;
+ let windowInnerHeight = 0;
+ let menu: HTMLMenuElement;
+
+ $: if (show && menu && trigger) {
+ const
+ selectedTriggerPosition = trigger.getBoundingClientRect(),
+ menuOnTop = (windowInnerHeight - selectedTriggerPosition.bottom) < selectedTriggerPosition.top,
+ left = selectedTriggerPosition.left,
+ right = (windowInnerWidth - selectedTriggerPosition.right),
+ isRight = (windowInnerWidth < selectedTriggerPosition.left + menu.offsetWidth),
+ vertical = menuOnTop
+ ? "bottom: " + (windowInnerHeight - selectedTriggerPosition.top) + "px;"
+ : "top: " + selectedTriggerPosition.bottom + "px;";
+
+ let horizontal = isRight ? "right: " + right + "px;" : "left: " + left + "px;";
+
+ // check right position is correct -> otherwise set left to 0
+ if (isRight && (right + menu.offsetWidth) > windowInnerWidth) horizontal = ("left: " + (windowInnerWidth - menu.offsetWidth) / 2 + "px;");
+ const maxHeight = menuOnTop ? selectedTriggerPosition.top - 20 : windowInnerHeight - selectedTriggerPosition.bottom - 20;
+ menu.setAttribute("style", horizontal + vertical + "max-height:" + Math.floor(maxHeight) + "px;");
+ }
+
+ function on_window_click(event) {
+ if (!event.target.closest("#" + id) && !event.target.closest("[aria-controls='" + id + "']")) show = false;
+ }
+
+ function on_window_touchend(event) {
+ if (!event.target.closest("#" + id) && !event.target.closest("[aria-controls='" + id + "']")) show = false;
+ }
+</script>
+
+<svelte:window
+ on:click={on_window_click}
+ on:touchend={on_window_touchend}
+ bind:innerWidth={windowInnerWidth}
+ bind:innerHeight={windowInnerHeight}
+/>
+
+<menu class="menu"
+ id="{id}"
+ bind:this={menu}
+ class:menu--is-visible={show}
+ aria-expanded="{show}"
+ aria-haspopup="true">
+ <slot name="options"/>
+</menu>
diff --git a/apps/web-shared/src/components/menu/separator.svelte b/apps/web-shared/src/components/menu/separator.svelte
new file mode 100644
index 0000000..798dce0
--- /dev/null
+++ b/apps/web-shared/src/components/menu/separator.svelte
@@ -0,0 +1,2 @@
+<li class="menu__separator"
+ role="separator"></li>
diff --git a/apps/web-shared/src/components/modal.svelte b/apps/web-shared/src/components/modal.svelte
new file mode 100644
index 0000000..f3b633c
--- /dev/null
+++ b/apps/web-shared/src/components/modal.svelte
@@ -0,0 +1,66 @@
+<script>
+ import {random_string} from "$shared/lib/helpers";
+
+ export let title = "";
+ let isVisible = false;
+ const modal_id = "modal_" + random_string(5);
+
+ function handle_keyup(e) {
+ if (e.key === "Escape") {
+ isVisible = false;
+ }
+ }
+
+ export const functions = {
+ open() {
+ isVisible = true;
+ window.addEventListener("keyup", handle_keyup);
+ },
+ close() {
+ isVisible = false;
+ window.removeEventListener("keyup", handle_keyup);
+ },
+ };
+</script>
+
+<div class="modal modal--animate-scale flex flex-center padding-md bg-dark bg-opacity-40% {isVisible ? 'modal--is-visible' : ''}"
+ id={modal_id}
+>
+ <div class="modal__content width-100% max-width-xs max-height-100% overflow-auto radius-md shadow-md bg"
+ role="alertdialog"
+ >
+ <header class="padding-y-sm padding-x-md flex items-center justify-between"
+ >
+ <h4 class="text-truncate">{title}</h4>
+
+ <button class="reset modal__close-btn modal__close-btn--inner"
+ on:click={functions.close}
+ >
+ <svg class="icon"
+ viewBox="0 0 20 20">
+ <title>Close modal window</title>
+ <g fill="none"
+ stroke="currentColor"
+ stroke-miterlimit="10"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ >
+ <line x1="3"
+ y1="3"
+ x2="17"
+ y2="17"/>
+ <line x1="17"
+ y1="3"
+ x2="3"
+ y2="17"/>
+ </g>
+ </svg>
+ </button>
+ </header>
+
+ <div class="padding-bottom-md padding-x-md">
+ <slot/>
+ </div>
+ </div>
+</div>
diff --git a/apps/web-shared/src/components/pre-header.svelte b/apps/web-shared/src/components/pre-header.svelte
new file mode 100644
index 0000000..87a19b1
--- /dev/null
+++ b/apps/web-shared/src/components/pre-header.svelte
@@ -0,0 +1,37 @@
+<script>
+ export let closable = true;
+ export let show = false;
+</script>
+
+<div class="pre-header padding-y-xs" style="{show ? '' : 'display:none'}">
+ <div class="container max-width-lg position-relative">
+ <div class="text-component text-sm padding-right-lg">
+ <p>
+ <slot/>
+ </p>
+ </div>
+ {#if closable}
+ <button class="reset pre-header__close-btn"
+ on:click={(event) => event.target.closest(".pre-header")?.remove()}>
+ <svg class="icon"
+ viewBox="0 0 20 20">
+ <title>Close header banner</title>
+ <g fill="none"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2">
+ <line x1="4"
+ y1="4"
+ x2="16"
+ y2="16"/>
+ <line x1="16"
+ y1="4"
+ x2="4"
+ y2="16"/>
+ </g>
+ </svg>
+ </button>
+ {/if}
+ </div>
+</div>
diff --git a/apps/web-shared/src/components/stopwatch.svelte b/apps/web-shared/src/components/stopwatch.svelte
new file mode 100644
index 0000000..8287e31
--- /dev/null
+++ b/apps/web-shared/src/components/stopwatch.svelte
@@ -0,0 +1,161 @@
+<script lang="ts">
+ import {writable_persistent} from "$shared/lib/persistent-store";
+ import Button from "$shared/components/button.svelte";
+ import {Textarea} from "$shared/components/form";
+ import {StorageKeys} from "$shared/lib/configuration";
+ import {Temporal} from "@js-temporal/polyfill";
+ import {createEventDispatcher, onMount} from "svelte";
+
+ const state = writable_persistent({
+ initialState: {
+ hours: 0,
+ minutes: 0,
+ seconds: 0,
+ startTime: null as Temporal.PlainTime,
+ isRunning: false,
+ intervalId: 0,
+ note: "",
+ },
+ name: StorageKeys.stopwatch,
+ });
+
+ let timeString;
+
+ $: if ($state.hours || $state.minutes || $state.seconds) {
+ timeString = $state.hours.toLocaleString(undefined, {minimumIntegerDigits: 2})
+ + ":" + $state.minutes.toLocaleString(undefined, {minimumIntegerDigits: 2})
+ + ":" + $state.seconds.toLocaleString(undefined, {minimumIntegerDigits: 2});
+ } else {
+ timeString = "--:--:--";
+ }
+
+ onMount(() => {
+ if ($state.isRunning) {
+ clearInterval($state.intervalId);
+ $state.intervalId = setInterval(step, 1000);
+ }
+ });
+
+ const dispatch = createEventDispatcher();
+
+ function step() {
+ $state.seconds = $state.seconds + 1;
+
+ if ($state.seconds == 60) {
+ $state.minutes = $state.minutes + 1;
+ $state.seconds = 0;
+ }
+
+ if ($state.minutes == 60) {
+ $state.hours = $state.hours + 1;
+ $state.minutes = 0;
+ $state.seconds = 0;
+ }
+
+ if (!$state.startTime) $state.startTime = Temporal.Now.plainTimeISO();
+ }
+
+ function reset() {
+ clearInterval($state.intervalId);
+ $state.isRunning = false;
+ $state.hours = 0;
+ $state.minutes = 0;
+ $state.seconds = 0;
+ $state.startTime = null;
+ $state.intervalId = 0;
+ $state.note = "";
+ }
+
+ let roundUpToNearest = 30;
+ let roundDownToNearest = 30;
+
+ function on_round_up() {
+ const newTime = Temporal.PlainTime
+ .from({hour: $state.hours, minute: $state.minutes, second: $state.seconds})
+ .round({
+ roundingIncrement: roundUpToNearest,
+ smallestUnit: "minute",
+ roundingMode: "ceil"
+ });
+ $state.hours = newTime.hour;
+ $state.minutes = newTime.minute;
+ $state.seconds = newTime.second;
+ }
+
+ function on_round_down() {
+ const newTime = Temporal.PlainTime
+ .from({hour: $state.hours, minute: $state.minutes, second: $state.seconds,})
+ .round({
+ roundingIncrement: roundDownToNearest,
+ smallestUnit: "minute",
+ roundingMode: "trunc"
+ });
+ $state.hours = newTime.hour;
+ $state.minutes = newTime.minute;
+ $state.seconds = newTime.second;
+ }
+
+ function on_start_stop() {
+ if ($state.isRunning) {
+ clearInterval($state.intervalId);
+ $state.isRunning = false;
+ return;
+ }
+ step();
+ $state.intervalId = setInterval(step, 1000);
+ $state.isRunning = true;
+ }
+
+ function on_create_entry() {
+ if (!$state.startTime) return;
+ const plainStartTime = Temporal.PlainTime.from($state.startTime);
+ dispatch("create", {
+ from: plainStartTime,
+ to: plainStartTime.add({hours: $state.hours, minutes: $state.minutes, seconds: $state.seconds}),
+ description: $state.note
+ });
+ reset();
+ }
+</script>
+
+<div class="grid">
+ <div class="col-6">
+ <slot name="header"></slot>
+ <pre class="text-xxl padding-y-sm">{timeString}</pre>
+ </div>
+ <div class="col-6 flex align-bottom flex-column text-xs">
+ <Button title="{$state.isRunning ? 'Stop' : 'Start'}"
+ text="{$state.isRunning ? 'Stop' : 'Start'}"
+ variant="link"
+ on:click={on_start_stop}/>
+
+ {#if $state.startTime}
+ <Button title="Reset"
+ text="Reset"
+ variant="link"
+ class="bg-error-lighter@hover color-white@hover"
+ on:click={reset}/>
+ {#if !$state.isRunning}
+ <Button title="Round up"
+ text="Round up"
+ variant="link"
+ on:click={on_round_up}/>
+ <Button title="Round down"
+ text="Round down"
+ variant="link"
+ on:click={on_round_down}/>
+ {#if $state.minutes > 0 || $state.hours > 0}
+ <Button title="Create entry"
+ text="Create entry"
+ variant="link"
+ on:click={on_create_entry}/>
+ {/if}
+ {/if}
+ {/if}
+ </div>
+</div>
+<Textarea class="width-100% margin-top-xs"
+ placeholder="What's your focus?"
+ rows="1"
+ bind:value={$state.note}
+/>
diff --git a/apps/web-shared/src/components/table/index.ts b/apps/web-shared/src/components/table/index.ts
new file mode 100644
index 0000000..8390c0e
--- /dev/null
+++ b/apps/web-shared/src/components/table/index.ts
@@ -0,0 +1,15 @@
+import TablePaginator from "./paginator.svelte";
+import Table from "./table.svelte";
+import THead from "./thead.svelte";
+import TBody from "./tbody.svelte";
+import TCell from "./tcell.svelte";
+import TRow from "./trow.svelte";
+
+export {
+ TablePaginator,
+ Table,
+ THead,
+ TBody,
+ TCell,
+ TRow
+};
diff --git a/apps/web-shared/src/components/table/paginator.svelte b/apps/web-shared/src/components/table/paginator.svelte
new file mode 100644
index 0000000..53c6392
--- /dev/null
+++ b/apps/web-shared/src/components/table/paginator.svelte
@@ -0,0 +1,101 @@
+<script>
+ import {createEventDispatcher, onMount} from "svelte";
+ import {restrict_input_to_numbers} from "$shared/lib/helpers";
+
+ const dispatch = createEventDispatcher();
+ export let page = 1;
+ export let pageCount = 1;
+ let prevCount = page;
+ let canIncrement = false;
+ let canDecrement = false;
+ $: canIncrement = page < pageCount;
+ $: canDecrement = page > 1;
+
+ onMount(() => {
+ restrict_input_to_numbers(document.querySelector("#curr-page"));
+ });
+
+ function increment() {
+ if (canIncrement) {
+ page++;
+ }
+ }
+
+ function decrement() {
+ if (canDecrement) {
+ page--;
+ }
+ }
+
+ $: if (page) {
+ handle_change();
+ }
+
+ function handle_change() {
+ if (page === prevCount) {
+ return;
+ }
+ prevCount = page;
+ if (page > pageCount) {
+ page = pageCount;
+ }
+ dispatch("value_change", {
+ newValue: page,
+ });
+ }
+</script>
+
+<nav class="pagination"
+ aria-label="Pagination">
+ <ul class="pagination__list flex flex-wrap gap-xxxs justify-center justify-end@md">
+ <li>
+ <button on:click={decrement}
+ class="reset pagination__item {canDecrement ? '' : 'c-disabled'}">
+ <svg class="icon icon--xs flip-x"
+ viewBox="0 0 16 16"
+ ><title>Go to previous page</title>
+ <polyline
+ points="6 2 12 8 6 14"
+ fill="none"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"
+ />
+ </svg>
+ </button>
+ </li>
+
+ <li>
+ <span class="pagination__jumper flex items-center">
+ <input
+ aria-label="Page number"
+ class="form-control"
+ id="curr-page"
+ type="text"
+ on:change={handle_change}
+ value={page}
+ />
+ <em>of {pageCount}</em>
+ </span>
+ </li>
+
+ <li>
+ <button on:click={increment}
+ class="reset pagination__item {canIncrement ? '' : 'c-disabled'}">
+ <svg class="icon icon--xs"
+ viewBox="0 0 16 16"
+ ><title>Go to next page</title>
+ <polyline
+ points="6 2 12 8 6 14"
+ fill="none"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"
+ />
+ </svg>
+ </button>
+ </li>
+ </ul>
+</nav>
diff --git a/apps/web-shared/src/components/table/table.svelte b/apps/web-shared/src/components/table/table.svelte
new file mode 100644
index 0000000..4acbf37
--- /dev/null
+++ b/apps/web-shared/src/components/table/table.svelte
@@ -0,0 +1,3 @@
+<table class="int-table {$$restProps.class ?? ''}">
+ <slot/>
+</table>
diff --git a/apps/web-shared/src/components/table/tbody.svelte b/apps/web-shared/src/components/table/tbody.svelte
new file mode 100644
index 0000000..f0617fa
--- /dev/null
+++ b/apps/web-shared/src/components/table/tbody.svelte
@@ -0,0 +1,3 @@
+<tbody class="int-table__body {$$restProps.class ?? ''}">
+<slot/>
+</tbody>
diff --git a/apps/web-shared/src/components/table/tcell.svelte b/apps/web-shared/src/components/table/tcell.svelte
new file mode 100644
index 0000000..76f500f
--- /dev/null
+++ b/apps/web-shared/src/components/table/tcell.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+ export let thScope: "row"|"col"|"rowgroup"|"colgroup"|"";
+ export let colspan = "";
+ export let type: "th"|"td" = "td";
+ export let style;
+
+ $: shared_props = {
+ colspan: colspan || null,
+ style: style || null,
+ class: [type === "th" ? "int-table__cell--th" : "", "int-table__cell", $$restProps.class ?? ""].filter(Boolean).join(" "),
+ };
+</script>
+{#if type === "th"}
+ <th {thScope}
+ {...shared_props}>
+ <slot/>
+ </th>
+{/if}
+{#if type === "td"}
+ <td {...shared_props}>
+ <slot/>
+ </td>
+{/if}
diff --git a/apps/web-shared/src/components/table/thead.svelte b/apps/web-shared/src/components/table/thead.svelte
new file mode 100644
index 0000000..aa20bf0
--- /dev/null
+++ b/apps/web-shared/src/components/table/thead.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+ import TRow from "./trow.svelte";
+</script>
+
+
+<thead class="int-table__header {$$restProps.class ?? ''}">
+<TRow>
+ <slot/>
+</TRow>
+</thead>
diff --git a/apps/web-shared/src/components/table/trow.svelte b/apps/web-shared/src/components/table/trow.svelte
new file mode 100644
index 0000000..35b34bb
--- /dev/null
+++ b/apps/web-shared/src/components/table/trow.svelte
@@ -0,0 +1,6 @@
+<script>
+ export let dataId;
+</script>
+<tr class="int-table__row {$$restProps.class ?? ''}" data-id={dataId}>
+ <slot/>
+</tr>
diff --git a/apps/web-shared/src/components/tile.svelte b/apps/web-shared/src/components/tile.svelte
new file mode 100644
index 0000000..b8e9cdf
--- /dev/null
+++ b/apps/web-shared/src/components/tile.svelte
@@ -0,0 +1,4 @@
+<section class="bg-light radius-sm padding-sm inner-glow shadow-xs {$$restProps.class??''}"
+ style="height: fit-content;">
+ <slot/>
+</section>
diff --git a/apps/web-shared/src/lib/api/internal-fetch.ts b/apps/web-shared/src/lib/api/internal-fetch.ts
new file mode 100644
index 0000000..8659ccb
--- /dev/null
+++ b/apps/web-shared/src/lib/api/internal-fetch.ts
@@ -0,0 +1,170 @@
+import {Temporal} from "@js-temporal/polyfill";
+import {clear_session_data} from "$shared/lib/session";
+import {resolve_references} from "$shared/lib/helpers";
+import {replace} from "svelte-spa-router";
+import type {IInternalFetchResponse} from "$shared/lib/models/IInternalFetchResponse";
+import type {IInternalFetchRequest} from "$shared/lib/models/IInternalFetchRequest";
+
+export async function http_post(url: string, body?: object|string, timeout = -1, skip_401_check = false, abort_signal: AbortSignal = undefined): Promise<IInternalFetchResponse> {
+ const init = {
+ method: "post",
+ } as RequestInit;
+
+ if (abort_signal) {
+ init.signal = abort_signal;
+ }
+
+ if (body) {
+ init.headers = {
+ "Content-Type": "application/json;charset=UTF-8",
+ };
+ init.body = JSON.stringify(body);
+ }
+
+ const response = await internal_fetch({url, init, timeout});
+ const result = {} as IInternalFetchResponse;
+
+ if (!skip_401_check && await is_401(response)) return result;
+
+ result.ok = response.ok;
+ result.status = response.status;
+ result.http_response = response;
+
+ if (response.status !== 204) {
+ try {
+ const ct = response.headers.get("Content-Type")?.toString() ?? "";
+ if (ct.startsWith("application/json")) {
+ const data = await response.json();
+ result.data = resolve_references(data);
+ } else if (ct.startsWith("text/plain")) {
+ const text = await response.text();
+ result.data = text as string;
+ }
+ } catch {
+ // Ignored
+ }
+ }
+
+ return result;
+}
+
+export async function http_get(url: string, timeout = -1, skip_401_check = false, abort_signal: AbortSignal = undefined): Promise<IInternalFetchResponse> {
+ const init = {
+ method: "get",
+ } as RequestInit;
+
+ if (abort_signal) {
+ init.signal = abort_signal;
+ }
+
+ const response = await internal_fetch({url, init, timeout});
+ const result = {} as IInternalFetchResponse;
+
+ if (!skip_401_check && await is_401(response)) return result;
+
+ result.ok = response.ok;
+ result.status = response.status;
+ result.http_response = response;
+
+ if (response.status !== 204) {
+ try {
+ const ct = response.headers.get("Content-Type")?.toString() ?? "";
+ if (ct.startsWith("application/json")) {
+ const data = await response.json();
+ result.data = resolve_references(data);
+ } else if (ct.startsWith("text/plain")) {
+ const text = await response.text();
+ result.data = text as string;
+ }
+ } catch {
+ // Ignored
+ }
+ }
+
+ return result;
+}
+
+export async function http_delete(url: string, body?: object|string, timeout = -1, skip_401_check = false, abort_signal: AbortSignal = undefined): Promise<IInternalFetchResponse> {
+ const init = {
+ method: "delete",
+ } as RequestInit;
+
+ if (abort_signal) {
+ init.signal = abort_signal;
+ }
+
+ if (body) {
+ init.headers = {
+ "Content-Type": "application/json;charset=UTF-8",
+ };
+ init.body = JSON.stringify(body);
+ }
+
+ const response = await internal_fetch({url, init, timeout});
+ const result = {} as IInternalFetchResponse;
+
+ if (!skip_401_check && await is_401(response)) return result;
+
+ result.ok = response.ok;
+ result.status = response.status;
+ result.http_response = response;
+
+ if (response.status !== 204) {
+ try {
+ const ct = response.headers.get("Content-Type")?.toString() ?? "";
+ if (ct.startsWith("application/json")) {
+ const data = await response.json();
+ result.data = resolve_references(data);
+ } else if (ct.startsWith("text/plain")) {
+ const text = await response.text();
+ result.data = text as string;
+ }
+ } catch (error) {
+ // ignored
+ }
+ }
+
+ return result;
+}
+
+async function internal_fetch(request: IInternalFetchRequest): Promise<Response> {
+ if (!request.init) request.init = {};
+ request.init.credentials = "include";
+ request.init.headers = {
+ "X-TimeZone": Temporal.Now.timeZone().id,
+ ...request.init.headers
+ };
+
+ const fetch_request = new Request(request.url, request.init);
+ let response: any;
+
+ try {
+ if (request.timeout > 500) {
+ response = await Promise.race([
+ fetch(fetch_request),
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), request.timeout))
+ ]);
+ } else {
+ response = await fetch(fetch_request);
+ }
+ } catch (error) {
+ if (error.message === "Timeout") {
+ console.error("Request timed out");
+ } else if (error.message === "Network request failed") {
+ console.error("No internet connection");
+ } else {
+ throw error; // rethrow other unexpected errors
+ }
+ }
+
+ return response;
+}
+
+async function is_401(response: Response): Promise<boolean> {
+ if (response.status === 401) {
+ clear_session_data();
+ await replace("/login");
+ return true;
+ }
+ return false;
+}
diff --git a/apps/web-shared/src/lib/api/root.ts b/apps/web-shared/src/lib/api/root.ts
new file mode 100644
index 0000000..d65efc4
--- /dev/null
+++ b/apps/web-shared/src/lib/api/root.ts
@@ -0,0 +1,6 @@
+import {http_post} from "$shared/lib/api/internal-fetch";
+import {api_base} from "$shared/lib/configuration";
+
+export function server_log(message: string): void {
+ http_post(api_base("_/api/log"), message);
+}
diff --git a/apps/web-shared/src/lib/api/time-entry.ts b/apps/web-shared/src/lib/api/time-entry.ts
new file mode 100644
index 0000000..e81329d
--- /dev/null
+++ b/apps/web-shared/src/lib/api/time-entry.ts
@@ -0,0 +1,86 @@
+import {api_base} from "$shared/lib/configuration";
+import {is_guid} from "$shared/lib/helpers";
+import {http_delete, http_get, http_post} from "./internal-fetch";
+import type {TimeCategoryDto} from "$shared/lib/models/TimeCategoryDto";
+import type {TimeLabelDto} from "$shared/lib/models/TimeLabelDto";
+import type {TimeEntryDto} from "$shared/lib/models/TimeEntryDto";
+import type {TimeEntryQuery} from "$shared/lib/models/TimeEntryQuery";
+import type {IInternalFetchResponse} from "$shared/lib/models/IInternalFetchResponse";
+
+
+// ENTRIES
+
+export async function create_time_entry(payload: TimeEntryDto): Promise<IInternalFetchResponse> {
+ return http_post(api_base("v1/entries/create"), payload);
+}
+
+export async function get_time_entry(entryId: string): Promise<IInternalFetchResponse> {
+ if (is_guid(entryId)) {
+ return http_get(api_base("v1/entries/" + entryId));
+ }
+ throw new Error("entryId is not a valid guid.");
+}
+
+export async function get_time_entries(entryQuery: TimeEntryQuery): Promise<IInternalFetchResponse> {
+ return http_post(api_base("v1/entries/query"), entryQuery);
+}
+
+export async function delete_time_entry(id: string): Promise<IInternalFetchResponse> {
+ if (!is_guid(id)) throw new Error("id is not a valid guid");
+ return http_delete(api_base("v1/entries/" + id + "/delete"));
+}
+
+export async function update_time_entry(entryDto: TimeEntryDto): Promise<IInternalFetchResponse> {
+ if (!is_guid(entryDto.id ?? "")) throw new Error("id is not a valid guid");
+ if (!entryDto.category) throw new Error("category is empty");
+ if (!entryDto.stop) throw new Error("stop is empty");
+ if (!entryDto.start) throw new Error("start is empty");
+ return http_post(api_base("v1/entries/update"), entryDto);
+}
+
+// LABELS
+
+
+export async function create_time_label(labelDto: TimeLabelDto): Promise<IInternalFetchResponse> {
+ return http_post(api_base("v1/labels/create"), labelDto);
+}
+
+export async function get_time_labels(): Promise<IInternalFetchResponse> {
+ return http_get(api_base("v1/labels"));
+}
+
+export async function delete_time_label(id: string): Promise<IInternalFetchResponse> {
+ if (!is_guid(id)) throw new Error("id is not a valid guid");
+ return http_delete(api_base("v1/labels/" + id + "/delete"));
+}
+
+export async function update_time_label(labelDto: TimeLabelDto): Promise<IInternalFetchResponse> {
+ if (!is_guid(labelDto.id ?? "")) throw new Error("id is not a valid guid");
+ if (!labelDto.name) throw new Error("name is empty");
+ if (!labelDto.color) throw new Error("color is empty");
+ return http_post(api_base("v1/labels/update"), labelDto);
+}
+
+
+// CATEGORIES
+export async function create_time_category(category: TimeCategoryDto): Promise<IInternalFetchResponse> {
+ if (!category.name) throw new Error("name is empty");
+ if (!category.color) throw new Error("color is empty");
+ return http_post(api_base("v1/categories/create"), category);
+}
+
+export async function get_time_categories(): Promise<IInternalFetchResponse> {
+ return http_get(api_base("v1/categories"));
+}
+
+export async function delete_time_category(id: string): Promise<IInternalFetchResponse> {
+ if (!is_guid(id)) throw new Error("id is not a valid guid");
+ return http_delete(api_base("v1/categories/" + id + "/delete"));
+}
+
+export async function update_time_category(category: TimeCategoryDto): Promise<IInternalFetchResponse> {
+ if (!is_guid(category.id ?? "")) throw new Error("id is not a valid guid");
+ if (!category.name) throw new Error("name is empty");
+ if (!category.color) throw new Error("color is empty");
+ return http_post(api_base("v1/categories/update"), category);
+}
diff --git a/apps/web-shared/src/lib/api/user.ts b/apps/web-shared/src/lib/api/user.ts
new file mode 100644
index 0000000..a3a149e
--- /dev/null
+++ b/apps/web-shared/src/lib/api/user.ts
@@ -0,0 +1,47 @@
+import {api_base} from "$shared/lib/configuration";
+import {http_delete, http_get, http_post} from "./internal-fetch";
+import type {LoginPayload} from "$shared/lib/models/LoginPayload";
+import type {UpdateProfilePayload} from "$shared/lib/models/UpdateProfilePayload";
+import type {CreateAccountPayload} from "$shared/lib/models/CreateAccountPayload";
+import type {IInternalFetchResponse} from "$shared/lib/models/IInternalFetchResponse";
+
+export async function login(payload: LoginPayload): Promise<IInternalFetchResponse> {
+ return http_post(api_base("_/account/login"), payload);
+}
+
+export async function logout(): Promise<IInternalFetchResponse> {
+ return http_get(api_base("_/account/logout"));
+}
+
+export async function create_forgot_password_request(username: string): Promise<IInternalFetchResponse> {
+ if (!username) throw new Error("Username is empty");
+ return http_get(api_base("_/forgot-password-requests/create?username=" + username));
+}
+
+export async function check_forgot_password_request(public_id: string): Promise<IInternalFetchResponse> {
+ if (!public_id) throw new Error("Id is empty");
+ return http_get(api_base("_/forgot-password-requests/is-valid?id=" + public_id));
+}
+
+export async function fulfill_forgot_password_request(public_id: string, newPassword: string): Promise<IInternalFetchResponse> {
+ if (!public_id) throw new Error("Id is empty");
+ return http_post(api_base("_/forgot-password-requests/fulfill"), {id: public_id, newPassword});
+}
+
+export async function delete_account(): Promise<IInternalFetchResponse> {
+ return http_delete(api_base("_/account/delete"));
+}
+
+export async function update_profile(payload: UpdateProfilePayload): Promise<IInternalFetchResponse> {
+ if (!payload.password && !payload.username) throw new Error("Password and Username is empty");
+ return http_post(api_base("_/account/update"), payload);
+}
+
+export async function create_account(payload: CreateAccountPayload): Promise<IInternalFetchResponse> {
+ if (!payload.password && !payload.username) throw new Error("Password and Username is empty");
+ return http_post(api_base("_/account/create"), payload);
+}
+
+export async function get_profile_for_active_check(): Promise<IInternalFetchResponse> {
+ return http_get(api_base("_/account"), 0, true);
+}
diff --git a/apps/web-shared/src/lib/colors.ts b/apps/web-shared/src/lib/colors.ts
new file mode 100644
index 0000000..c2da03d
--- /dev/null
+++ b/apps/web-shared/src/lib/colors.ts
@@ -0,0 +1,47 @@
+export function generate_random_hex_color(skip_contrast_check = false) {
+ let hex = __generate_random_hex_color();
+ if (skip_contrast_check) return hex;
+ while ((__calculate_contrast_ratio("#ffffff", hex) < 4.5) || (__calculate_contrast_ratio("#000000", hex) < 4.5)) {
+ hex = __generate_random_hex_color();
+ }
+
+ return hex;
+}
+
+// Largely copied from chroma js api
+function __generate_random_hex_color(): string {
+ let code = "#";
+ for (let i = 0; i < 6; i++) {
+ code += "0123456789abcdef".charAt(Math.floor(Math.random() * 16));
+ }
+ return code;
+}
+
+function __calculate_contrast_ratio(hex1: string, hex2: string): number {
+ const rgb1 = __hex_to_rgb(hex1);
+ const rgb2 = __hex_to_rgb(hex2);
+ const l1 = __get_luminance(rgb1[0], rgb1[1], rgb1[2]);
+ const l2 = __get_luminance(rgb2[0], rgb2[1], rgb2[2]);
+ const result = l1 > l2 ? (l1 + 0.05) / (l2 + 0.05) : (l2 + 0.05) / (l1 + 0.05);
+ return result;
+}
+
+function __hex_to_rgb(hex: string): number[] {
+ if (!hex.match(/^#([A-Fa-f0-9]{6})$/)) return [];
+ if (hex[0] === "#") hex = hex.substring(1, hex.length);
+ return [parseInt(hex.substring(0, 2), 16), parseInt(hex.substring(2, 4), 16), parseInt(hex.substring(4, 6), 16)];
+}
+
+function __get_luminance(r, g, b) {
+ // relative luminance
+ // see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+ r = __luminance_x(r);
+ g = __luminance_x(g);
+ b = __luminance_x(b);
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
+}
+
+function __luminance_x(x) {
+ x /= 255;
+ return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
+}
diff --git a/apps/web-shared/src/lib/configuration.ts b/apps/web-shared/src/lib/configuration.ts
new file mode 100644
index 0000000..f597bb4
--- /dev/null
+++ b/apps/web-shared/src/lib/configuration.ts
@@ -0,0 +1,60 @@
+export const API_ADDRESS = "https://api.dev.greatoffice.life";
+export const PROJECTS_ADDRESS = "https://projects.dev.greatoffice.life";
+export const ACCOUNTS_ADDRESS = "https://a.dev.greatoffice.life";
+export const FRONTPAGE_ADDRESS = "https://greatoffice.life";
+export const DEV_ACCOUNTS_ADDRESS = "http://localhost:3001";
+export const DEV_FRONTPAGE_ADDRESS = "http://localhost:3002";
+export const DEV_API_ADDRESS = "http://localhost:5000";
+export const DEV_PROJECTS_ADDRESS = "http://localhost:3000";
+export const SECONDS_BETWEEN_SESSION_CHECK = 600;
+
+export function projects_base(path: string): string {
+ return (is_development() ? DEV_PROJECTS_ADDRESS : PROJECTS_ADDRESS) + (path ? "/" + path : "/");
+}
+
+export function frontpage_base(path: string): string {
+ return (is_development() ? DEV_FRONTPAGE_ADDRESS : FRONTPAGE_ADDRESS) + (path ? "/" + path : "/");
+}
+
+export function accounts_base(path: string): string {
+ return (is_development() ? DEV_ACCOUNTS_ADDRESS : ACCOUNTS_ADDRESS) + (path ? "/" + path : "/");
+}
+
+export function api_base(path: string): string {
+ return (is_development() ? DEV_API_ADDRESS : API_ADDRESS) + (path ? "/" + path : "/");
+}
+
+export function is_development(): boolean {
+ // @ts-ignore
+ return import.meta.env.DEV;
+}
+
+export function is_debug(): boolean {
+ return localStorage.getItem(StorageKeys.debug) !== "true";
+}
+
+export const IconNames = {
+ github: "github",
+ verticalDots: "verticalDots",
+ clock: "clock",
+ trash: "trash",
+ pencilSquare: "pencilSquare",
+ x: "x",
+ funnel: "funnel",
+ funnelFilled: "funnelFilled",
+ refresh: "refresh",
+ resetHard: "resetHard",
+ arrowUp: "arrowUp",
+ arrowDown: "arrowDown",
+ chevronDown: "chevronDown"
+};
+
+export const StorageKeys = {
+ session: "sessionData",
+ theme: "theme",
+ debug: "debug",
+ categories: "categories",
+ labels: "labels",
+ entries: "entries",
+ stopwatch: "stopwatchState"
+};
diff --git a/apps/web-shared/src/lib/helpers.ts b/apps/web-shared/src/lib/helpers.ts
new file mode 100644
index 0000000..650bccf
--- /dev/null
+++ b/apps/web-shared/src/lib/helpers.ts
@@ -0,0 +1,489 @@
+import {StorageKeys} from "$shared/lib/configuration";
+import {TimeEntryDto} from "$shared/lib/models/TimeEntryDto";
+import {UnwrappedEntryDateTime} from "$shared/lib/models/UnwrappedEntryDateTime";
+import {Temporal} from "@js-temporal/polyfill";
+
+export const EMAIL_REGEX = new RegExp(/^([a-z0-9]+(?:([._\-])[a-z0-9]+)*@(?:[a-z0-9]+(?:(-)[a-z0-9]+)?\.)+[a-z0-9](?:[a-z0-9]*[a-z0-9])?)$/i);
+export const URL_REGEX = new RegExp(/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-.][a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/gm);
+export const GUID_REGEX = new RegExp(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
+export const NORWEGIAN_PHONE_NUMBER_REGEX = new RegExp(/(0047|\+47|47)?\d{8,12}/);
+
+export function is_email(value: string): boolean {
+ return EMAIL_REGEX.test(String(value).toLowerCase());
+}
+
+export function is_url(value: string): boolean {
+ return URL_REGEX.test(String(value).toLowerCase());
+}
+
+export function is_norwegian_phone_number(value: string): boolean {
+ if (value.length < 8 || value.length > 12) {
+ return false;
+ }
+ return NORWEGIAN_PHONE_NUMBER_REGEX.test(String(value));
+}
+
+export function switch_theme() {
+ const html = document.querySelector("html");
+ if (html) {
+ if (html.dataset.theme === "dark") {
+ html.dataset.theme = "light";
+ } else {
+ html.dataset.theme = "dark";
+ }
+ localStorage.setItem(StorageKeys.theme, html.dataset.theme);
+ }
+}
+
+export function unwrap_date_time_from_entry(entry: TimeEntryDto): UnwrappedEntryDateTime {
+ if (!entry) throw new Error("entry was undefined");
+ const currentTimeZone = Temporal.Now.timeZone().id;
+ const startInstant = Temporal.Instant.from(entry.start).toZonedDateTimeISO(currentTimeZone);
+ const stopInstant = Temporal.Instant.from(entry.stop).toZonedDateTimeISO(currentTimeZone);
+
+ return {
+ start_date: startInstant.toPlainDate(),
+ stop_date: stopInstant.toPlainDate(),
+ start_time: startInstant.toPlainTime(),
+ stop_time: stopInstant.toPlainTime(),
+ duration: Temporal.Duration.from({
+ hours: stopInstant.hour,
+ minutes: stopInstant.minute
+ }).subtract(Temporal.Duration.from({
+ hours: startInstant.hour,
+ minutes: startInstant.minute
+ }))
+ };
+}
+
+
+export function is_guid(value: string): boolean {
+ if (!value) {
+ return false;
+ }
+ if (value[0] === "{") {
+ value = value.substring(1, value.length - 1);
+ }
+ return GUID_REGEX.test(value);
+}
+
+export function is_empty_object(obj: object): boolean {
+ return obj !== void 0 && Object.keys(obj).length > 0;
+}
+
+export function merge_obj_arr<T>(a: Array<T>, b: Array<T>, props: Array<string>): Array<T> {
+ let start = 0;
+ let merge = [];
+
+ while (start < a.length) {
+
+ if (a[start] === b[start]) {
+ //pushing the merged objects into array
+ merge.push({...a[start], ...b[start]});
+ }
+ //incrementing start value
+ start = start + 1;
+ }
+ return merge;
+}
+
+export function set_favicon(url: string) {
+ // Find the current favicon element
+ const favicon = document.querySelector("link[rel=\"icon\"]") as HTMLLinkElement;
+ if (favicon) {
+ // Update the new link
+ favicon.href = url;
+ } else {
+ // Create new `link`
+ const link = document.createElement("link");
+ link.rel = "icon";
+ link.href = url;
+
+ // Append to the `head` element
+ document.head.appendChild(link);
+ }
+}
+
+export function set_emoji_favicon(emoji: string) {
+ // Create a canvas element
+ const canvas = document.createElement("canvas");
+ canvas.height = 64;
+ canvas.width = 64;
+
+ // Get the canvas context
+ const context = canvas.getContext("2d") as CanvasRenderingContext2D;
+ context.font = "64px serif";
+ context.fillText(emoji, 0, 64);
+
+ // Get the custom URL
+ const url = canvas.toDataURL();
+
+ // Update the favicon
+ set_favicon(url);
+}
+
+
+// https://stackoverflow.com/a/48400665/11961742
+export function seconds_to_hour_minute_string(seconds: number) {
+ const hours = Math.floor(seconds / (60 * 60));
+ seconds -= hours * (60 * 60);
+ const minutes = Math.floor(seconds / 60);
+ return hours + "h" + minutes + "m";
+}
+
+export function get_query_string(params: any = {}): string {
+ const map = Object.keys(params).reduce((arr: Array<string>, key: string) => {
+ if (params[key] !== undefined) {
+ return arr.concat(`${key}=${encodeURIComponent(params[key])}`);
+ }
+ return arr;
+ }, [] as any);
+
+ if (map.length) {
+ return `?${map.join("&")}`;
+ }
+
+ return "";
+}
+
+export function make_url(url: string, params: object): string {
+ return `${url}${get_query_string(params)}`;
+}
+
+export function load_script(url: string) {
+ unload_script(url, () => {
+ return new Promise(function (resolve, reject) {
+ const script = document.createElement("script");
+ script.src = url;
+
+ script.addEventListener("load", function () {
+ // The script is loaded completely
+ resolve(true);
+ });
+
+ document.body.appendChild(script);
+ });
+ });
+}
+
+export function unload_script(src: string, callback?: Function): void {
+ document.querySelectorAll("script[src='" + src + "']").forEach(el => el.remove());
+ if (typeof callback === "function") {
+ callback();
+ }
+}
+
+export function noop() {
+}
+
+export async function run_async(functionToRun: Function): Promise<any> {
+ return new Promise((greatSuccess, graveFailure) => {
+ try {
+ greatSuccess(functionToRun());
+ } catch (exception) {
+ graveFailure(exception);
+ }
+ });
+}
+
+// https://stackoverflow.com/a/45215694/11961742
+export function get_selected_options(domElement: HTMLSelectElement): Array<string> {
+ const ret = [];
+
+ // fast but not universally supported
+ if (domElement.selectedOptions !== undefined) {
+ for (let i = 0; i < domElement.selectedOptions.length; i++) {
+ ret.push(domElement.selectedOptions[i].value);
+ }
+
+ // compatible, but can be painfully slow
+ } else {
+ for (let i = 0; i < domElement.options.length; i++) {
+ if (domElement.options[i].selected) {
+ ret.push(domElement.options[i].value);
+ }
+ }
+ }
+ return ret;
+}
+
+export function uuid_v4(): string {
+ // @ts-ignore
+ return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
+}
+
+export function random_string(length: number): string {
+ if (!length) {
+ throw new Error("length is undefined");
+ }
+ let result = "";
+ const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ const charactersLength = characters.length;
+ for (let i = 0; i < length; i++) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ }
+ return result;
+}
+
+interface CreateElementOptions {
+ name: string,
+ properties?: object,
+ children?: Array<HTMLElement|Function|Node>
+}
+
+export function create_element_from_object(elementOptions: CreateElementOptions): HTMLElement {
+ return create_element(elementOptions.name, elementOptions.properties, elementOptions.children);
+}
+
+export function create_element(name: string, properties?: object, children?: Array<HTMLElement|any>): HTMLElement {
+ if (!name || name.length < 1) {
+ throw new Error("name is required");
+ }
+ const node = document.createElement(name);
+ if (properties) {
+ for (const [key, value] of Object.entries(properties)) {
+ // @ts-ignore
+ node[key] = value;
+ }
+ }
+
+ if (children && children.length > 0) {
+ let actualChildren = children;
+ if (typeof children === "function") {
+ // @ts-ignore
+ actualChildren = children();
+ }
+ for (const child of actualChildren) {
+ node.appendChild(child as Node);
+ }
+ }
+ return node;
+}
+
+export function get_element_position(element: HTMLElement|any) {
+ if (!element) return {x: 0, y: 0};
+ let x = 0;
+ let y = 0;
+ while (true) {
+ x += element.offsetLeft;
+ y += element.offsetTop;
+ if (element.offsetParent === null) {
+ break;
+ }
+ element = element.offsetParent;
+ }
+ return {x, y};
+}
+
+export function restrict_input_to_numbers(element: HTMLElement, specials: Array<string> = [], mergeSpecialsWithDefaults: boolean = false): void {
+ if (element) {
+ element.addEventListener("keydown", (e) => {
+ const defaultSpecials = ["Backspace", "ArrowLeft", "ArrowRight", "Tab",];
+ let keys = specials.length > 0 ? specials : defaultSpecials;
+ if (mergeSpecialsWithDefaults && specials) {
+ keys = [...specials, ...defaultSpecials];
+ }
+ if (keys.indexOf(e.key) !== -1) {
+ return;
+ }
+ if (isNaN(parseInt(e.key))) {
+ e.preventDefault();
+ }
+ });
+ }
+}
+
+export function element_has_focus(element: HTMLElement): boolean {
+ return element === document.activeElement;
+}
+
+export function move_focus(element: HTMLElement): void {
+ if (!element) {
+ element = document.getElementsByTagName("body")[0];
+ }
+ element.focus();
+ // @ts-ignore
+ if (!element_has_focus(element)) {
+ element.setAttribute("tabindex", "-1");
+ element.focus();
+ }
+}
+
+export function get_url_parameter(name: string): string {
+ // @ts-ignore
+ return new RegExp("[?&]" + name + "=([^&#]*)")?.exec(window.location.href)[1];
+}
+
+export function update_url_parameter(param: string, newVal: string): void {
+ let newAdditionalURL = "";
+ let tempArray = location.href.split("?");
+ const baseURL = tempArray[0];
+ const additionalURL = tempArray[1];
+ let temp = "";
+ if (additionalURL) {
+ tempArray = additionalURL.split("&");
+ for (let i = 0; i < tempArray.length; i++) {
+ if (tempArray[i].split("=")[0] !== param) {
+ newAdditionalURL += temp + tempArray[i];
+ temp = "&";
+ }
+ }
+ }
+ const rows_txt = temp + "" + param + "=" + newVal;
+ const newUrl = baseURL + "?" + newAdditionalURL + rows_txt;
+ window.history.replaceState("", "", newUrl);
+}
+
+
+export function get_style_string(rules: CSSRuleList) {
+ let styleString = "";
+ for (const [key, value] of Object.entries(rules)) {
+ styleString += key + ":" + value + ";";
+ }
+ return styleString;
+}
+
+export function get_local_time_zone_date(date: Date): Date {
+ const timeOffsetInMS = new Date().getTimezoneOffset() * 60000;
+ date.setTime(date.getTime() - timeOffsetInMS);
+ return date;
+}
+
+export function parse_iso_local(s: string) {
+ const b = s.split(/\D/);
+ //@ts-ignore
+ return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]);
+}
+
+export function resolve_references(json: any) {
+ if (!json) return;
+ if (typeof json === "string") {
+ json = JSON.parse(json ?? "{}");
+ }
+ const byid = {}, refs = [];
+ json = function recurse(obj, prop, parent) {
+ if (typeof obj !== "object" || !obj) {
+ return obj;
+ }
+ if (Object.prototype.toString.call(obj) === "[object Array]") {
+ for (let i = 0; i < obj.length; i++) {
+ if (typeof obj[i] !== "object" || !obj[i]) {
+ continue;
+ } else if ("$ref" in obj[i]) {
+ // @ts-ignore
+ obj[i] = recurse(obj[i], i, obj);
+ } else {
+ obj[i] = recurse(obj[i], prop, obj);
+ }
+ }
+ return obj;
+ }
+ if ("$ref" in obj) {
+ let ref = obj.$ref;
+ if (ref in byid) {
+ // @ts-ignore
+ return byid[ref];
+ }
+ refs.push([parent, prop, ref]);
+ return;
+ } else if ("$id" in obj) {
+ let id = obj.$id;
+ delete obj.$id;
+ if ("$values" in obj) {
+ obj = obj.$values.map(recurse);
+ } else {
+ for (let prop2 in obj) {
+ // @ts-ignore
+ obj[prop2] = recurse(obj[prop2], prop2, obj);
+ }
+ }
+ // @ts-ignore
+ byid[id] = obj;
+ }
+ return obj;
+ }(json);
+ for (let i = 0; i < refs.length; i++) {
+ let ref = refs[i];
+ // @ts-ignore
+ ref[0][ref[1]] = byid[ref[2]];
+ }
+ return json;
+}
+
+export function to_readable_date_string(date: Date, locale = "nb-NO"): string {
+ date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
+ return date.toLocaleString(locale);
+}
+
+export function get_random_int(min: number, max: number): number {
+ min = Math.ceil(min);
+ max = Math.floor(max);
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+export function to_readable_bytes(bytes: number): string {
+ const s = ["bytes", "kB", "MB", "GB", "TB", "PB"];
+ const e = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, e)).toFixed(2) + " " + s[e];
+}
+
+export function can_use_dom(): boolean {
+ return !!(typeof window !== "undefined" && window.document && window.document.createElement);
+}
+
+export function session_storage_remove_regex(regex: RegExp): void {
+ let n = sessionStorage.length;
+ while (n--) {
+ const key = sessionStorage.key(n);
+ if (key && regex.test(key)) {
+ sessionStorage.removeItem(key);
+ }
+ }
+}
+
+export function local_storage_remove_regex(regex: RegExp): void {
+ let n = localStorage.length;
+ while (n--) {
+ const key = localStorage.key(n);
+ if (key && regex.test(key)) {
+ localStorage.removeItem(key);
+ }
+ }
+}
+
+export function session_storage_set_json(key: string, value: object): void {
+ sessionStorage.setItem(key, JSON.stringify(value));
+}
+
+export function session_storage_get_json(key: string): object {
+ return JSON.parse(sessionStorage.getItem(key) ?? "{}");
+}
+
+export function local_storage_set_json(key: string, value: object): void {
+ localStorage.setItem(key, JSON.stringify(value));
+}
+
+export function local_storage_get_json(key: string): object {
+ return JSON.parse(localStorage.getItem(key) ?? "{}");
+}
+
+export function get_hash_code(value: string): number|undefined {
+ let hash = 0;
+ if (value.length === 0) {
+ return;
+ }
+ for (let i = 0; i < value.length; i++) {
+ const char = value.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash |= 0;
+ }
+ return hash;
+}
+
+export function $(selector: string): HTMLElement|null {
+ return document.querySelector(selector);
+}
+
+export function $$(selector: string): NodeListOf<Element> {
+ return document.querySelectorAll(selector);
+}
diff --git a/apps/web-shared/src/lib/models/CreateAccountPayload.ts b/apps/web-shared/src/lib/models/CreateAccountPayload.ts
new file mode 100644
index 0000000..d116308
--- /dev/null
+++ b/apps/web-shared/src/lib/models/CreateAccountPayload.ts
@@ -0,0 +1,4 @@
+export interface CreateAccountPayload {
+ username: string,
+ password: string
+}
diff --git a/apps/web-shared/src/lib/models/ErrorResult.ts b/apps/web-shared/src/lib/models/ErrorResult.ts
new file mode 100644
index 0000000..7c70017
--- /dev/null
+++ b/apps/web-shared/src/lib/models/ErrorResult.ts
@@ -0,0 +1,4 @@
+export interface ErrorResult {
+ title: string,
+ text: string
+}
diff --git a/apps/web-shared/src/lib/models/IInternalFetchRequest.ts b/apps/web-shared/src/lib/models/IInternalFetchRequest.ts
new file mode 100644
index 0000000..68505e2
--- /dev/null
+++ b/apps/web-shared/src/lib/models/IInternalFetchRequest.ts
@@ -0,0 +1,6 @@
+export interface IInternalFetchRequest {
+ url: string,
+ init?: RequestInit,
+ timeout?: number
+ retry_count?: number
+}
diff --git a/apps/web-shared/src/lib/models/IInternalFetchResponse.ts b/apps/web-shared/src/lib/models/IInternalFetchResponse.ts
new file mode 100644
index 0000000..6c91b35
--- /dev/null
+++ b/apps/web-shared/src/lib/models/IInternalFetchResponse.ts
@@ -0,0 +1,6 @@
+export interface IInternalFetchResponse {
+ ok: boolean,
+ status: number,
+ data: any,
+ http_response: Response
+}
diff --git a/apps/web-shared/src/lib/models/ISession.ts b/apps/web-shared/src/lib/models/ISession.ts
new file mode 100644
index 0000000..f7ed46b
--- /dev/null
+++ b/apps/web-shared/src/lib/models/ISession.ts
@@ -0,0 +1,7 @@
+export interface ISession {
+ profile: {
+ username: string,
+ id: string,
+ },
+ lastChecked: number,
+} \ No newline at end of file
diff --git a/apps/web-shared/src/lib/models/IValidationResult.ts b/apps/web-shared/src/lib/models/IValidationResult.ts
new file mode 100644
index 0000000..9a21b13
--- /dev/null
+++ b/apps/web-shared/src/lib/models/IValidationResult.ts
@@ -0,0 +1,31 @@
+export interface IValidationResult {
+ errors: Array<IValidationError>,
+ has_errors: Function,
+ add_error: Function,
+ remove_error: Function,
+}
+
+export interface IValidationError {
+ _id?: string,
+ title: string,
+ text?: string
+}
+
+export default class ValidationResult implements IValidationResult {
+ errors: IValidationError[]
+ has_errors(): boolean {
+ return this.errors?.length > 0;
+ }
+ add_error(prop: string, error: IValidationError): void {
+ if (!this.errors) this.errors = [];
+ error._id = prop;
+ this.errors.push(error);
+ }
+ remove_error(property: string): void {
+ const new_errors = [];
+ for (const error of this.errors) {
+ if (error._id != property) new_errors.push(error)
+ }
+ this.errors = new_errors;
+ }
+}
diff --git a/apps/web-shared/src/lib/models/LoginPayload.ts b/apps/web-shared/src/lib/models/LoginPayload.ts
new file mode 100644
index 0000000..ccd9bed
--- /dev/null
+++ b/apps/web-shared/src/lib/models/LoginPayload.ts
@@ -0,0 +1,4 @@
+export interface LoginPayload {
+ username: string,
+ password: string
+}
diff --git a/apps/web-shared/src/lib/models/TimeCategoryDto.ts b/apps/web-shared/src/lib/models/TimeCategoryDto.ts
new file mode 100644
index 0000000..875e8cb
--- /dev/null
+++ b/apps/web-shared/src/lib/models/TimeCategoryDto.ts
@@ -0,0 +1,9 @@
+import { Temporal } from "@js-temporal/polyfill";
+
+export interface TimeCategoryDto {
+ selected?: boolean;
+ id?: string,
+ modified_at?: Temporal.PlainDate,
+ name?: string,
+ color?: string
+}
diff --git a/apps/web-shared/src/lib/models/TimeEntryDto.ts b/apps/web-shared/src/lib/models/TimeEntryDto.ts
new file mode 100644
index 0000000..71fe7a3
--- /dev/null
+++ b/apps/web-shared/src/lib/models/TimeEntryDto.ts
@@ -0,0 +1,13 @@
+import type { TimeLabelDto } from "./TimeLabelDto";
+import type { TimeCategoryDto } from "./TimeCategoryDto";
+import { Temporal } from "@js-temporal/polyfill";
+
+export interface TimeEntryDto {
+ id: string,
+ modified_at?: Temporal.PlainDate,
+ start: string,
+ stop: string,
+ description: string,
+ labels?: Array<TimeLabelDto>,
+ category: TimeCategoryDto,
+}
diff --git a/apps/web-shared/src/lib/models/TimeEntryQuery.ts b/apps/web-shared/src/lib/models/TimeEntryQuery.ts
new file mode 100644
index 0000000..6681c79
--- /dev/null
+++ b/apps/web-shared/src/lib/models/TimeEntryQuery.ts
@@ -0,0 +1,27 @@
+import type { TimeCategoryDto } from "./TimeCategoryDto";
+import type { TimeLabelDto } from "./TimeLabelDto";
+import type { Temporal } from "@js-temporal/polyfill";
+
+export interface TimeEntryQuery {
+ duration: TimeEntryQueryDuration,
+ categories?: Array<TimeCategoryDto>,
+ labels?: Array<TimeLabelDto>,
+ dateRange?: TimeEntryQueryDateRange,
+ specificDate?: Temporal.PlainDateTime
+ page: number,
+ pageSize: number
+}
+
+export interface TimeEntryQueryDateRange {
+ from: Temporal.PlainDateTime,
+ to: Temporal.PlainDateTime
+}
+
+export enum TimeEntryQueryDuration {
+ TODAY = 0,
+ THIS_WEEK = 1,
+ THIS_MONTH = 2,
+ THIS_YEAR = 3,
+ SPECIFIC_DATE = 4,
+ DATE_RANGE = 5,
+}
diff --git a/apps/web-shared/src/lib/models/TimeLabelDto.ts b/apps/web-shared/src/lib/models/TimeLabelDto.ts
new file mode 100644
index 0000000..2b42d07
--- /dev/null
+++ b/apps/web-shared/src/lib/models/TimeLabelDto.ts
@@ -0,0 +1,8 @@
+import { Temporal } from "@js-temporal/polyfill";
+
+export interface TimeLabelDto {
+ id?: string,
+ modified_at?: Temporal.PlainDate,
+ name?: string,
+ color?: string
+}
diff --git a/apps/web-shared/src/lib/models/TimeQueryDto.ts b/apps/web-shared/src/lib/models/TimeQueryDto.ts
new file mode 100644
index 0000000..607c51e
--- /dev/null
+++ b/apps/web-shared/src/lib/models/TimeQueryDto.ts
@@ -0,0 +1,29 @@
+import type { TimeEntryDto } from "./TimeEntryDto";
+import ValidationResult, { IValidationResult } from "./IValidationResult";
+
+export interface ITimeQueryDto {
+ results: Array<TimeEntryDto>,
+ page: number,
+ pageSize: number,
+ totalRecords: number,
+ totalPageCount: number,
+ is_valid: Function
+}
+
+export class TimeQueryDto implements ITimeQueryDto {
+ results: TimeEntryDto[];
+ page: number;
+ pageSize: number;
+ totalRecords: number;
+ totalPageCount: number;
+
+ is_valid(): IValidationResult {
+ const result = new ValidationResult();
+ if (this.page < 0) {
+ result.add_error("page", {
+ title: "Page cannot be less than zero",
+ })
+ }
+ return result;
+ }
+}
diff --git a/apps/web-shared/src/lib/models/UnwrappedEntryDateTime.ts b/apps/web-shared/src/lib/models/UnwrappedEntryDateTime.ts
new file mode 100644
index 0000000..e6022d8
--- /dev/null
+++ b/apps/web-shared/src/lib/models/UnwrappedEntryDateTime.ts
@@ -0,0 +1,9 @@
+import {Temporal} from "@js-temporal/polyfill";
+
+export interface UnwrappedEntryDateTime {
+ start_date: Temporal.PlainDate,
+ stop_date: Temporal.PlainDate,
+ start_time: Temporal.PlainTime,
+ stop_time: Temporal.PlainTime,
+ duration: Temporal.Duration,
+}
diff --git a/apps/web-shared/src/lib/models/UpdateProfilePayload.ts b/apps/web-shared/src/lib/models/UpdateProfilePayload.ts
new file mode 100644
index 0000000..d2983ff
--- /dev/null
+++ b/apps/web-shared/src/lib/models/UpdateProfilePayload.ts
@@ -0,0 +1,4 @@
+export interface UpdateProfilePayload {
+ username?: string,
+ password?: string,
+}
diff --git a/apps/web-shared/src/lib/persistent-store.ts b/apps/web-shared/src/lib/persistent-store.ts
new file mode 100644
index 0000000..922f3ab
--- /dev/null
+++ b/apps/web-shared/src/lib/persistent-store.ts
@@ -0,0 +1,102 @@
+import { writable as _writable, readable as _readable, } from "svelte/store";
+import type { Writable, Readable, StartStopNotifier } from "svelte/store";
+
+enum StoreType {
+ SESSION = 0,
+ LOCAL = 1
+}
+
+interface StoreOptions {
+ store?: StoreType;
+}
+
+const default_store_options = {
+ store: StoreType.SESSION
+} as StoreOptions;
+
+interface WritableStore<T> {
+ name: string,
+ initialState: T,
+ options?: StoreOptions
+}
+
+interface ReadableStore<T> {
+ name: string,
+ initialState: T,
+ callback: StartStopNotifier<any>,
+ options?: StoreOptions
+}
+
+function get_store(type: StoreType): Storage {
+ switch (type) {
+ case StoreType.SESSION:
+ return window.sessionStorage;
+ case StoreType.LOCAL:
+ return window.localStorage;
+ }
+}
+
+function prepared_store_value(value: any): string {
+ try {
+ return JSON.stringify(value);
+ } catch (e) {
+ console.error(e);
+ return "__INVALID__";
+ }
+}
+
+function get_store_value<T>(options: WritableStore<T> | ReadableStore<T>): any {
+ try {
+ const storage = get_store(options.options.store);
+ const value = storage.getItem(options.name);
+ if (!value) return false;
+ return JSON.parse(value);
+ } catch (e) {
+ console.error(e);
+ return { __INVALID__: true };
+ }
+}
+
+function hydrate<T>(store: Writable<T>, options: WritableStore<T> | ReadableStore<T>): void {
+ const value = get_store_value<T>(options);
+ if (value && store.set) store.set(value);
+}
+
+function subscribe<T>(store: Writable<T> | Readable<T>, options: WritableStore<T> | ReadableStore<T>): void {
+ const storage = get_store(options.options.store);
+ if (!store.subscribe) return;
+ store.subscribe((state: any) => {
+ storage.setItem(options.name, prepared_store_value(state));
+ });
+}
+
+function writable_persistent<T>(options: WritableStore<T>): Writable<T> {
+ if (options.options === undefined) options.options = default_store_options;
+ console.log("Creating writable store with options: ", options);
+ const store = _writable<T>(options.initialState);
+ hydrate(store, options);
+ subscribe(store, options);
+ return store;
+}
+
+function readable_persistent<T>(options: ReadableStore<T>): Readable<T> {
+ if (options.options === undefined) options.options = default_store_options;
+ console.log("Creating readable store with options: ", options);
+ const store = _readable<T>(options.initialState, options.callback);
+ // hydrate(store, options);
+ subscribe(store, options);
+ return store;
+}
+
+export {
+ writable_persistent,
+ readable_persistent,
+ StoreType
+};
+
+export type {
+ WritableStore,
+ ReadableStore,
+ StoreOptions
+};
+
diff --git a/apps/web-shared/src/lib/session.ts b/apps/web-shared/src/lib/session.ts
new file mode 100644
index 0000000..4f40a17
--- /dev/null
+++ b/apps/web-shared/src/lib/session.ts
@@ -0,0 +1,62 @@
+import {Temporal} from "@js-temporal/polyfill";
+import {get_profile_for_active_check} from "./api/user";
+import {is_guid, session_storage_get_json, session_storage_set_json} from "./helpers";
+import {SECONDS_BETWEEN_SESSION_CHECK, StorageKeys} from "./configuration";
+import type {ISession} from "$shared/lib/models/ISession";
+
+export async function is_active(forceRefresh: boolean = false): Promise<boolean> {
+ const nowEpoch = Temporal.Now.instant().epochSeconds;
+ const data = session_storage_get_json(StorageKeys.session) as ISession;
+ const expiryEpoch = data?.lastChecked + SECONDS_BETWEEN_SESSION_CHECK;
+ const lastCheckIsStaleOrNone = !is_guid(data?.profile?.id) || (expiryEpoch < nowEpoch);
+ if (forceRefresh || lastCheckIsStaleOrNone) {
+ return await call_api();
+ } else {
+ const sessionIsValid = data.profile && is_guid(data.profile.id);
+ if (!sessionIsValid) {
+ clear_session_data();
+ console.log("Session data is not valid");
+ }
+ return sessionIsValid;
+ }
+}
+
+async function call_api(): Promise<boolean> {
+ console.log("Getting profile data while checking session state");
+ try {
+ const response = await get_profile_for_active_check();
+ if (response.ok) {
+ const userData = await response.data;
+ if (is_guid(userData.id) && userData.username) {
+ const session = {
+ profile: userData,
+ lastChecked: Temporal.Now.instant().epochSeconds
+ } as ISession;
+ session_storage_set_json(StorageKeys.session, session);
+ console.log("Successfully got profile data while checking session state");
+ return true;
+ } else {
+ console.error("Api returned invalid data while getting profile data");
+ clear_session_data();
+ return false;
+ }
+ } else {
+ console.error("Api returned unsuccessfully while getting profile data");
+ clear_session_data();
+ return false;
+ }
+ } catch (e) {
+ console.error(e);
+ clear_session_data();
+ return false;
+ }
+}
+
+export function clear_session_data() {
+ session_storage_set_json(StorageKeys.session, {});
+ console.log("Cleared session data.");
+}
+
+export function get_session_data(): ISession {
+ return session_storage_get_json(StorageKeys.session) as ISession;
+}
diff --git a/apps/web-shared/src/styles/_base.scss b/apps/web-shared/src/styles/_base.scss
new file mode 100644
index 0000000..414b440
--- /dev/null
+++ b/apps/web-shared/src/styles/_base.scss
@@ -0,0 +1,48 @@
+// --------------------------------
+
+// Basic Style - Essential CSS rules and utility classes
+
+// --------------------------------
+
+@forward 'base/breakpoints';
+@forward 'base/mixins';
+@forward 'base/grid-layout';
+
+@use 'base/reset';
+@use 'base/colors';
+@use 'base/spacing';
+@use 'base/shared-styles';
+@use 'base/typography';
+@use 'base/icons';
+@use 'base/buttons';
+@use 'base/forms';
+@use 'base/z-index';
+@use 'base/visibility';
+@use 'base/accessibility';
+@use 'base/util';
+
+pre {
+ font-family: monospace !important;
+}
+
+*:focus-visible {
+ outline: 1px auto;
+}
+
+.c-disabled {
+ cursor: not-allowed !important;
+ filter: opacity(.45);
+ pointer-events: none !important;
+
+ &.loading {
+ cursor: wait !important;
+ }
+}
+
+button.btn--state-b {
+ cursor: wait;
+}
+
+button.reset {
+ cursor: pointer !important;
+}
diff --git a/apps/web-shared/src/styles/base/_accessibility.scss b/apps/web-shared/src/styles/base/_accessibility.scss
new file mode 100644
index 0000000..9f71937
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_accessibility.scss
@@ -0,0 +1,17 @@
+.sr-only { // content made available only to screen readers
+ position: absolute;
+ clip: rect(1px, 1px, 1px, 1px);
+ clip-path: inset(50%);
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ padding: 0;
+ border: 0;
+ white-space: nowrap;
+}
+
+.sr-only-focusable { // focusable, visually hidden element
+ &:not(:focus):not(:focus-within){
+ @extend .sr-only
+ }
+} \ No newline at end of file
diff --git a/apps/web-shared/src/styles/base/_breakpoints.scss b/apps/web-shared/src/styles/base/_breakpoints.scss
new file mode 100644
index 0000000..a7f1eda
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_breakpoints.scss
@@ -0,0 +1,15 @@
+$breakpoints: (
+ xs: 32rem, // ~512px
+ sm: 48rem, // ~768px
+ md: 64rem, // ~1024px
+ lg: 80rem, // ~1280px
+ xl: 90rem // ~1440px
+) !default;
+
+@mixin breakpoint($breakpoint, $logic: false) {
+ @if( $logic ) {
+ @media #{$logic} and (min-width: map-get($map: $breakpoints, $key: $breakpoint)) { @content; }
+ } @else {
+ @media (min-width: map-get($map: $breakpoints, $key: $breakpoint)) { @content; }
+ }
+}
diff --git a/apps/web-shared/src/styles/base/_buttons.scss b/apps/web-shared/src/styles/base/_buttons.scss
new file mode 100644
index 0000000..2a7ff34
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_buttons.scss
@@ -0,0 +1,24 @@
+// don't modify this file -> edit 📁 custom-style/_buttons.scss to create your custom buttons
+
+.btn { // basic button style
+ position: relative;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ white-space: nowrap;
+ text-decoration: none;
+ font-size: var(--btn-font-size, 1em);
+ padding-top: var(--btn-padding-y, 0.5em);
+ padding-bottom: var(--btn-padding-y, 0.5em);
+ padding-left: var(--btn-padding-x, 0.75em);
+ padding-right: var(--btn-padding-x, 0.75em);
+ border-radius: var(--btn-radius, 0.25em);
+}
+
+// default size variations
+.btn--sm { font-size: var(--btn-font-size-sm, 0.8em); }
+.btn--md { font-size: var(--btn-font-size-md, 1.2em); }
+.btn--lg { font-size: var(--btn-font-size-lg, 1.4em); }
+
+// button with (only) icon
+.btn--icon { padding: var(--btn-padding-y, 0.5em); } \ No newline at end of file
diff --git a/apps/web-shared/src/styles/base/_colors.scss b/apps/web-shared/src/styles/base/_colors.scss
new file mode 100644
index 0000000..f061d9a
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_colors.scss
@@ -0,0 +1,6 @@
+// don't modify this file -> edit 📁 custom-style/_colors.scss to create your color palette
+
+[data-theme] {
+ background-color: var(--color-bg, hsl(0, 0%, 100%));
+ color: var(--color-contrast-high, hsl(210, 7%, 21%));
+} \ No newline at end of file
diff --git a/apps/web-shared/src/styles/base/_forms.scss b/apps/web-shared/src/styles/base/_forms.scss
new file mode 100644
index 0000000..faffddd
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_forms.scss
@@ -0,0 +1,22 @@
+// don't modify this file -> edit 📁 custom-style/_forms.scss to create your custom form elements
+
+.form-control {
+ font-size: var(--form-control-font-size, 1em);
+ padding-top: var(--form-control-padding-y, 0.5em);
+ padding-bottom: var(--form-control-padding-y, 0.5em);
+ padding-left: var(--form-control-padding-x, 0.75em);
+ padding-right: var(--form-control-padding-x, 0.75em);
+ border-radius: var(--form-control-radius, 0.25em);
+}
+
+.form-legend {
+ color: var(--color-contrast-higher, hsl(204, 28%, 7%));
+ line-height: var(--heading-line-height, 1.2);
+ font-size: var(--text-md, 1.125rem);
+ margin-bottom: var(--space-sm);
+}
+
+.form-label {
+ display: inline-block;
+ font-size: var(--text-sm, 0.75rem);
+}
diff --git a/apps/web-shared/src/styles/base/_grid-layout.scss b/apps/web-shared/src/styles/base/_grid-layout.scss
new file mode 100644
index 0000000..bd8b6c9
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_grid-layout.scss
@@ -0,0 +1,261 @@
+@use 'mixins' as *;
+@use 'breakpoints' as *;
+
+// --------------------------------
+
+// Container - center content on x-axis
+
+// --------------------------------
+
+.container {
+ width: calc(100% - 2*var(--component-padding));
+ margin-left: auto;
+ margin-right: auto;
+}
+
+// --------------------------------
+
+// Grid System
+
+// --------------------------------
+
+$grid-columns: 12 !default;
+
+.grid, .flex, .inline-flex {
+ --gap: 0px;
+ --gap-x: var(--gap);
+ --gap-y: var(--gap);
+ gap: var(--gap-y) var(--gap-x);
+
+ > * {
+ --sub-gap: 0px;
+ --sub-gap-x: var(--sub-gap);
+ --sub-gap-y: var(--sub-gap);
+ }
+}
+
+.grid {
+ --grid-columns: 12;
+ display: flex;
+ flex-wrap: wrap;
+
+ > * {
+ flex-basis: 100%;
+ max-width: 100%;
+ min-width: 0;
+ }
+}
+
+/* #region (Safari < 14.1 fallback) */
+@media not all and (min-resolution:.001dpcm) {
+ @supports (not(translate: none)) {
+ .grid, .flex[class*="gap-"], .inline-flex[class*="gap-"] {
+ gap: 0; // reset
+ margin-bottom: calc(-1 * var(--gap-y));
+ margin-left: calc(-1 * var(--gap-x));
+
+ > * {
+ margin-bottom: var(--sub-gap-y);
+ }
+ }
+
+ .grid {
+ --offset: var(--gap-x);
+ --gap-modifier: 0;
+ --offset-modifier: 1;
+
+ > * {
+ margin-left: var(--offset);
+ }
+ }
+
+ .flex[class*="gap-"], .inline-flex[class*="gap-"] {
+ > * {
+ margin-left: var(--sub-gap-x);
+ }
+ }
+ }
+}
+/* #endregion */
+
+.gap-xxxxs { --gap-x: var(--space-xxxxs); --gap-y: var(--space-xxxxs); > * { --sub-gap-x: var(--space-xxxxs); --sub-gap-y: var(--space-xxxxs); }}
+.gap-xxxs { --gap-x: var(--space-xxxs); --gap-y: var(--space-xxxs); > * { --sub-gap-x: var(--space-xxxs); --sub-gap-y: var(--space-xxxs); }}
+.gap-xxs { --gap-x: var(--space-xxs); --gap-y: var(--space-xxs); > * { --sub-gap-x: var(--space-xxs); --sub-gap-y: var(--space-xxs); }}
+.gap-xs { --gap-x: var(--space-xs); --gap-y: var(--space-xs); > * { --sub-gap-x: var(--space-xs); --sub-gap-y: var(--space-xs); }}
+.gap-sm { --gap-x: var(--space-sm); --gap-y: var(--space-sm); > * { --sub-gap-x: var(--space-sm); --sub-gap-y: var(--space-sm); }}
+.gap-md { --gap-x: var(--space-md); --gap-y: var(--space-md); > * { --sub-gap-x: var(--space-md); --sub-gap-y: var(--space-md); }}
+.gap-lg { --gap-x: var(--space-lg); --gap-y: var(--space-lg); > * { --sub-gap-x: var(--space-lg); --sub-gap-y: var(--space-lg); }}
+.gap-xl { --gap-x: var(--space-xl); --gap-y: var(--space-xl); > * { --sub-gap-x: var(--space-xl); --sub-gap-y: var(--space-xl); }}
+.gap-xxl { --gap-x: var(--space-xxl); --gap-y: var(--space-xxl); > * { --sub-gap-x: var(--space-xxl); --sub-gap-y: var(--space-xxl); }}
+.gap-xxxl { --gap-x: var(--space-xxxl); --gap-y: var(--space-xxxl); > * { --sub-gap-x: var(--space-xxxl); --sub-gap-y: var(--space-xxxl); }}
+.gap-xxxxl { --gap-x: var(--space-xxxxl); --gap-y: var(--space-xxxxl); > * { --sub-gap-x: var(--space-xxxxl); --sub-gap-y: var(--space-xxxxl); }}
+.gap-0 { --gap-x: 0; --gap-y: 0; > * { --sub-gap-x: 0; --sub-gap-y: 0; }}
+
+.gap-x-xxxxs { --gap-x: var(--space-xxxxs); > * { --sub-gap-x: var(--space-xxxxs); }}
+.gap-x-xxxs { --gap-x: var(--space-xxxs); > * { --sub-gap-x: var(--space-xxxs); }}
+.gap-x-xxs { --gap-x: var(--space-xxs); > * { --sub-gap-x: var(--space-xxs); }}
+.gap-x-xs { --gap-x: var(--space-xs); > * { --sub-gap-x: var(--space-xs); }}
+.gap-x-sm { --gap-x: var(--space-sm); > * { --sub-gap-x: var(--space-sm); }}
+.gap-x-md { --gap-x: var(--space-md); > * { --sub-gap-x: var(--space-md); }}
+.gap-x-lg { --gap-x: var(--space-lg); > * { --sub-gap-x: var(--space-lg); }}
+.gap-x-xl { --gap-x: var(--space-xl); > * { --sub-gap-x: var(--space-xl); }}
+.gap-x-xxl { --gap-x: var(--space-xxl); > * { --sub-gap-x: var(--space-xxl); }}
+.gap-x-xxxl { --gap-x: var(--space-xxxl); > * { --sub-gap-x: var(--space-xxxl); }}
+.gap-x-xxxxl { --gap-x: var(--space-xxxxl); > * { --sub-gap-x: var(--space-xxxxl); }}
+.gap-x-0 { --gap-x: 0; > * { --sub-gap-x: 0; }}
+
+.gap-y-xxxxs { --gap-y: var(--space-xxxxs); > * { --sub-gap-y: var(--space-xxxxs); }}
+.gap-y-xxxs { --gap-y: var(--space-xxxs); > * { --sub-gap-y: var(--space-xxxs); }}
+.gap-y-xxs { --gap-y: var(--space-xxs); > * { --sub-gap-y: var(--space-xxs); }}
+.gap-y-xs { --gap-y: var(--space-xs); > * { --sub-gap-y: var(--space-xs); }}
+.gap-y-sm { --gap-y: var(--space-sm); > * { --sub-gap-y: var(--space-sm); }}
+.gap-y-md { --gap-y: var(--space-md); > * { --sub-gap-y: var(--space-md); }}
+.gap-y-lg { --gap-y: var(--space-lg); > * { --sub-gap-y: var(--space-lg); }}
+.gap-y-xl { --gap-y: var(--space-xl); > * { --sub-gap-y: var(--space-xl); }}
+.gap-y-xxl { --gap-y: var(--space-xxl); > * { --sub-gap-y: var(--space-xxl); }}
+.gap-y-xxxl { --gap-y: var(--space-xxxl); > * { --sub-gap-y: var(--space-xxxl); }}
+.gap-y-xxxxl { --gap-y: var(--space-xxxxl); > * { --sub-gap-y: var(--space-xxxxl); }}
+.gap-y-0 { --gap-y: 0; > * { --sub-gap-y: 0; }}
+
+$grid-col-class-list: ''; // list of col-{span} classes
+
+@for $i from 1 through $grid-columns {
+ $grid-col-class-list: $grid-col-class-list + ".col-#{$i}";
+ @if($i < $grid-columns) {
+ $grid-col-class-list: $grid-col-class-list + ', ';
+ }
+ .grid-col-#{$i} { --grid-columns: #{$i}; } // set number of grid columns
+ .col-#{$i} { --span: #{$i}; } // set grid item span
+}
+
+#{$grid-col-class-list} {
+ flex-basis: calc(((100% - (var(--grid-columns) - var(--gap-modifier, 1)) * var(--sub-gap-x)) * var(--span) / var(--grid-columns)) + (var(--span) - 1) * var(--sub-gap-x));
+ max-width: calc(((100% - (var(--grid-columns) - var(--gap-modifier, 1)) * var(--sub-gap-x)) * var(--span) / var(--grid-columns)) + (var(--span) - 1) * var(--sub-gap-x));
+}
+
+.col { // auto-expanding column
+ flex-grow: 1;
+ flex-basis: 0;
+ max-width: 100%;
+}
+
+.col-content { // column width depends on its content
+ flex-grow: 0;
+ flex-basis: initial;
+ max-width: initial;
+}
+
+// offset
+$grid-offset-class-list: ''; // list of offset-{span} classes
+
+@for $i from 1 through $grid-columns - 1 {
+ $grid-offset-class-list: $grid-offset-class-list + ".offset-#{$i}";
+ @if($i < $grid-columns) {
+ $grid-offset-class-list: $grid-offset-class-list + ', ';
+ }
+ .offset-#{$i} { --offset: #{$i}; }
+}
+
+#{$grid-offset-class-list} {
+ margin-left: calc(((100% - (var(--grid-columns) - var(--gap-modifier, 1)) * var(--sub-gap-x)) * var(--offset) / var(--grid-columns)) + (var(--offset) + var(--offset-modifier, 0)) * var(--sub-gap-x));
+}
+
+// responsive variations
+@each $breakpoint, $value in $breakpoints {
+ @include breakpoint(#{$breakpoint}) {
+ .gap-xxxxs\@#{$breakpoint} { --gap-x: var(--space-xxxxs); --gap-y: var(--space-xxxxs); > * { --sub-gap-x: var(--space-xxxxs); --sub-gap-y: var(--space-xxxxs); }}
+ .gap-xxxs\@#{$breakpoint} { --gap-x: var(--space-xxxs); --gap-y: var(--space-xxxs); > * { --sub-gap-x: var(--space-xxxs); --sub-gap-y: var(--space-xxxs); }}
+ .gap-xxs\@#{$breakpoint} { --gap-x: var(--space-xxs); --gap-y: var(--space-xxs); > * { --sub-gap-x: var(--space-xxs); --sub-gap-y: var(--space-xxs); }}
+ .gap-xs\@#{$breakpoint} { --gap-x: var(--space-xs); --gap-y: var(--space-xs); > * { --sub-gap-x: var(--space-xs); --sub-gap-y: var(--space-xs); }}
+ .gap-sm\@#{$breakpoint} { --gap-x: var(--space-sm); --gap-y: var(--space-sm); > * { --sub-gap-x: var(--space-sm); --sub-gap-y: var(--space-sm); }}
+ .gap-md\@#{$breakpoint} { --gap-x: var(--space-md); --gap-y: var(--space-md); > * { --sub-gap-x: var(--space-md); --sub-gap-y: var(--space-md); }}
+ .gap-lg\@#{$breakpoint} { --gap-x: var(--space-lg); --gap-y: var(--space-lg); > * { --sub-gap-x: var(--space-lg); --sub-gap-y: var(--space-lg); }}
+ .gap-xl\@#{$breakpoint} { --gap-x: var(--space-xl); --gap-y: var(--space-xl); > * { --sub-gap-x: var(--space-xl); --sub-gap-y: var(--space-xl); }}
+ .gap-xxl\@#{$breakpoint} { --gap-x: var(--space-xxl); --gap-y: var(--space-xxl); > * { --sub-gap-x: var(--space-xxl); --sub-gap-y: var(--space-xxl); }}
+ .gap-xxxl\@#{$breakpoint} { --gap-x: var(--space-xxxl); --gap-y: var(--space-xxxl); > * { --sub-gap-x: var(--space-xxxl); --sub-gap-y: var(--space-xxxl); }}
+ .gap-xxxxl\@#{$breakpoint} { --gap-x: var(--space-xxxxl); --gap-y: var(--space-xxxxl); > * { --sub-gap-x: var(--space-xxxxl); --sub-gap-y: var(--space-xxxxl); }}
+ .gap-0\@#{$breakpoint} { --gap-x: 0; --gap-y: 0; > * { --sub-gap-x: 0; --sub-gap-y: 0; }}
+
+ .gap-x-xxxxs\@#{$breakpoint} { --gap-x: var(--space-xxxxs); > * { --sub-gap-x: var(--space-xxxxs); }}
+ .gap-x-xxxs\@#{$breakpoint} { --gap-x: var(--space-xxxs); > * { --sub-gap-x: var(--space-xxxs); }}
+ .gap-x-xxs\@#{$breakpoint} { --gap-x: var(--space-xxs); > * { --sub-gap-x: var(--space-xxs); }}
+ .gap-x-xs\@#{$breakpoint} { --gap-x: var(--space-xs); > * { --sub-gap-x: var(--space-xs); }}
+ .gap-x-sm\@#{$breakpoint} { --gap-x: var(--space-sm); > * { --sub-gap-x: var(--space-sm); }}
+ .gap-x-md\@#{$breakpoint} { --gap-x: var(--space-md); > * { --sub-gap-x: var(--space-md); }}
+ .gap-x-lg\@#{$breakpoint} { --gap-x: var(--space-lg); > * { --sub-gap-x: var(--space-lg); }}
+ .gap-x-xl\@#{$breakpoint} { --gap-x: var(--space-xl); > * { --sub-gap-x: var(--space-xl); }}
+ .gap-x-xxl\@#{$breakpoint} { --gap-x: var(--space-xxl); > * { --sub-gap-x: var(--space-xxl); }}
+ .gap-x-xxxl\@#{$breakpoint} { --gap-x: var(--space-xxxl); > * { --sub-gap-x: var(--space-xxxl); }}
+ .gap-x-xxxxl\@#{$breakpoint} { --gap-x: var(--space-xxxxl); > * { --sub-gap-x: var(--space-xxxxl); }}
+ .gap-x-0\@#{$breakpoint} { --gap-x: 0; > * { --sub-gap-x: 0; }}
+
+ .gap-y-xxxxs\@#{$breakpoint} { --gap-y: var(--space-xxxxs); > * { --sub-gap-y: var(--space-xxxxs); }}
+ .gap-y-xxxs\@#{$breakpoint} { --gap-y: var(--space-xxxs); > * { --sub-gap-y: var(--space-xxxs); }}
+ .gap-y-xxs\@#{$breakpoint} { --gap-y: var(--space-xxs); > * { --sub-gap-y: var(--space-xxs); }}
+ .gap-y-xs\@#{$breakpoint} { --gap-y: var(--space-xs); > * { --sub-gap-y: var(--space-xs); }}
+ .gap-y-sm\@#{$breakpoint} { --gap-y: var(--space-sm); > * { --sub-gap-y: var(--space-sm); }}
+ .gap-y-md\@#{$breakpoint} { --gap-y: var(--space-md); > * { --sub-gap-y: var(--space-md); }}
+ .gap-y-lg\@#{$breakpoint} { --gap-y: var(--space-lg); > * { --sub-gap-y: var(--space-lg); }}
+ .gap-y-xl\@#{$breakpoint} { --gap-y: var(--space-xl); > * { --sub-gap-y: var(--space-xl); }}
+ .gap-y-xxl\@#{$breakpoint} { --gap-y: var(--space-xxl); > * { --sub-gap-y: var(--space-xxl); }}
+ .gap-y-xxxl\@#{$breakpoint} { --gap-y: var(--space-xxxl); > * { --sub-gap-y: var(--space-xxxl); }}
+ .gap-y-xxxxl\@#{$breakpoint} { --gap-y: var(--space-xxxxl); > * { --sub-gap-y: var(--space-xxxxl); }}
+ .gap-y-0\@#{$breakpoint} { --gap-y: 0; > * { --sub-gap-y: 0; }}
+
+ $grid-col-class-list: ''; // list of col-{span} classes
+
+ @for $i from 1 through $grid-columns {
+ $grid-col-class-list: $grid-col-class-list + ".col-#{$i}\\@#{$breakpoint}";
+ @if($i < $grid-columns) {
+ $grid-col-class-list: $grid-col-class-list + ', ';
+ }
+ .grid-col-#{$i}\@#{$breakpoint} { --grid-columns: #{$i}; } // set number of grid columns
+ .col-#{$i}\@#{$breakpoint} { --span: #{$i}; } // set grid item span
+ }
+
+ #{$grid-col-class-list} {
+ flex-basis: calc(((100% - (var(--grid-columns) - var(--gap-modifier, 1)) * var(--sub-gap-x)) * var(--span) / var(--grid-columns)) + (var(--span) - 1) * var(--sub-gap-x));
+ max-width: calc(((100% - (var(--grid-columns) - var(--gap-modifier, 1)) * var(--sub-gap-x)) * var(--span) / var(--grid-columns)) + (var(--span) - 1) * var(--sub-gap-x));
+ }
+
+ .col\@#{$breakpoint} { // auto-expanding column
+ flex-grow: 1;
+ flex-basis: 0;
+ max-width: 100%;
+ }
+
+ .col-content\@#{$breakpoint} { // column width depends on its content
+ flex-grow: 0;
+ flex-basis: initial;
+ max-width: initial;
+ }
+
+ // offset
+ $grid-offset-class-list: ''; // list of offset-{span} classes
+
+ @for $i from 1 through $grid-columns - 1 {
+ $grid-offset-class-list: $grid-offset-class-list + ".offset-#{$i}\\@#{$breakpoint}";
+ @if($i < $grid-columns) {
+ $grid-offset-class-list: $grid-offset-class-list + ', ';
+ }
+ .offset-#{$i}\@#{$breakpoint} { --offset: #{$i}; }
+ }
+
+ #{$grid-offset-class-list} {
+ margin-left: calc(((100% - (var(--grid-columns) - var(--gap-modifier, 1)) * var(--sub-gap-x)) * var(--offset) / var(--grid-columns)) + (var(--offset) + var(--offset-modifier, 0)) * var(--sub-gap-x));
+ }
+
+ .offset-0\@#{$breakpoint} {
+ margin-left: 0;
+ }
+
+ @media not all and (min-resolution:.001dpcm) {
+ @supports (not(translate: none)) {
+ .offset-0\@#{$breakpoint} {
+ margin-left: var(--gap-x);
+ }
+ }
+ }
+ }
+}
+
diff --git a/apps/web-shared/src/styles/base/_icons.scss b/apps/web-shared/src/styles/base/_icons.scss
new file mode 100644
index 0000000..1674a7c
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_icons.scss
@@ -0,0 +1,62 @@
+// don't modify this file -> edit 📁 custom-style/_icons.scss to set your custom icons style
+
+:root {
+ // default icon sizes
+ --icon-xxxs: 8px;
+ --icon-xxs: 12px;
+ --icon-xs: 16px;
+ --icon-sm: 24px;
+ --icon-md: 32px;
+ --icon-lg: 48px;
+ --icon-xl: 64px;
+ --icon-xxl: 96px;
+ --icon-xxxl: 128px;
+}
+
+.icon {
+ --size: 1em;
+ height: var(--size);
+ width: var(--size);
+ display: inline-block;
+ color: inherit;
+ fill: currentColor;
+ line-height: 1;
+ flex-shrink: 0;
+ max-width: initial;
+}
+
+// icon size
+.icon--xxxs { --size: var(--icon-xxxs); }
+.icon--xxs { --size: var(--icon-xxs); }
+.icon--xs { --size: var(--icon-xs); }
+.icon--sm { --size: var(--icon-sm); }
+.icon--md { --size: var(--icon-md); }
+.icon--lg { --size: var(--icon-lg); }
+.icon--xl { --size: var(--icon-xl); }
+.icon--xxl { --size: var(--icon-xxl); }
+.icon--xxxl { --size: var(--icon-xxxl); }
+
+.icon--is-spinning { // rotate the icon infinitely
+ animation: icon-spin 1s infinite linear;
+}
+
+@keyframes icon-spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+// --------------------------------
+
+// SVG <symbol>
+
+// --------------------------------
+
+// enable icon color corrections
+.icon use {
+ color: inherit;
+ fill: currentColor;
+} \ No newline at end of file
diff --git a/apps/web-shared/src/styles/base/_mixins.scss b/apps/web-shared/src/styles/base/_mixins.scss
new file mode 100644
index 0000000..8fe82f6
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_mixins.scss
@@ -0,0 +1,151 @@
+@use 'sass:math';
+
+// --------------------------------
+
+// Typography
+
+// --------------------------------
+
+// edit font rendering -> tip: use for light text on dark backgrounds
+@mixin fontSmooth {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+// crop top space on text elements - caused by line height
+@mixin lhCrop($line-height, $capital-letter: 1) {
+ &::before {
+ content: '';
+ display: block;
+ height: 0;
+ width: 0;
+ margin-top: calc((#{$capital-letter} - #{$line-height}) * 0.5em);
+ }
+}
+
+// edit text unit on a component level
+@mixin textUnit($text-unit) {
+ --text-unit: #{$text-unit};
+ font-size: var(--text-unit);
+}
+
+// --------------------------------
+
+// Spacing
+
+// --------------------------------
+
+// edit space unit on a component level
+@mixin spaceUnit($space-unit) {
+ --space-unit: #{$space-unit};
+}
+
+// --------------------------------
+
+// Reset
+
+// --------------------------------
+
+// reset user agent style
+@mixin reset {
+ background-color: transparent;
+ padding: 0;
+ border: 0;
+ border-radius: 0;
+ color: inherit;
+ line-height: inherit;
+ appearance: none;
+}
+
+// --------------------------------
+
+// Colors
+
+// --------------------------------
+
+// define HSL color variable
+@mixin defineColorHSL($color, $hue, $saturation, $lightness) {
+ #{$color}: unquote("hsl(#{$hue}, #{$saturation}, #{$lightness})");#{$color}-h: #{$hue};#{$color}-s: #{$saturation};#{$color}-l: #{$lightness};
+}
+
+// return color with different opacity value
+@function alpha($color, $opacity) {
+ $color: str-replace($color, 'var(');
+ $color: str-replace($color, ')');
+ $color-h: var(#{$color+'-h'});
+ $color-s: var(#{$color+'-s'});
+ $color-l: var(#{$color+'-l'});
+ @return hsla($color-h, $color-s, $color-l, $opacity);
+}
+
+// return color with different lightness value
+@function lightness($color, $lightnessMultiplier) {
+ $color: str-replace($color, 'var(');
+ $color: str-replace($color, ')');
+ $color-h: var(#{$color+'-h'});
+ $color-s: var(#{$color+'-s'});
+ $color-l: var(#{$color+'-l'});
+ @return hsl($color-h, $color-s, calc(#{$color-l} * #{$lightnessMultiplier}));
+}
+
+// modify color HSLA values
+@function adjustHSLA($color, $hueMultiplier: 1, $saturationMultiplier: 1, $lightnessMultiplier: 1, $opacity: 1) {
+ $color: str-replace($color, 'var(');
+ $color: str-replace($color, ')');
+ $color-h: var(#{$color+'-h'});
+ $color-s: var(#{$color+'-s'});
+ $color-l: var(#{$color+'-l'});
+ @return hsla(calc(#{$color-h} * #{$hueMultiplier}), calc(#{$color-s} * #{$saturationMultiplier}), calc(#{$color-l} * #{$lightnessMultiplier}), $opacity);
+}
+
+// replace substring with another string
+// credits: https://css-tricks.com/snippets/sass/str-replace-function/
+@function str-replace($string, $search, $replace: '') {
+ $index: str-index($string, $search);
+ @if $index {
+ @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
+ }
+ @return $string;
+}
+
+// --------------------------------
+
+// Accessibility
+
+// --------------------------------
+
+// hide - content made available only to screen readers
+@mixin srHide {
+ position: absolute;
+ clip: rect(1px, 1px, 1px, 1px);
+ clip-path: inset(50%);
+}
+
+// show
+@mixin srShow {
+ position: static;
+ clip: auto;
+ clip-path: none;
+}
+
+// --------------------------------
+
+// CSS Triangle
+
+// --------------------------------
+
+@mixin triangle ($direction: up, $width: 12px, $color: red) {
+ width: 0;
+ height: 0;
+ border: $width solid transparent;
+
+ @if( $direction == left ) {
+ border-right-color: $color;
+ } @else if( $direction == right ) {
+ border-left-color: $color;
+ } @else if( $direction == down ) {
+ border-top-color: $color;
+ } @else {
+ border-bottom-color: $color;
+ }
+}
diff --git a/apps/web-shared/src/styles/base/_reset.scss b/apps/web-shared/src/styles/base/_reset.scss
new file mode 100644
index 0000000..5ba4534
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_reset.scss
@@ -0,0 +1,83 @@
+*, *::after, *::before {
+ box-sizing: inherit;
+}
+
+* {
+ font: inherit;
+}
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video, hr {
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+
+html {
+ box-sizing: border-box;
+}
+
+body {
+ background-color: var(--color-bg, white);
+}
+
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section, main, form legend {
+ display: block;
+}
+
+ol, ul, menu {
+ list-style: none;
+}
+
+blockquote, q {
+ quotes: none;
+}
+
+button, input, textarea, select {
+ margin: 0;
+}
+
+.btn, .form-control, .link, .reset { // reset style of buttons + form controls
+ background-color: transparent;
+ padding: 0;
+ border: 0;
+ border-radius: 0;
+ color: inherit;
+ line-height: inherit;
+ appearance: none;
+}
+
+select.form-control::-ms-expand {
+ display: none; // hide Select default icon on IE
+}
+
+textarea {
+ resize: vertical;
+ overflow: auto;
+ vertical-align: top;
+}
+
+input::-ms-clear {
+ display: none; // hide X icon in IE and Edge
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+img, video, svg {
+ max-width: 100%;
+} \ No newline at end of file
diff --git a/apps/web-shared/src/styles/base/_shared-styles.scss b/apps/web-shared/src/styles/base/_shared-styles.scss
new file mode 100644
index 0000000..dae02fe
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_shared-styles.scss
@@ -0,0 +1,34 @@
+// don't modify this file -> edit 📁 custom-style/_shared-style.scss to set your custom shared styles
+
+:root {
+ // radius
+ --radius-sm: calc(var(--radius, 0.25em)/2);
+ --radius-md: var(--radius, 0.25em);
+ --radius-lg: calc(var(--radius, 0.25em)*2);
+
+ // box shadow
+ --shadow-xs: 0 0.1px 0.3px rgba(0, 0, 0, 0.06),
+ 0 1px 2px rgba(0, 0, 0, 0.12);
+ --shadow-sm: 0 0.3px 0.4px rgba(0, 0, 0, 0.025),
+ 0 0.9px 1.5px rgba(0, 0, 0, 0.05),
+ 0 3.5px 6px rgba(0, 0, 0, 0.1);
+ --shadow-md: 0 0.9px 1.5px rgba(0, 0, 0, 0.03),
+ 0 3.1px 5.5px rgba(0, 0, 0, 0.08),
+ 0 14px 25px rgba(0, 0, 0, 0.12);
+ --shadow-lg: 0 1.2px 1.9px -1px rgba(0, 0, 0, 0.014),
+ 0 3.3px 5.3px -1px rgba(0, 0, 0, 0.038),
+ 0 8.5px 12.7px -1px rgba(0, 0, 0, 0.085),
+ 0 30px 42px -1px rgba(0, 0, 0, 0.15);
+ --shadow-xl: 0 1.5px 2.1px -6px rgba(0, 0, 0, 0.012),
+ 0 3.6px 5.2px -6px rgba(0, 0, 0, 0.035),
+ 0 7.3px 10.6px -6px rgba(0, 0, 0, 0.07),
+ 0 16.2px 21.9px -6px rgba(0, 0, 0, 0.117),
+ 0 46px 60px -6px rgba(0, 0, 0, 0.2);
+
+ // timing functions
+ // credits: https://github.com/ai/easings.net
+ --ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
+ --ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ --ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
+ --ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
+} \ No newline at end of file
diff --git a/apps/web-shared/src/styles/base/_spacing.scss b/apps/web-shared/src/styles/base/_spacing.scss
new file mode 100644
index 0000000..24e6645
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_spacing.scss
@@ -0,0 +1,20 @@
+// don't modify this file -> edit 📁 custom-style/_spacing.scss to set your custom spacing scale
+
+:root {
+ --space-unit: 1rem;
+}
+
+:root, * {
+ --space-xxxxs: calc(0.125 * var(--space-unit));
+ --space-xxxs: calc(0.25 * var(--space-unit));
+ --space-xxs: calc(0.375 * var(--space-unit));
+ --space-xs: calc(0.5 * var(--space-unit));
+ --space-sm: calc(0.75 * var(--space-unit));
+ --space-md: calc(1.25 * var(--space-unit));
+ --space-lg: calc(2 * var(--space-unit));
+ --space-xl: calc(3.25 * var(--space-unit));
+ --space-xxl: calc(5.25 * var(--space-unit));
+ --space-xxxl: calc(8.5 * var(--space-unit));
+ --space-xxxxl: calc(13.75 * var(--space-unit));
+ --component-padding: var(--space-md);
+}
diff --git a/apps/web-shared/src/styles/base/_typography.scss b/apps/web-shared/src/styles/base/_typography.scss
new file mode 100644
index 0000000..85b974a
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_typography.scss
@@ -0,0 +1,185 @@
+// don't modify this file -> edit 📁 custom-style/_typography.scss to set your custom typography
+
+@use 'breakpoints' as *;
+
+:root {
+ --heading-line-height: 1.2;
+ --body-line-height: 1.4;
+}
+
+body {
+ font-size: var(--text-base-size, 1rem);
+ font-family: var(--font-primary, sans-serif);
+ color: var(--color-contrast-high, hsl(210, 7%, 21%));
+ font-weight: var(--body-font-weight, normal);
+}
+
+h1, h2, h3, h4 {
+ color: var(--color-contrast-higher, hsl(204, 28%, 7%));
+ line-height: var(--heading-line-height, 1.2);
+ font-weight: var(--heading-font-weight, 700);
+}
+
+h1 {
+ font-size: var(--text-xxl, 2rem);
+}
+
+h2 {
+ font-size: var(--text-xl, 1.75rem);
+}
+
+h3 {
+ font-size: var(--text-lg, 1.375rem);
+}
+
+h4 {
+ font-size: var(--text-md, 1.125rem);
+}
+
+small {
+ font-size: var(--text-sm, 0.75rem);
+}
+
+// --------------------------------
+
+// Inline Text
+
+// --------------------------------
+
+a, .link {
+ color: var(--color-primary, hsl(250, 84%, 54%));
+ text-decoration: underline;
+}
+
+strong {
+ font-weight: bold;
+}
+
+s {
+ text-decoration: line-through;
+}
+
+u {
+ text-decoration: underline;
+}
+
+// --------------------------------
+
+// Text Component - Class used to stylize text blocks
+
+// --------------------------------
+
+.text-component {
+ h1, h2, h3, h4 {
+ line-height: calc(var(--heading-line-height) * var(--line-height-multiplier, 1));
+ margin-bottom: calc(var(--space-unit) * 0.3125 * var(--text-space-y-multiplier, 1));
+ }
+
+ h2, h3, h4 {
+ margin-top: calc(var(--space-unit) * 0.9375 * var(--text-space-y-multiplier, 1));
+ }
+
+ p, blockquote, ul li, ol li {
+ line-height: calc(var(--body-line-height) * var(--line-height-multiplier, 1));
+ }
+
+ ul, ol, p, blockquote, .text-component__block {
+ margin-bottom: calc(var(--space-unit) * 0.9375 * var(--text-space-y-multiplier, 1));
+ }
+
+ ul, ol {
+ list-style-position: inside;
+
+ ul, ol {
+ padding-left: 1em;
+ margin-bottom: 0;
+ }
+ }
+
+ ul {
+ list-style-type: disc;
+ }
+
+ ol {
+ list-style-type: decimal;
+ }
+
+ img {
+ display: block;
+ margin: 0 auto;
+ }
+
+ figcaption {
+ text-align: center;
+ margin-top: calc(var(--space-unit) * 0.5);
+ }
+
+ em {
+ font-style: italic;
+ }
+
+ hr {
+ margin-top: calc(var(--space-unit) * 1.875 * var(--text-space-y-multiplier, 1));
+ margin-bottom: calc(var(--space-unit) * 1.875 * var(--text-space-y-multiplier, 1));
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ > *:first-child {
+ margin-top: 0;
+ }
+
+ > *:last-child {
+ margin-bottom: 0;
+ }
+}
+
+// text block container
+.text-component__block--full-width {
+ width: 100vw;
+ margin-left: calc(50% - 50vw);
+}
+
+@include breakpoint(sm) {
+ .text-component__block--left,
+ .text-component__block--right {
+ width: 45%;
+
+ img {
+ width: 100%;
+ }
+ }
+
+ .text-component__block--left {
+ float: left;
+ margin-right: calc(var(--space-unit) * 0.9375 * var(--text-space-y-multiplier, 1));
+ }
+
+ .text-component__block--right {
+ float: right;
+ margin-left: calc(var(--space-unit) * 0.9375 * var(--text-space-y-multiplier, 1));
+ }
+}
+
+// outset content
+@include breakpoint(xl) {
+ .text-component__block--outset {
+ width: calc(100% + 10.5 * var(--space-unit));
+
+ img {
+ width: 100%;
+ }
+ }
+
+ .text-component__block--outset:not(.text-component__block--right) {
+ margin-left: calc(-5.25 * var(--space-unit));
+ }
+
+ .text-component__block--left, .text-component__block--right {
+ width: 50%;
+ }
+
+ .text-component__block--right.text-component__block--outset {
+ margin-right: calc(-5.25 * var(--space-unit));
+ }
+} \ No newline at end of file
diff --git a/apps/web-shared/src/styles/base/_util.scss b/apps/web-shared/src/styles/base/_util.scss
new file mode 100644
index 0000000..d688e1c
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_util.scss
@@ -0,0 +1,1738 @@
+@use 'mixins' as *;
+@use 'breakpoints' as *;
+
+// --------------------------------
+
+// Flexbox
+
+// --------------------------------
+
+.flex { display: flex; }
+.inline-flex { display: inline-flex; }
+.flex-wrap { flex-wrap: wrap; }
+.flex-nowrap { flex-wrap: nowrap; }
+.flex-column { flex-direction: column; }
+.flex-column-reverse { flex-direction: column-reverse; }
+.flex-row { flex-direction: row; }
+.flex-row-reverse { flex-direction: row-reverse; }
+.flex-center { justify-content: center; align-items: center; }
+
+
+// flex items
+.flex-grow { flex-grow: 1; }
+.flex-grow-0 { flex-grow: 0; }
+.flex-shrink { flex-shrink: 1; }
+.flex-shrink-0 { flex-shrink: 0; }
+.flex-basis-0 { flex-basis: 0; }
+
+// --------------------------------
+
+// Justify Content
+
+// --------------------------------
+
+.justify-start { justify-content: flex-start; }
+.justify-end { justify-content: flex-end; }
+.justify-center { justify-content: center; }
+.justify-between { justify-content: space-between; }
+
+// --------------------------------
+
+// Align Items
+
+// --------------------------------
+
+.items-center { align-items: center; }
+.items-start { align-items: flex-start; }
+.items-end { align-items: flex-end; }
+.items-baseline { align-items: baseline; }
+
+// --------------------------------
+
+// Order
+
+// --------------------------------
+
+.order-1 { order: 1; }
+.order-2 { order: 2; }
+.order-3 { order: 3; }
+
+// --------------------------------
+
+// Aspect Ratio
+
+// --------------------------------
+
+[class^="aspect-ratio"], [class*=" aspect-ratio"] {
+ --aspect-ratio: calc(16/9);
+ position: relative;
+ height: 0;
+ padding-bottom: calc(100%/(var(--aspect-ratio)));
+
+ > * {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ &:not(iframe) {
+ object-fit: cover;
+ }
+ }
+}
+
+.aspect-ratio-16\:9 { --aspect-ratio: calc(16/9); }
+.aspect-ratio-3\:2 { --aspect-ratio: calc(3/2); }
+.aspect-ratio-4\:3 { --aspect-ratio: calc(4/3); }
+.aspect-ratio-5\:4 { --aspect-ratio: calc(5/4); }
+.aspect-ratio-1\:1 { --aspect-ratio: calc(1/1); }
+.aspect-ratio-4\:5 { --aspect-ratio: calc(4/5); }
+.aspect-ratio-3\:4 { --aspect-ratio: calc(3/4); }
+.aspect-ratio-2\:3 { --aspect-ratio: calc(2/3); }
+.aspect-ratio-9\:16 { --aspect-ratio: calc(9/16); }
+
+// --------------------------------
+
+// Display
+
+// --------------------------------
+
+.block { display: block; }
+.inline-block { display: inline-block; }
+.inline { display: inline; }
+.contents { display: contents; }
+.hide { display: none; }
+
+// --------------------------------
+
+// Space unit
+
+// --------------------------------
+
+.space-unit-rem { --space-unit: 1rem; }
+.space-unit-em { --space-unit: 1em; }
+.space-unit-px { --space-unit: 16px; }
+
+// --------------------------------
+
+// Margin
+
+// --------------------------------
+
+.margin-xxxxs { margin: var(--space-xxxxs); }
+.margin-xxxs { margin: var(--space-xxxs); }
+.margin-xxs { margin: var(--space-xxs); }
+.margin-xs { margin: var(--space-xs); }
+.margin-sm { margin: var(--space-sm); }
+.margin-md { margin: var(--space-md); }
+.margin-lg { margin: var(--space-lg); }
+.margin-xl { margin: var(--space-xl); }
+.margin-xxl { margin: var(--space-xxl); }
+.margin-xxxl { margin: var(--space-xxxl); }
+.margin-xxxxl { margin: var(--space-xxxxl); }
+.margin-auto { margin: auto; }
+.margin-0 { margin: 0; }
+
+.margin-top-xxxxs { margin-top: var(--space-xxxxs); }
+.margin-top-xxxs { margin-top: var(--space-xxxs); }
+.margin-top-xxs { margin-top: var(--space-xxs); }
+.margin-top-xs { margin-top: var(--space-xs); }
+.margin-top-sm { margin-top: var(--space-sm); }
+.margin-top-md { margin-top: var(--space-md); }
+.margin-top-lg { margin-top: var(--space-lg); }
+.margin-top-xl { margin-top: var(--space-xl); }
+.margin-top-xxl { margin-top: var(--space-xxl); }
+.margin-top-xxxl { margin-top: var(--space-xxxl); }
+.margin-top-xxxxl { margin-top: var(--space-xxxxl); }
+.margin-top-auto { margin-top: auto; }
+.margin-top-0 { margin-top: 0; }
+
+.margin-bottom-xxxxs { margin-bottom: var(--space-xxxxs); }
+.margin-bottom-xxxs { margin-bottom: var(--space-xxxs); }
+.margin-bottom-xxs { margin-bottom: var(--space-xxs); }
+.margin-bottom-xs { margin-bottom: var(--space-xs); }
+.margin-bottom-sm { margin-bottom: var(--space-sm); }
+.margin-bottom-md { margin-bottom: var(--space-md); }
+.margin-bottom-lg { margin-bottom: var(--space-lg); }
+.margin-bottom-xl { margin-bottom: var(--space-xl); }
+.margin-bottom-xxl { margin-bottom: var(--space-xxl); }
+.margin-bottom-xxxl { margin-bottom: var(--space-xxxl); }
+.margin-bottom-xxxxl { margin-bottom: var(--space-xxxxl); }
+.margin-bottom-auto { margin-bottom: auto; }
+.margin-bottom-0 { margin-bottom: 0; }
+
+.margin-right-xxxxs { margin-right: var(--space-xxxxs); }
+.margin-right-xxxs { margin-right: var(--space-xxxs); }
+.margin-right-xxs { margin-right: var(--space-xxs); }
+.margin-right-xs { margin-right: var(--space-xs); }
+.margin-right-sm { margin-right: var(--space-sm); }
+.margin-right-md { margin-right: var(--space-md); }
+.margin-right-lg { margin-right: var(--space-lg); }
+.margin-right-xl { margin-right: var(--space-xl); }
+.margin-right-xxl { margin-right: var(--space-xxl); }
+.margin-right-xxxl { margin-right: var(--space-xxxl); }
+.margin-right-xxxxl { margin-right: var(--space-xxxxl); }
+.margin-right-auto { margin-right: auto; }
+.margin-right-0 { margin-right: 0; }
+
+.margin-left-xxxxs { margin-left: var(--space-xxxxs); }
+.margin-left-xxxs { margin-left: var(--space-xxxs); }
+.margin-left-xxs { margin-left: var(--space-xxs); }
+.margin-left-xs { margin-left: var(--space-xs); }
+.margin-left-sm { margin-left: var(--space-sm); }
+.margin-left-md { margin-left: var(--space-md); }
+.margin-left-lg { margin-left: var(--space-lg); }
+.margin-left-xl { margin-left: var(--space-xl); }
+.margin-left-xxl { margin-left: var(--space-xxl); }
+.margin-left-xxxl { margin-left: var(--space-xxxl); }
+.margin-left-xxxxl { margin-left: var(--space-xxxxl); }
+.margin-left-auto { margin-left: auto; }
+.margin-left-0 { margin-left: 0; }
+
+.margin-x-xxxxs { margin-left: var(--space-xxxxs); margin-right: var(--space-xxxxs); }
+.margin-x-xxxs { margin-left: var(--space-xxxs); margin-right: var(--space-xxxs); }
+.margin-x-xxs { margin-left: var(--space-xxs); margin-right: var(--space-xxs); }
+.margin-x-xs { margin-left: var(--space-xs); margin-right: var(--space-xs); }
+.margin-x-sm { margin-left: var(--space-sm); margin-right: var(--space-sm); }
+.margin-x-md { margin-left: var(--space-md); margin-right: var(--space-md); }
+.margin-x-lg { margin-left: var(--space-lg); margin-right: var(--space-lg); }
+.margin-x-xl { margin-left: var(--space-xl); margin-right: var(--space-xl); }
+.margin-x-xxl { margin-left: var(--space-xxl); margin-right: var(--space-xxl); }
+.margin-x-xxxl { margin-left: var(--space-xxxl); margin-right: var(--space-xxxl); }
+.margin-x-xxxxl { margin-left: var(--space-xxxxl); margin-right: var(--space-xxxxl); }
+.margin-x-auto { margin-left: auto; margin-right: auto; }
+.margin-x-0 { margin-left: 0; margin-right: 0; }
+
+.margin-y-xxxxs { margin-top: var(--space-xxxxs); margin-bottom: var(--space-xxxxs); }
+.margin-y-xxxs { margin-top: var(--space-xxxs); margin-bottom: var(--space-xxxs); }
+.margin-y-xxs { margin-top: var(--space-xxs); margin-bottom: var(--space-xxs); }
+.margin-y-xs { margin-top: var(--space-xs); margin-bottom: var(--space-xs); }
+.margin-y-sm { margin-top: var(--space-sm); margin-bottom: var(--space-sm); }
+.margin-y-md { margin-top: var(--space-md); margin-bottom: var(--space-md); }
+.margin-y-lg { margin-top: var(--space-lg); margin-bottom: var(--space-lg); }
+.margin-y-xl { margin-top: var(--space-xl); margin-bottom: var(--space-xl); }
+.margin-y-xxl { margin-top: var(--space-xxl); margin-bottom: var(--space-xxl); }
+.margin-y-xxxl { margin-top: var(--space-xxxl); margin-bottom: var(--space-xxxl); }
+.margin-y-xxxxl { margin-top: var(--space-xxxxl); margin-bottom: var(--space-xxxxl); }
+.margin-y-auto { margin-top: auto; margin-bottom: auto; }
+.margin-y-0 { margin-top: 0; margin-bottom: 0; }
+
+// --------------------------------
+
+// Padding
+
+// --------------------------------
+
+.padding-xxxxs { padding: var(--space-xxxxs); }
+.padding-xxxs { padding: var(--space-xxxs); }
+.padding-xxs { padding: var(--space-xxs); }
+.padding-xs { padding: var(--space-xs); }
+.padding-sm { padding: var(--space-sm); }
+.padding-md { padding: var(--space-md); }
+.padding-lg { padding: var(--space-lg); }
+.padding-xl { padding: var(--space-xl); }
+.padding-xxl { padding: var(--space-xxl); }
+.padding-xxxl { padding: var(--space-xxxl); }
+.padding-xxxxl { padding: var(--space-xxxxl); }
+.padding-0 { padding: 0; }
+.padding-component { padding: var(--component-padding); }
+
+.padding-top-xxxxs { padding-top: var(--space-xxxxs); }
+.padding-top-xxxs { padding-top: var(--space-xxxs); }
+.padding-top-xxs { padding-top: var(--space-xxs); }
+.padding-top-xs { padding-top: var(--space-xs); }
+.padding-top-sm { padding-top: var(--space-sm); }
+.padding-top-md { padding-top: var(--space-md); }
+.padding-top-lg { padding-top: var(--space-lg); }
+.padding-top-xl { padding-top: var(--space-xl); }
+.padding-top-xxl { padding-top: var(--space-xxl); }
+.padding-top-xxxl { padding-top: var(--space-xxxl); }
+.padding-top-xxxxl { padding-top: var(--space-xxxxl); }
+.padding-top-0 { padding-top: 0; }
+.padding-top-component { padding-top: var(--component-padding); }
+
+.padding-bottom-xxxxs { padding-bottom: var(--space-xxxxs); }
+.padding-bottom-xxxs { padding-bottom: var(--space-xxxs); }
+.padding-bottom-xxs { padding-bottom: var(--space-xxs); }
+.padding-bottom-xs { padding-bottom: var(--space-xs); }
+.padding-bottom-sm { padding-bottom: var(--space-sm); }
+.padding-bottom-md { padding-bottom: var(--space-md); }
+.padding-bottom-lg { padding-bottom: var(--space-lg); }
+.padding-bottom-xl { padding-bottom: var(--space-xl); }
+.padding-bottom-xxl { padding-bottom: var(--space-xxl); }
+.padding-bottom-xxxl { padding-bottom: var(--space-xxxl); }
+.padding-bottom-xxxxl { padding-bottom: var(--space-xxxxl); }
+.padding-bottom-0 { padding-bottom: 0; }
+.padding-bottom-component { padding-bottom: var(--component-padding); }
+
+.padding-right-xxxxs { padding-right: var(--space-xxxxs); }
+.padding-right-xxxs { padding-right: var(--space-xxxs); }
+.padding-right-xxs { padding-right: var(--space-xxs); }
+.padding-right-xs { padding-right: var(--space-xs); }
+.padding-right-sm { padding-right: var(--space-sm); }
+.padding-right-md { padding-right: var(--space-md); }
+.padding-right-lg { padding-right: var(--space-lg); }
+.padding-right-xl { padding-right: var(--space-xl); }
+.padding-right-xxl { padding-right: var(--space-xxl); }
+.padding-right-xxxl { padding-right: var(--space-xxxl); }
+.padding-right-xxxxl { padding-right: var(--space-xxxxl); }
+.padding-right-0 { padding-right: 0; }
+.padding-right-component { padding-right: var(--component-padding); }
+
+.padding-left-xxxxs { padding-left: var(--space-xxxxs); }
+.padding-left-xxxs { padding-left: var(--space-xxxs); }
+.padding-left-xxs { padding-left: var(--space-xxs); }
+.padding-left-xs { padding-left: var(--space-xs); }
+.padding-left-sm { padding-left: var(--space-sm); }
+.padding-left-md { padding-left: var(--space-md); }
+.padding-left-lg { padding-left: var(--space-lg); }
+.padding-left-xl { padding-left: var(--space-xl); }
+.padding-left-xxl { padding-left: var(--space-xxl); }
+.padding-left-xxxl { padding-left: var(--space-xxxl); }
+.padding-left-xxxxl { padding-left: var(--space-xxxxl); }
+.padding-left-0 { padding-left: 0; }
+.padding-left-component { padding-left: var(--component-padding); }
+
+.padding-x-xxxxs { padding-left: var(--space-xxxxs); padding-right: var(--space-xxxxs); }
+.padding-x-xxxs { padding-left: var(--space-xxxs); padding-right: var(--space-xxxs); }
+.padding-x-xxs { padding-left: var(--space-xxs); padding-right: var(--space-xxs); }
+.padding-x-xs { padding-left: var(--space-xs); padding-right: var(--space-xs); }
+.padding-x-sm { padding-left: var(--space-sm); padding-right: var(--space-sm); }
+.padding-x-md { padding-left: var(--space-md); padding-right: var(--space-md); }
+.padding-x-lg { padding-left: var(--space-lg); padding-right: var(--space-lg); }
+.padding-x-xl { padding-left: var(--space-xl); padding-right: var(--space-xl); }
+.padding-x-xxl { padding-left: var(--space-xxl); padding-right: var(--space-xxl); }
+.padding-x-xxxl { padding-left: var(--space-xxxl); padding-right: var(--space-xxxl); }
+.padding-x-xxxxl { padding-left: var(--space-xxxxl); padding-right: var(--space-xxxxl); }
+.padding-x-0 { padding-left: 0; padding-right: 0; }
+.padding-x-component { padding-left: var(--component-padding); padding-right: var(--component-padding); }
+
+.padding-y-xxxxs { padding-top: var(--space-xxxxs); padding-bottom: var(--space-xxxxs); }
+.padding-y-xxxs { padding-top: var(--space-xxxs); padding-bottom: var(--space-xxxs); }
+.padding-y-xxs { padding-top: var(--space-xxs); padding-bottom: var(--space-xxs); }
+.padding-y-xs { padding-top: var(--space-xs); padding-bottom: var(--space-xs); }
+.padding-y-sm { padding-top: var(--space-sm); padding-bottom: var(--space-sm); }
+.padding-y-md { padding-top: var(--space-md); padding-bottom: var(--space-md); }
+.padding-y-lg { padding-top: var(--space-lg); padding-bottom: var(--space-lg); }
+.padding-y-xl { padding-top: var(--space-xl); padding-bottom: var(--space-xl); }
+.padding-y-xxl { padding-top: var(--space-xxl); padding-bottom: var(--space-xxl); }
+.padding-y-xxxl { padding-top: var(--space-xxxl); padding-bottom: var(--space-xxxl); }
+.padding-y-xxxxl { padding-top: var(--space-xxxxl); padding-bottom: var(--space-xxxxl); }
+.padding-y-0 { padding-top: 0; padding-bottom: 0; }
+.padding-y-component { padding-top: var(--component-padding); padding-bottom: var(--component-padding); }
+
+// --------------------------------
+
+// Vertical Align
+
+// --------------------------------
+
+.align-baseline { vertical-align: baseline; }
+.align-top { vertical-align: top; }
+.align-middle { vertical-align: middle; }
+.align-bottom { vertical-align: bottom; }
+
+// --------------------------------
+
+// Typography
+
+// --------------------------------
+
+.truncate, .text-truncate { // truncate text if it exceeds its parent
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.text-replace { // replace text with bg img
+ overflow: hidden;
+ color: transparent;
+ text-indent: 100%;
+ white-space: nowrap;
+}
+
+.break-word {
+ overflow-wrap: break-word;
+ min-width: 0;
+}
+
+// --------------------------------
+
+// Font Size
+
+// --------------------------------
+
+.text-unit-rem, .text-unit-em, .text-unit-px {
+ font-size: var(--text-unit);
+}
+
+.text-unit-rem { --text-unit: 1rem; }
+.text-unit-em { --text-unit: 1em; }
+.text-unit-px { --text-unit: 16px; }
+
+.text-xs { font-size: var(--text-xs, 0.6875rem); }
+.text-sm { font-size: var(--text-sm, 0.75rem); }
+.text-base { font-size: var(--text-unit, 1rem); }
+.text-md { font-size: var(--text-md, 1.125rem); }
+.text-lg { font-size: var(--text-lg, 1.375rem); }
+.text-xl { font-size: var(--text-xl, 1.75rem); }
+.text-xxl { font-size: var(--text-xxl, 2rem); }
+.text-xxxl { font-size: var(--text-xxxl, 2.5rem); }
+.text-xxxxl { font-size: var(--text-xxxxl, 3rem); }
+
+// --------------------------------
+
+// Text Transform
+
+// --------------------------------
+
+.text-uppercase { text-transform: uppercase; }
+.text-capitalize { text-transform: capitalize; }
+
+// --------------------------------
+
+// Letter Spacing
+
+// --------------------------------
+
+.letter-spacing-xs { letter-spacing: -0.1em; }
+.letter-spacing-sm { letter-spacing: -0.05em; }
+.letter-spacing-md { letter-spacing: 0.05em; }
+.letter-spacing-lg { letter-spacing: 0.1em; }
+.letter-spacing-xl { letter-spacing: 0.2em; }
+
+// --------------------------------
+
+// Font Weight
+
+// --------------------------------
+
+.font-light { font-weight: 300; }
+.font-normal { font-weight: 400; }
+.font-medium { font-weight: 500; }
+.font-semibold { font-weight: 600; }
+.font-bold, .text-bold { font-weight: 700; }
+
+// --------------------------------
+
+// Font Style
+
+// --------------------------------
+
+.font-italic { font-style: italic; }
+
+// --------------------------------
+
+// Font Smooth
+
+// --------------------------------
+
+.font-smooth {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+// --------------------------------
+
+// Font Family
+
+// --------------------------------
+
+.font-primary { font-family: var(--font-primary); }
+
+// --------------------------------
+
+// Text Align
+
+// --------------------------------
+
+.text-center { text-align: center; }
+.text-left { text-align: left; }
+.text-right { text-align: right; }
+.text-justify { text-align: justify; }
+
+// --------------------------------
+
+// Text Decoration
+
+// --------------------------------
+
+.text-line-through { text-decoration: line-through; }
+.text-underline { text-decoration: underline; }
+.text-decoration-none { text-decoration: none; }
+
+// --------------------------------
+
+// Text Shadow
+
+// --------------------------------
+
+.text-shadow-xs { text-shadow: 0 1px 1px rgba(#000, 0.15); }
+.text-shadow-sm { text-shadow: 0 1px 2px rgba(#000, 0.25); }
+.text-shadow-md { text-shadow: 0 1px 2px rgba(#000, 0.1), 0 2px 4px rgba(#000, 0.2); }
+.text-shadow-lg { text-shadow: 0 1px 4px rgba(#000, 0.1), 0 2px 8px rgba(#000, 0.15), 0 4px 16px rgba(#000, 0.2); }
+.text-shadow-xl { text-shadow: 0 1px 4px rgba(#000, 0.1), 0 2px 8px rgba(#000, 0.15), 0 4px 16px rgba(#000, 0.2), 0 6px 24px rgba(#000, 0.25); }
+.text-shadow-none { text-shadow: none; }
+
+// --------------------------------
+
+// .text-component vertical spacing
+
+// --------------------------------
+
+.text-space-y-xxs { --text-space-y-multiplier: 0.25 !important; }
+.text-space-y-xs { --text-space-y-multiplier: 0.5 !important; }
+.text-space-y-sm { --text-space-y-multiplier: 0.75 !important; }
+.text-space-y-md { --text-space-y-multiplier: 1.25 !important; }
+.text-space-y-lg { --text-space-y-multiplier: 1.5 !important; }
+.text-space-y-xl { --text-space-y-multiplier: 1.75 !important; }
+.text-space-y-xxl { --text-space-y-multiplier: 2 !important; }
+
+// --------------------------------
+
+// Line Height
+
+// --------------------------------
+
+.line-height-xs {
+ --heading-line-height: 1;
+ --body-line-height: 1.1;
+
+ &:not(.text-component) {
+ line-height: 1.1;
+ }
+}
+
+.line-height-sm {
+ --heading-line-height: 1.1;
+ --body-line-height: 1.2;
+
+ &:not(.text-component) {
+ line-height: 1.2;
+ }
+}
+
+.line-height-md {
+ --heading-line-height: 1.15;
+ --body-line-height: 1.4;
+
+ &:not(.text-component) {
+ line-height: 1.4;
+ }
+}
+
+.line-height-lg {
+ --heading-line-height: 1.22;
+ --body-line-height: 1.58;
+
+ &:not(.text-component) {
+ line-height: 1.58;
+ }
+}
+
+.line-height-xl {
+ --heading-line-height: 1.3;
+ --body-line-height: 1.72;
+
+ &:not(.text-component) {
+ line-height: 1.72;
+ }
+}
+
+.line-height-body { line-height: var(--body-line-height); }
+.line-height-heading { line-height: var(--heading-line-height); }
+.line-height-normal { line-height: normal !important; }
+.line-height-1 { line-height: 1 !important; }
+
+// --------------------------------
+
+// White Space
+
+// --------------------------------
+
+.ws-nowrap, .text-nowrap { white-space: nowrap; }
+
+// --------------------------------
+
+// Cursor
+
+// --------------------------------
+
+.cursor-pointer { cursor: pointer; }
+.cursor-default { cursor: default; }
+
+// --------------------------------
+
+// Pointer Events
+
+// --------------------------------
+
+.pointer-events-auto { pointer-events: auto; }
+.pointer-events-none { pointer-events: none; }
+
+// --------------------------------
+
+// User Select
+
+// --------------------------------
+
+.user-select-none { user-select: none; }
+.user-select-all { user-select: all; }
+
+// --------------------------------
+
+// Color
+
+// --------------------------------
+
+[class^="color-"], [class*=" color-"] { --color-o: 1; }
+
+.color-inherit { color: inherit; }
+
+.color-bg-darker { color: alpha(var(--color-bg-darker), var(--color-o, 1)); }
+.color-bg-dark { color: alpha(var(--color-bg-dark), var(--color-o, 1)); }
+.color-bg { color: alpha(var(--color-bg), var(--color-o, 1)); }
+.color-bg-light { color: alpha(var(--color-bg-light), var(--color-o, 1)); }
+.color-bg-lighter { color: alpha(var(--color-bg-lighter), var(--color-o, 1)); }
+
+.color-contrast-lower { color: alpha(var(--color-contrast-lower), var(--color-o, 1)); }
+.color-contrast-low { color: alpha(var(--color-contrast-low), var(--color-o, 1)); }
+.color-contrast-medium { color: alpha(var(--color-contrast-medium), var(--color-o, 1)); }
+.color-contrast-high { color: alpha(var(--color-contrast-high), var(--color-o, 1)); }
+.color-contrast-higher { color: alpha(var(--color-contrast-higher), var(--color-o, 1)); }
+
+.color-primary-darker { color: alpha(var(--color-primary-darker), var(--color-o, 1)); }
+.color-primary-dark { color: alpha(var(--color-primary-dark), var(--color-o, 1)); }
+.color-primary { color: alpha(var(--color-primary), var(--color-o, 1)); }
+.color-primary-light { color: alpha(var(--color-primary-light), var(--color-o, 1)); }
+.color-primary-lighter { color: alpha(var(--color-primary-lighter), var(--color-o, 1)); }
+
+.color-accent-darker { color: alpha(var(--color-accent-darker), var(--color-o, 1)); }
+.color-accent-dark { color: alpha(var(--color-accent-dark), var(--color-o, 1)); }
+.color-accent { color: alpha(var(--color-accent), var(--color-o, 1)); }
+.color-accent-light { color: alpha(var(--color-accent-light), var(--color-o, 1)); }
+.color-accent-lighter { color: alpha(var(--color-accent-lighter), var(--color-o, 1)); }
+
+.color-success-darker { color: alpha(var(--color-success-darker), var(--color-o, 1)); }
+.color-success-dark { color: alpha(var(--color-success-dark), var(--color-o, 1)); }
+.color-success { color: alpha(var(--color-success), var(--color-o, 1)); }
+.color-success-light { color: alpha(var(--color-success-light), var(--color-o, 1)); }
+.color-success-lighter { color: alpha(var(--color-success-lighter), var(--color-o, 1)); }
+
+.color-warning-darker { color: alpha(var(--color-warning-darker), var(--color-o, 1)); }
+.color-warning-dark { color: alpha(var(--color-warning-dark), var(--color-o, 1)); }
+.color-warning { color: alpha(var(--color-warning), var(--color-o, 1)); }
+.color-warning-light { color: alpha(var(--color-warning-light), var(--color-o, 1)); }
+.color-warning-lighter { color: alpha(var(--color-warning-lighter), var(--color-o, 1)); }
+
+.color-error-darker { color: alpha(var(--color-error-darker), var(--color-o, 1)); }
+.color-error-dark { color: alpha(var(--color-error-dark), var(--color-o, 1)); }
+.color-error { color: alpha(var(--color-error), var(--color-o, 1)); }
+.color-error-light { color: alpha(var(--color-error-light), var(--color-o, 1)); }
+.color-error-lighter { color: alpha(var(--color-error-lighter), var(--color-o, 1)); }
+
+.color-white { color: alpha(var(--color-white), var(--color-o, 1)); }
+.color-black { color: alpha(var(--color-black), var(--color-o, 1)); }
+
+.color-opacity-0 { --color-o: 0; }
+.color-opacity-10\% { --color-o: 0.1; }
+.color-opacity-20\% { --color-o: 0.2; }
+.color-opacity-30\% { --color-o: 0.3; }
+.color-opacity-40\% { --color-o: 0.4; }
+.color-opacity-50\% { --color-o: 0.5; }
+.color-opacity-60\% { --color-o: 0.6; }
+.color-opacity-70\% { --color-o: 0.7; }
+.color-opacity-80\% { --color-o: 0.8; }
+.color-opacity-90\% { --color-o: 0.9; }
+
+// --------------------------------
+
+// Gradients
+
+// --------------------------------
+
+[class^="color-gradient"], [class*=" color-gradient"] {
+ color: transparent !important;
+ background-clip: text;
+}
+
+// --------------------------------
+
+// Width
+
+// --------------------------------
+
+.width-xxxxs { width: var(--size-xxxxs, 0.25rem); }
+.width-xxxs { width: var(--size-xxxs, 0.5rem); }
+.width-xxs { width: var(--size-xxs, 0.75rem); }
+.width-xs { width: var(--size-xs, 1rem); }
+.width-sm { width: var(--size-sm, 1.5rem); }
+.width-md { width: var(--size-md, 2rem); }
+.width-lg { width: var(--size-lg, 3rem); }
+.width-xl { width: var(--size-xl, 4rem); }
+.width-xxl { width: var(--size-xxl, 6rem); }
+.width-xxxl { width: var(--size-xxxl, 8rem); }
+.width-xxxxl { width: var(--size-xxxxl, 16rem); }
+.width-0 { width: 0; }
+.width-10\% { width: 10%; }
+.width-20\% { width: 20%; }
+.width-25\% { width: 25%; }
+.width-30\% { width: 30%; }
+.width-33\% { width: calc(100% / 3); }
+.width-40\% { width: 40%; }
+.width-50\% { width: 50%; }
+.width-60\% { width: 60%; }
+.width-66\% { width: calc(100% / 1.5); }
+.width-70\% { width: 70%; }
+.width-75\% { width: 75%; }
+.width-80\% { width: 80%; }
+.width-90\% { width: 90%; }
+.width-100\% { width: 100%; }
+.width-100vw { width: 100vw; }
+.width-auto { width: auto; }
+.width-fit-content { width: fit-content; }
+.width-max-content { width: max-content; }
+
+// --------------------------------
+
+// Height
+
+// --------------------------------
+
+.height-xxxxs { height: var(--size-xxxxs, 0.25rem); }
+.height-xxxs { height: var(--size-xxxs, 0.5rem); }
+.height-xxs { height: var(--size-xxs, 0.75rem); }
+.height-xs { height: var(--size-xs, 1rem); }
+.height-sm { height: var(--size-sm, 1.5rem); }
+.height-md { height: var(--size-md, 2rem); }
+.height-lg { height: var(--size-lg, 3rem); }
+.height-xl { height: var(--size-xl, 4rem); }
+.height-xxl { height: var(--size-xxl, 6rem); }
+.height-xxxl { height: var(--size-xxxl, 8rem); }
+.height-xxxxl { height: var(--size-xxxxl, 16rem); }
+.height-0 { height: 0; }
+.height-10\% { height: 10%; }
+.height-20\% { height: 20%; }
+.height-25\% { height: 25%; }
+.height-30\% { height: 30%; }
+.height-33\% { height: calc(100% / 3); }
+.height-40\% { height: 40%; }
+.height-50\% { height: 50%; }
+.height-60\% { height: 60%; }
+.height-66\% { height: calc(100% / 1.5); }
+.height-70\% { height: 70%; }
+.height-75\% { height: 75%; }
+.height-80\% { height: 80%; }
+.height-90\% { height: 90%; }
+.height-100\% { height: 100%; }
+.height-100vh { height: 100vh; }
+.height-auto { height: auto; }
+.height-fit-content { height: fit-content; }
+.height-max-content { height: max-content; }
+
+// --------------------------------
+
+// Min-Width
+
+// --------------------------------
+
+.min-width-0 { min-width: 0; }
+.min-width-25\% { min-width: 25%; }
+.min-width-33\% { min-width: calc(100% / 3); }
+.min-width-50\% { min-width: 50%; }
+.min-width-66\% { min-width: calc(100% / 1.5); }
+.min-width-75\% { min-width: 75%; }
+.min-width-100\% { min-width: 100%; }
+.min-width-100vw { min-width: 100vw; }
+.min-width-fit-content { min-width: fit-content; }
+.min-width-max-content { min-width: max-content; }
+
+// --------------------------------
+
+// Min-Height
+
+// --------------------------------
+
+.min-height-100\% { min-height: 100%; }
+.min-height-100vh { min-height: 100vh; }
+.min-height-fit-content { min-height: fit-content; }
+.min-height-max-content { min-height: max-content; }
+
+// --------------------------------
+
+// Max-Width
+
+// --------------------------------
+
+:root {
+ --max-width-xxxxs: 20rem; // ~320px
+ --max-width-xxxs: 26rem; // ~416px
+ --max-width-xxs: 32rem; // ~512px
+ --max-width-xs: 38rem; // ~608px
+ --max-width-sm: 48rem; // ~768px
+ --max-width-md: 64rem; // ~1024px
+ --max-width-lg: 80rem; // ~1280px
+ --max-width-xl: 90rem; // ~1440px
+ --max-width-xxl: 100rem; // ~1600px
+ --max-width-xxxl: 120rem; // ~1920px
+ --max-width-xxxxl: 150rem; // ~2400px
+}
+
+.max-width-xxxxs { max-width: var(--max-width-xxxxs); }
+.max-width-xxxs { max-width: var(--max-width-xxxs); }
+.max-width-xxs { max-width: var(--max-width-xxs); }
+.max-width-xs { max-width: var(--max-width-xs); }
+.max-width-sm { max-width: var(--max-width-sm); }
+.max-width-md { max-width: var(--max-width-md); }
+.max-width-lg { max-width: var(--max-width-lg); }
+.max-width-xl { max-width: var(--max-width-xl); }
+.max-width-xxl { max-width: var(--max-width-xxl); }
+.max-width-xxxl { max-width: var(--max-width-xxxl); }
+.max-width-xxxxl { max-width: var(--max-width-xxxxl); }
+.max-width-100\% { max-width: 100%; }
+.max-width-none { max-width: none; }
+.max-width-fit-content { max-width: fit-content; }
+.max-width-max-content { max-width: max-content; }
+
+// alt approach - max-width is equal to current breakpoint
+$breakpointsNr: length($breakpoints);
+@each $breakpoint, $value in $breakpoints {
+ $i: index($breakpoints, $breakpoint $value);
+ @if $i == 1 {
+ [class^="max-width-adaptive"], [class*=" max-width-adaptive"] {
+ max-width: map-get($map: $breakpoints, $key: #{$breakpoint});
+ }
+ } @else {
+ $classList : '';
+ @each $subBreakpoint, $subValue in $breakpoints {
+ $j: index($breakpoints, $subBreakpoint $subValue);
+ @if $j == $i {
+ $classList: '.max-width-adaptive-#{$subBreakpoint}';
+ } @else if $j > $i {
+ $classList: $classList+', .max-width-adaptive-#{$subBreakpoint}';
+ }
+ }
+ @if $i < $breakpointsNr {
+ $classList: $classList+', .max-width-adaptive';
+ }
+ @include breakpoint(#{$breakpoint}) {
+ #{$classList} {
+ max-width: map-get($map: $breakpoints, $key: #{$breakpoint});
+ }
+ }
+ }
+}
+
+// --------------------------------
+
+// Max-Height
+
+// --------------------------------
+
+.max-height-100\% { max-height: 100%; }
+.max-height-100vh { max-height: 100vh; }
+
+// --------------------------------
+
+// Box-Shadow
+
+// --------------------------------
+
+.shadow-xs { box-shadow: var(--shadow-xs); }
+.shadow-sm { box-shadow: var(--shadow-sm); }
+.shadow-md { box-shadow: var(--shadow-md); }
+.shadow-lg { box-shadow: var(--shadow-lg); }
+.shadow-xl { box-shadow: var(--shadow-xl); }
+.shadow-none { box-shadow: none; }
+
+:root {
+ --inner-glow: inset 0 0 0.5px 1px hsla(0, 0%, 100%, 0.075);
+ --inner-glow-top: inset 0 1px 0.5px hsla(0, 0%, 100%, 0.075);
+}
+
+.inner-glow, .inner-glow-top {
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: inherit;
+ pointer-events: none;
+ }
+}
+
+.inner-glow::after { box-shadow: var(--inner-glow); }
+.inner-glow-top::after { box-shadow: var(--inner-glow-top); }
+
+// --------------------------------
+
+// Position
+
+// --------------------------------
+
+.position-relative { position: relative; }
+.position-absolute { position: absolute; }
+.position-fixed { position: fixed; }
+.position-sticky { position: sticky; }
+
+.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
+
+.top-0 { top: 0; }
+.top-50\% { top: 50%; }
+.top-xxxxs { top: var(--space-xxxxs); }
+.top-xxxs { top: var(--space-xxxs); }
+.top-xxs { top: var(--space-xxs); }
+.top-xs { top: var(--space-xs); }
+.top-sm { top: var(--space-sm); }
+.top-md { top: var(--space-md); }
+.top-lg { top: var(--space-lg); }
+.top-xl { top: var(--space-xl); }
+.top-xxl { top: var(--space-xxl); }
+.top-xxxl { top: var(--space-xxxl); }
+.top-xxxxl { top: var(--space-xxxxl); }
+
+.bottom-0 { bottom: 0; }
+.bottom-unset { bottom: unset; }
+.bottom-50\% { bottom: 50%; }
+.bottom-xxxxs { bottom: var(--space-xxxxs); }
+.bottom-xxxs { bottom: var(--space-xxxs); }
+.bottom-xxs { bottom: var(--space-xxs); }
+.bottom-xs { bottom: var(--space-xs); }
+.bottom-sm { bottom: var(--space-sm); }
+.bottom-md { bottom: var(--space-md); }
+.bottom-lg { bottom: var(--space-lg); }
+.bottom-xl { bottom: var(--space-xl); }
+.bottom-xxl { bottom: var(--space-xxl); }
+.bottom-xxxl { bottom: var(--space-xxxl); }
+.bottom-xxxxl { bottom: var(--space-xxxxl); }
+
+.right-0 { right: 0; }
+.right-50\% { right: 50%; }
+.right-xxxxs { right: var(--space-xxxxs); }
+.right-xxxs { right: var(--space-xxxs); }
+.right-xxs { right: var(--space-xxs); }
+.right-xs { right: var(--space-xs); }
+.right-sm { right: var(--space-sm); }
+.right-md { right: var(--space-md); }
+.right-lg { right: var(--space-lg); }
+.right-xl { right: var(--space-xl); }
+.right-xxl { right: var(--space-xxl); }
+.right-xxxl { right: var(--space-xxxl); }
+.right-xxxxl { right: var(--space-xxxxl); }
+
+.left-0 { left: 0; }
+.left-50\% { left: 50%; }
+.left-xxxxs { left: var(--space-xxxxs); }
+.left-xxxs { left: var(--space-xxxs); }
+.left-xxs { left: var(--space-xxs); }
+.left-xs { left: var(--space-xs); }
+.left-sm { left: var(--space-sm); }
+.left-md { left: var(--space-md); }
+.left-lg { left: var(--space-lg); }
+.left-xl { left: var(--space-xl); }
+.left-xxl { left: var(--space-xxl); }
+.left-xxxl { left: var(--space-xxxl); }
+.left-xxxxl { left: var(--space-xxxxl); }
+
+// --------------------------------
+
+// Z-Index
+
+// --------------------------------
+
+.z-index-header { z-index: var(--z-index-header); }
+.z-index-popover { z-index: var(--z-index-popover); }
+.z-index-fixed-element { z-index: var(--z-index-fixed-element); }
+.z-index-overlay { z-index: var(--z-index-overlay); }
+
+.z-index-1 { z-index: 1; }
+.z-index-2 { z-index: 2; }
+.z-index-3 { z-index: 3; }
+
+// --------------------------------
+
+// Overflow
+
+// --------------------------------
+
+.overflow-hidden { overflow: hidden; }
+.overflow-auto { overflow: auto; }
+.momentum-scrolling { -webkit-overflow-scrolling: touch; }
+
+// overscroll-behavior
+.overscroll-contain { overscroll-behavior: contain; }
+
+// --------------------------------
+
+// Scroll Behavior
+
+// --------------------------------
+
+.scroll-smooth { scroll-behavior: smooth; }
+
+.scroll-padding-xxxxs { scroll-padding: var(--space-xxxxs); }
+.scroll-padding-xxxs { scroll-padding: var(--space-xxxs); }
+.scroll-padding-xxs { scroll-padding: var(--space-xxs); }
+.scroll-padding-xs { scroll-padding: var(--space-xs); }
+.scroll-padding-sm { scroll-padding: var(--space-sm); }
+.scroll-padding-md { scroll-padding: var(--space-md); }
+.scroll-padding-lg { scroll-padding: var(--space-lg); }
+.scroll-padding-xl { scroll-padding: var(--space-xl); }
+.scroll-padding-xxl { scroll-padding: var(--space-xxl); }
+.scroll-padding-xxxl { scroll-padding: var(--space-xxxl); }
+.scroll-padding-xxxxl { scroll-padding: var(--space-xxxxl); }
+
+
+// --------------------------------
+
+// Opacity
+
+// --------------------------------
+
+.opacity-0 { opacity: 0; }
+.opacity-10\% { opacity: 0.1; }
+.opacity-20\% { opacity: 0.2; }
+.opacity-30\% { opacity: 0.3; }
+.opacity-40\% { opacity: 0.4; }
+.opacity-50\% { opacity: 0.5; }
+.opacity-60\% { opacity: 0.6; }
+.opacity-70\% { opacity: 0.7; }
+.opacity-80\% { opacity: 0.8; }
+.opacity-90\% { opacity: 0.9; }
+
+// --------------------------------
+
+// Float
+
+// --------------------------------
+
+.float-left { float: left; }
+.float-right { float: right; }
+
+.clearfix::after {
+ content: "";
+ display: table;
+ clear: both;
+}
+
+// --------------------------------
+
+// Border
+
+// --------------------------------
+
+[class^="border-"], [class*=" border-"] {
+ --border-o: 1;
+}
+
+.border { border: var(--border-width, 1px) var(--border-style, solid) alpha(var(--color-contrast-lower), var(--border-o, 1)); }
+.border-top { border-top: var(--border-width, 1px) var(--border-style, solid) alpha(var(--color-contrast-lower), var(--border-o, 1)); }
+.border-bottom { border-bottom: var(--border-width, 1px) var(--border-style, solid) alpha(var(--color-contrast-lower), var(--border-o, 1)); }
+.border-left { border-left: var(--border-width, 1px) var(--border-style, solid) alpha(var(--color-contrast-lower), var(--border-o, 1)); }
+.border-right { border-right: var(--border-width, 1px) var(--border-style, solid) alpha(var(--color-contrast-lower), var(--border-o, 1)); }
+
+.border-2 { --border-width: 2px; }
+.border-3 { --border-width: 3px; }
+.border-4 { --border-width: 4px; }
+.border-dotted { --border-style: dotted; }
+.border-dashed { --border-style: dashed; }
+
+.border-bg-darker { border-color: alpha(var(--color-bg-darker), var(--border-o, 1)); }
+.border-bg-dark { border-color: alpha(var(--color-bg-dark), var(--border-o, 1)); }
+.border-bg { border-color: alpha(var(--color-bg), var(--border-o, 1)); }
+.border-bg-light { border-color: alpha(var(--color-bg-light), var(--border-o, 1)); }
+.border-bg-lighter { border-color: alpha(var(--color-bg-lighter), var(--border-o, 1)); }
+
+.border-contrast-lower { border-color: alpha(var(--color-contrast-lower), var(--border-o, 1)); }
+.border-contrast-low { border-color: alpha(var(--color-contrast-low), var(--border-o, 1)); }
+.border-contrast-medium { border-color: alpha(var(--color-contrast-medium), var(--border-o, 1)); }
+.border-contrast-high { border-color: alpha(var(--color-contrast-high), var(--border-o, 1)); }
+.border-contrast-higher { border-color: alpha(var(--color-contrast-higher), var(--border-o, 1)); }
+
+.border-primary-darker { border-color: alpha(var(--color-primary-darker), var(--border-o, 1)); }
+.border-primary-dark { border-color: alpha(var(--color-primary-dark), var(--border-o, 1)); }
+.border-primary { border-color: alpha(var(--color-primary), var(--border-o, 1)); }
+.border-primary-light { border-color: alpha(var(--color-primary-light), var(--border-o, 1)); }
+.border-primary-lighter { border-color: alpha(var(--color-primary-lighter), var(--border-o, 1)); }
+
+.border-accent-darker { border-color: alpha(var(--color-accent-darker), var(--border-o, 1)); }
+.border-accent-dark { border-color: alpha(var(--color-accent-dark), var(--border-o, 1)); }
+.border-accent { border-color: alpha(var(--color-accent), var(--border-o, 1)); }
+.border-accent-light { border-color: alpha(var(--color-accent-light), var(--border-o, 1)); }
+.border-accent-lighter { border-color: alpha(var(--color-accent-lighter), var(--border-o, 1)); }
+
+.border-success-darker { border-color: alpha(var(--color-success-darker), var(--border-o, 1)); }
+.border-success-dark { border-color: alpha(var(--color-success-dark), var(--border-o, 1)); }
+.border-success { border-color: alpha(var(--color-success), var(--border-o, 1)); }
+.border-success-light { border-color: alpha(var(--color-success-light), var(--border-o, 1)); }
+.border-success-lighter { border-color: alpha(var(--color-success-lighter), var(--border-o, 1)); }
+
+.border-warning-darker { border-color: alpha(var(--color-warning-darker), var(--border-o, 1)); }
+.border-warning-dark { border-color: alpha(var(--color-warning-dark), var(--border-o, 1)); }
+.border-warning { border-color: alpha(var(--color-warning), var(--border-o, 1)); }
+.border-warning-light { border-color: alpha(var(--color-warning-light), var(--border-o, 1)); }
+.border-warning-lighter { border-color: alpha(var(--color-warning-lighter), var(--border-o, 1)); }
+
+.border-error-darker { border-color: alpha(var(--color-error-darker), var(--border-o, 1)); }
+.border-error-dark { border-color: alpha(var(--color-error-dark), var(--border-o, 1)); }
+.border-error { border-color: alpha(var(--color-error), var(--border-o, 1)); }
+.border-error-light { border-color: alpha(var(--color-error-light), var(--border-o, 1)); }
+.border-error-lighter { border-color: alpha(var(--color-error-lighter), var(--border-o, 1)); }
+
+.border-white { border-color: alpha(var(--color-white), var(--border-o, 1)); }
+.border-black { border-color: alpha(var(--color-black), var(--border-o, 1)); }
+
+.border-opacity-0 { --border-o: 0; }
+.border-opacity-10\% { --border-o: 0.1; }
+.border-opacity-20\% { --border-o: 0.2; }
+.border-opacity-30\% { --border-o: 0.3; }
+.border-opacity-40\% { --border-o: 0.4; }
+.border-opacity-50\% { --border-o: 0.5; }
+.border-opacity-60\% { --border-o: 0.6; }
+.border-opacity-70\% { --border-o: 0.7; }
+.border-opacity-80\% { --border-o: 0.8; }
+.border-opacity-90\% { --border-o: 0.9; }
+
+// --------------------------------
+
+// Border Radius
+
+// --------------------------------
+
+.radius-sm { border-radius: var(--radius-sm); }
+.radius-md { border-radius: var(--radius-md); }
+.radius-lg { border-radius: var(--radius-lg); }
+.radius-50\% { border-radius: 50%; }
+.radius-full { border-radius: 50em; }
+.radius-0 { border-radius: 0; }
+.radius-inherit { border-radius: inherit; }
+.radius-top-left-0 { border-top-left-radius: 0; }
+.radius-top-right-0 { border-top-right-radius: 0; }
+.radius-bottom-right-0 { border-bottom-right-radius: 0; }
+.radius-bottom-left-0 { border-bottom-left-radius: 0; }
+
+// --------------------------------
+
+// Background
+
+// --------------------------------
+
+.bg, [class^="bg-"], [class*=" bg-"] { --bg-o: 1; }
+
+.bg-transparent { background-color: transparent; }
+.bg-inherit { background-color: inherit; }
+
+.bg-darker { background-color: alpha(var(--color-bg-darker), var(--bg-o)); }
+.bg-dark { background-color: alpha(var(--color-bg-dark), var(--bg-o)); }
+.bg { background-color: alpha(var(--color-bg), var(--bg-o)); }
+.bg-light { background-color: alpha(var(--color-bg-light), var(--bg-o)); }
+.bg-lighter { background-color: alpha(var(--color-bg-lighter), var(--bg-o)); }
+
+.bg-contrast-lower { background-color: alpha(var(--color-contrast-lower), var(--bg-o, 1)); }
+.bg-contrast-low { background-color: alpha(var(--color-contrast-low), var(--bg-o, 1)); }
+.bg-contrast-medium { background-color: alpha(var(--color-contrast-medium), var(--bg-o, 1)); }
+.bg-contrast-high { background-color: alpha(var(--color-contrast-high), var(--bg-o, 1)); }
+.bg-contrast-higher { background-color: alpha(var(--color-contrast-higher), var(--bg-o, 1)); }
+
+.bg-primary-darker { background-color: alpha(var(--color-primary-darker), var(--bg-o, 1)); }
+.bg-primary-dark { background-color: alpha(var(--color-primary-dark), var(--bg-o, 1)); }
+.bg-primary { background-color: alpha(var(--color-primary), var(--bg-o, 1)); }
+.bg-primary-light { background-color: alpha(var(--color-primary-light), var(--bg-o, 1)); }
+.bg-primary-lighter { background-color: alpha(var(--color-primary-lighter), var(--bg-o, 1)); }
+
+.bg-accent-darker { background-color: alpha(var(--color-accent-darker), var(--bg-o, 1)); }
+.bg-accent-dark { background-color: alpha(var(--color-accent-dark), var(--bg-o, 1)); }
+.bg-accent { background-color: alpha(var(--color-accent), var(--bg-o, 1)); }
+.bg-accent-light { background-color: alpha(var(--color-accent-light), var(--bg-o, 1)); }
+.bg-accent-lighter { background-color: alpha(var(--color-accent-lighter), var(--bg-o, 1)); }
+
+.bg-success-darker { background-color: alpha(var(--color-success-darker), var(--bg-o, 1)); }
+.bg-success-dark { background-color: alpha(var(--color-success-dark), var(--bg-o, 1)); }
+.bg-success { background-color: alpha(var(--color-success), var(--bg-o, 1)); }
+.bg-success-light { background-color: alpha(var(--color-success-light), var(--bg-o, 1)); }
+.bg-success-lighter { background-color: alpha(var(--color-success-lighter), var(--bg-o, 1)); }
+
+.bg-warning-darker { background-color: alpha(var(--color-warning-darker), var(--bg-o, 1)); }
+.bg-warning-dark { background-color: alpha(var(--color-warning-dark), var(--bg-o, 1)); }
+.bg-warning { background-color: alpha(var(--color-warning), var(--bg-o, 1)); }
+.bg-warning-light { background-color: alpha(var(--color-warning-light), var(--bg-o, 1)); }
+.bg-warning-lighter { background-color: alpha(var(--color-warning-lighter), var(--bg-o, 1)); }
+
+.bg-error-darker { background-color: alpha(var(--color-error-darker), var(--bg-o, 1)); }
+.bg-error-dark { background-color: alpha(var(--color-error-dark), var(--bg-o, 1)); }
+.bg-error { background-color: alpha(var(--color-error), var(--bg-o, 1)); }
+.bg-error-light { background-color: alpha(var(--color-error-light), var(--bg-o, 1)); }
+.bg-error-lighter { background-color: alpha(var(--color-error-lighter), var(--bg-o, 1)); }
+
+.bg-white { background-color: alpha(var(--color-white), var(--bg-o, 1)); }
+.bg-black { background-color: alpha(var(--color-black), var(--bg-o, 1)); }
+
+.bg-opacity-0 { --bg-o: 0; }
+.bg-opacity-10\% { --bg-o: 0.1; }
+.bg-opacity-20\% { --bg-o: 0.2; }
+.bg-opacity-30\% { --bg-o: 0.3; }
+.bg-opacity-40\% { --bg-o: 0.4; }
+.bg-opacity-50\% { --bg-o: 0.5; }
+.bg-opacity-60\% { --bg-o: 0.6; }
+.bg-opacity-70\% { --bg-o: 0.7; }
+.bg-opacity-80\% { --bg-o: 0.8; }
+.bg-opacity-90\% { --bg-o: 0.9; }
+
+.bg-center { background-position: center; }
+.bg-top { background-position: center top; }
+.bg-right { background-position: right center; }
+.bg-bottom { background-position: center bottom; }
+.bg-left { background-position: left center; }
+.bg-top-left { background-position: left top; }
+.bg-top-right { background-position: right top; }
+.bg-bottom-left { background-position: left bottom; }
+.bg-bottom-right { background-position: right bottom; }
+
+.bg-cover { background-size: cover; }
+.bg-no-repeat { background-repeat: no-repeat; }
+
+// --------------------------------
+
+// Backdrop Filter
+
+// --------------------------------
+
+.backdrop-blur-10 { backdrop-filter: blur(10px); }
+.backdrop-blur-20 { backdrop-filter: blur(20px); }
+
+// --------------------------------
+
+// Mix-Blend Mode
+
+// --------------------------------
+
+.isolate { isolation: isolate; }
+.blend-multiply { mix-blend-mode: multiply; }
+.blend-overlay { mix-blend-mode: overlay; }
+.blend-difference { mix-blend-mode: difference; }
+
+// --------------------------------
+
+// Object-Fit
+
+// --------------------------------
+
+.object-contain { object-fit: contain; }
+.object-cover { object-fit: cover; }
+
+// --------------------------------
+
+// Perspective
+
+// --------------------------------
+
+.perspective-xs { perspective: 250px; }
+.perspective-sm { perspective: 500px; }
+.perspective-md { perspective: 1000px; }
+.perspective-lg { perspective: 1500px; }
+.perspective-xl { perspective: 3000px; }
+
+// --------------------------------
+
+// Transform
+
+// --------------------------------
+
+[class^="flip"], [class*=" flip"],
+[class^="-rotate"], [class*=" -rotate"],
+[class^="rotate"], [class*=" rotate"],
+[class^="-translate"], [class*=" -translate"],
+[class^="translate"], [class*=" translate"],
+[class^="-scale"], [class*=" -scale"],
+[class^="scale"], [class*=" scale"],
+[class^="-skew"], [class*=" -skew"]
+[class^="skew"], [class*=" skew"] {
+ --translate: 0;
+ --rotate: 0;
+ --skew: 0;
+ --scale: 1;
+
+ transform: translate3d(var(--translate-x, var(--translate)), var(--translate-y, var(--translate)), var(--translate-z, 0)) rotateX(var(--rotate-x, 0)) rotateY(var(--rotate-y, 0)) rotateZ(var(--rotate-z, var(--rotate))) skewX(var(--skew-x, var(--skew))) skewY(var(--skew-y, 0)) scaleX(var(--scale-x, var(--scale))) scaleY(var(--scale-y, var(--scale)));
+}
+
+.flip { --scale: -1; }
+.flip-x { --scale-x: -1; }
+.flip-y { --scale-y: -1; }
+
+.rotate-90 { --rotate: 90deg; }
+.rotate-180 { --rotate: 180deg; }
+.rotate-270 { --rotate: 270deg; }
+
+.-translate-50\% { --translate: -50%; }
+.-translate-x-50\% { --translate-x: -50%; }
+.-translate-y-50\% { --translate-y: -50%; }
+
+.translate-50\% { --translate: 50%; }
+.translate-x-50\% { --translate-x: 50%; }
+.translate-y-50\% { --translate-y: 50%; }
+
+// --------------------------------
+
+// Transform Origin
+
+// --------------------------------
+
+.origin-center { transform-origin: center; }
+.origin-top { transform-origin: center top; }
+.origin-right { transform-origin: right center; }
+.origin-bottom { transform-origin: center bottom; }
+.origin-left { transform-origin: left center; }
+.origin-top-left { transform-origin: left top; }
+.origin-top-right { transform-origin: right top; }
+.origin-bottom-left { transform-origin: left bottom; }
+.origin-bottom-right { transform-origin: right bottom; }
+
+// --------------------------------
+
+// SVG
+
+// --------------------------------
+
+.fill-current { fill: currentColor; }
+
+.stroke-current { stroke: currentColor; }
+
+.stroke-1 { stroke-width: 1px; }
+.stroke-2 { stroke-width: 2px; }
+.stroke-3 { stroke-width: 3px; }
+.stroke-4 { stroke-width: 4px; }
+
+// --------------------------------
+
+// Visibility
+
+// --------------------------------
+
+.visible { visibility: visible; }
+.invisible { visibility: hidden; }
+
+// --------------------------------
+
+// Responsive Variations
+
+// --------------------------------
+
+@each $breakpoint, $value in $breakpoints {
+ @include breakpoint(#{$breakpoint}) {
+ // flexbox
+ .flex\@#{$breakpoint} { display: flex; }
+ .inline-flex\@#{$breakpoint} { display: inline-flex; }
+ .flex-wrap\@#{$breakpoint} { flex-wrap: wrap; }
+ .flex-nowrap\@#{$breakpoint} { flex-wrap:nowrap; }
+ .flex-column\@#{$breakpoint} { flex-direction: column; }
+ .flex-column-reverse\@#{$breakpoint} { flex-direction: column-reverse; }
+ .flex-row\@#{$breakpoint} { flex-direction: row; }
+ .flex-row-reverse\@#{$breakpoint} { flex-direction: row-reverse; }
+ .flex-center\@#{$breakpoint} { justify-content: center; align-items: center; }
+
+ .flex-grow\@#{$breakpoint} { flex-grow: 1; }
+ .flex-grow-0\@#{$breakpoint} { flex-grow: 0; }
+ .flex-shrink\@#{$breakpoint} { flex-shrink: 1; }
+ .flex-shrink-0\@#{$breakpoint} { flex-shrink: 0; }
+ .flex-basis-0\@#{$breakpoint} { flex-basis: 0; }
+
+ // justify-content
+ .justify-start\@#{$breakpoint} { justify-content: flex-start; }
+ .justify-end\@#{$breakpoint} { justify-content: flex-end; }
+ .justify-center\@#{$breakpoint} { justify-content: center; }
+ .justify-between\@#{$breakpoint} { justify-content: space-between; }
+
+ // align-items
+ .items-center\@#{$breakpoint} { align-items: center; }
+ .items-start\@#{$breakpoint} { align-items: flex-start; }
+ .items-end\@#{$breakpoint} { align-items: flex-end; }
+ .items-baseline\@#{$breakpoint} { align-items: baseline; }
+
+ // order
+ .order-1\@#{$breakpoint} { order: 1; }
+ .order-2\@#{$breakpoint} { order: 2; }
+ .order-3\@#{$breakpoint} { order: 3; }
+
+ // display
+ .block\@#{$breakpoint} { display: block; }
+ .inline-block\@#{$breakpoint} { display: inline-block; }
+ .inline\@#{$breakpoint} { display: inline; }
+ .contents\@#{$breakpoint} { display: contents; }
+ .hide\@#{$breakpoint} { display: none !important; }
+
+ // margin
+ .margin-xxxxs\@#{$breakpoint} { margin: var(--space-xxxxs); }
+ .margin-xxxs\@#{$breakpoint} { margin: var(--space-xxxs); }
+ .margin-xxs\@#{$breakpoint} { margin: var(--space-xxs); }
+ .margin-xs\@#{$breakpoint} { margin: var(--space-xs); }
+ .margin-sm\@#{$breakpoint} { margin: var(--space-sm); }
+ .margin-md\@#{$breakpoint} { margin: var(--space-md); }
+ .margin-lg\@#{$breakpoint} { margin: var(--space-lg); }
+ .margin-xl\@#{$breakpoint} { margin: var(--space-xl); }
+ .margin-xxl\@#{$breakpoint} { margin: var(--space-xxl); }
+ .margin-xxxl\@#{$breakpoint} { margin: var(--space-xxxl); }
+ .margin-xxxxl\@#{$breakpoint} { margin: var(--space-xxxxl); }
+ .margin-auto\@#{$breakpoint} { margin: auto; }
+ .margin-0\@#{$breakpoint} { margin: 0; }
+
+ .margin-top-xxxxs\@#{$breakpoint} { margin-top: var(--space-xxxxs); }
+ .margin-top-xxxs\@#{$breakpoint} { margin-top: var(--space-xxxs); }
+ .margin-top-xxs\@#{$breakpoint} { margin-top: var(--space-xxs); }
+ .margin-top-xs\@#{$breakpoint} { margin-top: var(--space-xs); }
+ .margin-top-sm\@#{$breakpoint} { margin-top: var(--space-sm); }
+ .margin-top-md\@#{$breakpoint} { margin-top: var(--space-md); }
+ .margin-top-lg\@#{$breakpoint} { margin-top: var(--space-lg); }
+ .margin-top-xl\@#{$breakpoint} { margin-top: var(--space-xl); }
+ .margin-top-xxl\@#{$breakpoint} { margin-top: var(--space-xxl); }
+ .margin-top-xxxl\@#{$breakpoint} { margin-top: var(--space-xxxl); }
+ .margin-top-xxxxl\@#{$breakpoint} { margin-top: var(--space-xxxxl); }
+ .margin-top-auto\@#{$breakpoint} { margin-top: auto; }
+ .margin-top-0\@#{$breakpoint} { margin-top: 0; }
+
+ .margin-bottom-xxxxs\@#{$breakpoint} { margin-bottom: var(--space-xxxxs); }
+ .margin-bottom-xxxs\@#{$breakpoint} { margin-bottom: var(--space-xxxs); }
+ .margin-bottom-xxs\@#{$breakpoint} { margin-bottom: var(--space-xxs); }
+ .margin-bottom-xs\@#{$breakpoint} { margin-bottom: var(--space-xs); }
+ .margin-bottom-sm\@#{$breakpoint} { margin-bottom: var(--space-sm); }
+ .margin-bottom-md\@#{$breakpoint} { margin-bottom: var(--space-md); }
+ .margin-bottom-lg\@#{$breakpoint} { margin-bottom: var(--space-lg); }
+ .margin-bottom-xl\@#{$breakpoint} { margin-bottom: var(--space-xl); }
+ .margin-bottom-xxl\@#{$breakpoint} { margin-bottom: var(--space-xxl); }
+ .margin-bottom-xxxl\@#{$breakpoint} { margin-bottom: var(--space-xxxl); }
+ .margin-bottom-xxxxl\@#{$breakpoint} { margin-bottom: var(--space-xxxxl); }
+ .margin-bottom-auto\@#{$breakpoint} { margin-bottom: auto; }
+ .margin-bottom-0\@#{$breakpoint} { margin-bottom: 0; }
+
+ .margin-right-xxxxs\@#{$breakpoint} { margin-right: var(--space-xxxxs); }
+ .margin-right-xxxs\@#{$breakpoint} { margin-right: var(--space-xxxs); }
+ .margin-right-xxs\@#{$breakpoint} { margin-right: var(--space-xxs); }
+ .margin-right-xs\@#{$breakpoint} { margin-right: var(--space-xs); }
+ .margin-right-sm\@#{$breakpoint} { margin-right: var(--space-sm); }
+ .margin-right-md\@#{$breakpoint} { margin-right: var(--space-md); }
+ .margin-right-lg\@#{$breakpoint} { margin-right: var(--space-lg); }
+ .margin-right-xl\@#{$breakpoint} { margin-right: var(--space-xl); }
+ .margin-right-xxl\@#{$breakpoint} { margin-right: var(--space-xxl); }
+ .margin-right-xxxl\@#{$breakpoint} { margin-right: var(--space-xxxl); }
+ .margin-right-xxxxl\@#{$breakpoint} { margin-right: var(--space-xxxxl); }
+ .margin-right-auto\@#{$breakpoint} { margin-right: auto; }
+ .margin-right-0\@#{$breakpoint} { margin-right: 0; }
+
+ .margin-left-xxxxs\@#{$breakpoint} { margin-left: var(--space-xxxxs); }
+ .margin-left-xxxs\@#{$breakpoint} { margin-left: var(--space-xxxs); }
+ .margin-left-xxs\@#{$breakpoint} { margin-left: var(--space-xxs); }
+ .margin-left-xs\@#{$breakpoint} { margin-left: var(--space-xs); }
+ .margin-left-sm\@#{$breakpoint} { margin-left: var(--space-sm); }
+ .margin-left-md\@#{$breakpoint} { margin-left: var(--space-md); }
+ .margin-left-lg\@#{$breakpoint} { margin-left: var(--space-lg); }
+ .margin-left-xl\@#{$breakpoint} { margin-left: var(--space-xl); }
+ .margin-left-xxl\@#{$breakpoint} { margin-left: var(--space-xxl); }
+ .margin-left-xxxl\@#{$breakpoint} { margin-left: var(--space-xxxl); }
+ .margin-left-xxxxl\@#{$breakpoint} { margin-left: var(--space-xxxxl); }
+ .margin-left-auto\@#{$breakpoint} { margin-left: auto; }
+ .margin-left-0\@#{$breakpoint} { margin-left: 0; }
+
+ .margin-x-xxxxs\@#{$breakpoint} { margin-left: var(--space-xxxxs); margin-right: var(--space-xxxxs); }
+ .margin-x-xxxs\@#{$breakpoint} { margin-left: var(--space-xxxs); margin-right: var(--space-xxxs); }
+ .margin-x-xxs\@#{$breakpoint} { margin-left: var(--space-xxs); margin-right: var(--space-xxs); }
+ .margin-x-xs\@#{$breakpoint} { margin-left: var(--space-xs); margin-right: var(--space-xs); }
+ .margin-x-sm\@#{$breakpoint} { margin-left: var(--space-sm); margin-right: var(--space-sm); }
+ .margin-x-md\@#{$breakpoint} { margin-left: var(--space-md); margin-right: var(--space-md); }
+ .margin-x-lg\@#{$breakpoint} { margin-left: var(--space-lg); margin-right: var(--space-lg); }
+ .margin-x-xl\@#{$breakpoint} { margin-left: var(--space-xl); margin-right: var(--space-xl); }
+ .margin-x-xxl\@#{$breakpoint} { margin-left: var(--space-xxl); margin-right: var(--space-xxl); }
+ .margin-x-xxxl\@#{$breakpoint} { margin-left: var(--space-xxxl); margin-right: var(--space-xxxl); }
+ .margin-x-xxxxl\@#{$breakpoint} { margin-left: var(--space-xxxxl); margin-right: var(--space-xxxxl); }
+ .margin-x-auto\@#{$breakpoint} { margin-left: auto; margin-right: auto; }
+ .margin-x-0\@#{$breakpoint} { margin-left: 0; margin-right: 0; }
+
+ .margin-y-xxxxs\@#{$breakpoint} { margin-top: var(--space-xxxxs); margin-bottom: var(--space-xxxxs); }
+ .margin-y-xxxs\@#{$breakpoint} { margin-top: var(--space-xxxs); margin-bottom: var(--space-xxxs); }
+ .margin-y-xxs\@#{$breakpoint} { margin-top: var(--space-xxs); margin-bottom: var(--space-xxs); }
+ .margin-y-xs\@#{$breakpoint} { margin-top: var(--space-xs); margin-bottom: var(--space-xs); }
+ .margin-y-sm\@#{$breakpoint} { margin-top: var(--space-sm); margin-bottom: var(--space-sm); }
+ .margin-y-md\@#{$breakpoint} { margin-top: var(--space-md); margin-bottom: var(--space-md); }
+ .margin-y-lg\@#{$breakpoint} { margin-top: var(--space-lg); margin-bottom: var(--space-lg); }
+ .margin-y-xl\@#{$breakpoint} { margin-top: var(--space-xl); margin-bottom: var(--space-xl); }
+ .margin-y-xxl\@#{$breakpoint} { margin-top: var(--space-xxl); margin-bottom: var(--space-xxl); }
+ .margin-y-xxxl\@#{$breakpoint} { margin-top: var(--space-xxxl); margin-bottom: var(--space-xxxl); }
+ .margin-y-xxxxl\@#{$breakpoint} { margin-top: var(--space-xxxxl); margin-bottom: var(--space-xxxxl); }
+ .margin-y-auto\@#{$breakpoint} { margin-top: auto; margin-bottom: auto; }
+ .margin-y-0\@#{$breakpoint} { margin-top: 0; margin-bottom: 0; }
+
+ // padding
+ .padding-xxxxs\@#{$breakpoint} { padding: var(--space-xxxxs); }
+ .padding-xxxs\@#{$breakpoint} { padding: var(--space-xxxs); }
+ .padding-xxs\@#{$breakpoint} { padding: var(--space-xxs); }
+ .padding-xs\@#{$breakpoint} { padding: var(--space-xs); }
+ .padding-sm\@#{$breakpoint} { padding: var(--space-sm); }
+ .padding-md\@#{$breakpoint} { padding: var(--space-md); }
+ .padding-lg\@#{$breakpoint} { padding: var(--space-lg); }
+ .padding-xl\@#{$breakpoint} { padding: var(--space-xl); }
+ .padding-xxl\@#{$breakpoint} { padding: var(--space-xxl); }
+ .padding-xxxl\@#{$breakpoint} { padding: var(--space-xxxl); }
+ .padding-xxxxl\@#{$breakpoint} { padding: var(--space-xxxxl); }
+ .padding-0\@#{$breakpoint} { padding: 0; }
+ .padding-component\@#{$breakpoint} { padding: var(--component-padding); }
+
+ .padding-top-xxxxs\@#{$breakpoint} { padding-top: var(--space-xxxxs); }
+ .padding-top-xxxs\@#{$breakpoint} { padding-top: var(--space-xxxs); }
+ .padding-top-xxs\@#{$breakpoint} { padding-top: var(--space-xxs); }
+ .padding-top-xs\@#{$breakpoint} { padding-top: var(--space-xs); }
+ .padding-top-sm\@#{$breakpoint} { padding-top: var(--space-sm); }
+ .padding-top-md\@#{$breakpoint} { padding-top: var(--space-md); }
+ .padding-top-lg\@#{$breakpoint} { padding-top: var(--space-lg); }
+ .padding-top-xl\@#{$breakpoint} { padding-top: var(--space-xl); }
+ .padding-top-xxl\@#{$breakpoint} { padding-top: var(--space-xxl); }
+ .padding-top-xxxl\@#{$breakpoint} { padding-top: var(--space-xxxl); }
+ .padding-top-xxxxl\@#{$breakpoint} { padding-top: var(--space-xxxxl); }
+ .padding-top-0\@#{$breakpoint} { padding-top: 0; }
+ .padding-top-component\@#{$breakpoint} { padding-top: var(--component-padding); }
+
+ .padding-bottom-xxxxs\@#{$breakpoint} { padding-bottom: var(--space-xxxxs); }
+ .padding-bottom-xxxs\@#{$breakpoint} { padding-bottom: var(--space-xxxs); }
+ .padding-bottom-xxs\@#{$breakpoint} { padding-bottom: var(--space-xxs); }
+ .padding-bottom-xs\@#{$breakpoint} { padding-bottom: var(--space-xs); }
+ .padding-bottom-sm\@#{$breakpoint} { padding-bottom: var(--space-sm); }
+ .padding-bottom-md\@#{$breakpoint} { padding-bottom: var(--space-md); }
+ .padding-bottom-lg\@#{$breakpoint} { padding-bottom: var(--space-lg); }
+ .padding-bottom-xl\@#{$breakpoint} { padding-bottom: var(--space-xl); }
+ .padding-bottom-xxl\@#{$breakpoint} { padding-bottom: var(--space-xxl); }
+ .padding-bottom-xxxl\@#{$breakpoint} { padding-bottom: var(--space-xxxl); }
+ .padding-bottom-xxxxl\@#{$breakpoint} { padding-bottom: var(--space-xxxxl); }
+ .padding-bottom-0\@#{$breakpoint} { padding-bottom: 0; }
+ .padding-bottom-component\@#{$breakpoint} { padding-bottom: var(--component-padding); }
+
+ .padding-right-xxxxs\@#{$breakpoint} { padding-right: var(--space-xxxxs); }
+ .padding-right-xxxs\@#{$breakpoint} { padding-right: var(--space-xxxs); }
+ .padding-right-xxs\@#{$breakpoint} { padding-right: var(--space-xxs); }
+ .padding-right-xs\@#{$breakpoint} { padding-right: var(--space-xs); }
+ .padding-right-sm\@#{$breakpoint} { padding-right: var(--space-sm); }
+ .padding-right-md\@#{$breakpoint} { padding-right: var(--space-md); }
+ .padding-right-lg\@#{$breakpoint} { padding-right: var(--space-lg); }
+ .padding-right-xl\@#{$breakpoint} { padding-right: var(--space-xl); }
+ .padding-right-xxl\@#{$breakpoint} { padding-right: var(--space-xxl); }
+ .padding-right-xxxl\@#{$breakpoint} { padding-right: var(--space-xxxl); }
+ .padding-right-xxxxl\@#{$breakpoint} { padding-right: var(--space-xxxxl); }
+ .padding-right-0\@#{$breakpoint} { padding-right: 0; }
+ .padding-right-component\@#{$breakpoint} { padding-right: var(--component-padding); }
+
+ .padding-left-xxxxs\@#{$breakpoint} { padding-left: var(--space-xxxxs); }
+ .padding-left-xxxs\@#{$breakpoint} { padding-left: var(--space-xxxs); }
+ .padding-left-xxs\@#{$breakpoint} { padding-left: var(--space-xxs); }
+ .padding-left-xs\@#{$breakpoint} { padding-left: var(--space-xs); }
+ .padding-left-sm\@#{$breakpoint} { padding-left: var(--space-sm); }
+ .padding-left-md\@#{$breakpoint} { padding-left: var(--space-md); }
+ .padding-left-lg\@#{$breakpoint} { padding-left: var(--space-lg); }
+ .padding-left-xl\@#{$breakpoint} { padding-left: var(--space-xl); }
+ .padding-left-xxl\@#{$breakpoint} { padding-left: var(--space-xxl); }
+ .padding-left-xxxl\@#{$breakpoint} { padding-left: var(--space-xxxl); }
+ .padding-left-xxxxl\@#{$breakpoint} { padding-left: var(--space-xxxxl); }
+ .padding-left-0\@#{$breakpoint} { padding-left: 0; }
+ .padding-left-component\@#{$breakpoint} { padding-left: var(--component-padding); }
+
+ .padding-x-xxxxs\@#{$breakpoint} { padding-left: var(--space-xxxxs); padding-right: var(--space-xxxxs); }
+ .padding-x-xxxs\@#{$breakpoint} { padding-left: var(--space-xxxs); padding-right: var(--space-xxxs); }
+ .padding-x-xxs\@#{$breakpoint} { padding-left: var(--space-xxs); padding-right: var(--space-xxs); }
+ .padding-x-xs\@#{$breakpoint} { padding-left: var(--space-xs); padding-right: var(--space-xs); }
+ .padding-x-sm\@#{$breakpoint} { padding-left: var(--space-sm); padding-right: var(--space-sm); }
+ .padding-x-md\@#{$breakpoint} { padding-left: var(--space-md); padding-right: var(--space-md); }
+ .padding-x-lg\@#{$breakpoint} { padding-left: var(--space-lg); padding-right: var(--space-lg); }
+ .padding-x-xl\@#{$breakpoint} { padding-left: var(--space-xl); padding-right: var(--space-xl); }
+ .padding-x-xxl\@#{$breakpoint} { padding-left: var(--space-xxl); padding-right: var(--space-xxl); }
+ .padding-x-xxxl\@#{$breakpoint} { padding-left: var(--space-xxxl); padding-right: var(--space-xxxl); }
+ .padding-x-xxxxl\@#{$breakpoint} { padding-left: var(--space-xxxxl); padding-right: var(--space-xxxxl); }
+ .padding-x-0\@#{$breakpoint} { padding-left: 0; padding-right: 0; }
+ .padding-x-component\@#{$breakpoint} { padding-left: var(--component-padding); padding-right: var(--component-padding); }
+
+ .padding-y-xxxxs\@#{$breakpoint} { padding-top: var(--space-xxxxs); padding-bottom: var(--space-xxxxs); }
+ .padding-y-xxxs\@#{$breakpoint} { padding-top: var(--space-xxxs); padding-bottom: var(--space-xxxs); }
+ .padding-y-xxs\@#{$breakpoint} { padding-top: var(--space-xxs); padding-bottom: var(--space-xxs); }
+ .padding-y-xs\@#{$breakpoint} { padding-top: var(--space-xs); padding-bottom: var(--space-xs); }
+ .padding-y-sm\@#{$breakpoint} { padding-top: var(--space-sm); padding-bottom: var(--space-sm); }
+ .padding-y-md\@#{$breakpoint} { padding-top: var(--space-md); padding-bottom: var(--space-md); }
+ .padding-y-lg\@#{$breakpoint} { padding-top: var(--space-lg); padding-bottom: var(--space-lg); }
+ .padding-y-xl\@#{$breakpoint} { padding-top: var(--space-xl); padding-bottom: var(--space-xl); }
+ .padding-y-xxl\@#{$breakpoint} { padding-top: var(--space-xxl); padding-bottom: var(--space-xxl); }
+ .padding-y-xxxl\@#{$breakpoint} { padding-top: var(--space-xxxl); padding-bottom: var(--space-xxxl); }
+ .padding-y-xxxxl\@#{$breakpoint} { padding-top: var(--space-xxxxl); padding-bottom: var(--space-xxxxl); }
+ .padding-y-0\@#{$breakpoint} { padding-top: 0; padding-bottom: 0; }
+ .padding-y-component\@#{$breakpoint} { padding-top: var(--component-padding); padding-bottom: var(--component-padding); }
+
+ // text-align
+ .text-center\@#{$breakpoint} { text-align: center; }
+ .text-left\@#{$breakpoint} { text-align: left; }
+ .text-right\@#{$breakpoint} { text-align: right; }
+ .text-justify\@#{$breakpoint} { text-align: justify; }
+
+ // font-size
+ .text-xs\@#{$breakpoint} { font-size: var(--text-xs, 0.6875rem); }
+ .text-sm\@#{$breakpoint} { font-size: var(--text-sm, 0.75rem); }
+ .text-base\@#{$breakpoint} { font-size: var(--text-unit, 1rem); }
+ .text-md\@#{$breakpoint} { font-size: var(--text-md, 1.125rem); }
+ .text-lg\@#{$breakpoint} { font-size: var(--text-lg, 1.375rem); }
+ .text-xl\@#{$breakpoint} { font-size: var(--text-xl, 1.75rem); }
+ .text-xxl\@#{$breakpoint} { font-size: var(--text-xxl, 2rem); }
+ .text-xxxl\@#{$breakpoint} { font-size: var(--text-xxxl, 2.5rem); }
+ .text-xxxxl\@#{$breakpoint} { font-size: var(--text-xxxxl, 3rem); }
+
+ // width
+ .width-xxxxs\@#{$breakpoint} { width: var(--size-xxxxs, 0.25rem); }
+ .width-xxxs\@#{$breakpoint} { width: var(--size-xxxs, 0.5rem); }
+ .width-xxs\@#{$breakpoint} { width: var(--size-xxs, 0.75rem); }
+ .width-xs\@#{$breakpoint} { width: var(--size-xs, 1rem); }
+ .width-sm\@#{$breakpoint} { width: var(--size-sm, 1.5rem); }
+ .width-md\@#{$breakpoint} { width: var(--size-md, 2rem); }
+ .width-lg\@#{$breakpoint} { width: var(--size-lg, 3rem); }
+ .width-xl\@#{$breakpoint} { width: var(--size-xl, 4rem); }
+ .width-xxl\@#{$breakpoint} { width: var(--size-xxl, 6rem); }
+ .width-xxxl\@#{$breakpoint} { width: var(--size-xxxl, 8rem); }
+ .width-xxxxl\@#{$breakpoint} { width: var(--size-xxxxl, 16rem); }
+ .width-0\@#{$breakpoint} { width: 0; }
+ .width-10\%\@#{$breakpoint} { width: 10%; }
+ .width-20\%\@#{$breakpoint} { width: 20%; }
+ .width-25\%\@#{$breakpoint} { width: 25%; }
+ .width-30\%\@#{$breakpoint} { width: 30%; }
+ .width-33\%\@#{$breakpoint} { width: calc(100% / 3); }
+ .width-40\%\@#{$breakpoint} { width: 40%; }
+ .width-50\%\@#{$breakpoint} { width: 50%; }
+ .width-60\%\@#{$breakpoint} { width: 60%; }
+ .width-66\%\@#{$breakpoint} { width: calc(100% / 1.5); }
+ .width-70\%\@#{$breakpoint} { width: 70%; }
+ .width-75\%\@#{$breakpoint} { width: 75%; }
+ .width-80\%\@#{$breakpoint} { width: 80%; }
+ .width-90\%\@#{$breakpoint} { width: 90%; }
+ .width-100\%\@#{$breakpoint} { width: 100%; }
+ .width-100vw\@#{$breakpoint} { width: 100vw; }
+ .width-auto\@#{$breakpoint} { width: auto; }
+
+ // height
+ .height-xxxxs\@#{$breakpoint} { height: var(--size-xxxxs, 0.25rem); }
+ .height-xxxs\@#{$breakpoint} { height: var(--size-xxxs, 0.5rem); }
+ .height-xxs\@#{$breakpoint} { height: var(--size-xxs, 0.75rem); }
+ .height-xs\@#{$breakpoint} { height: var(--size-xs, 1rem); }
+ .height-sm\@#{$breakpoint} { height: var(--size-sm, 1.5rem); }
+ .height-md\@#{$breakpoint} { height: var(--size-md, 2rem); }
+ .height-lg\@#{$breakpoint} { height: var(--size-lg, 3rem); }
+ .height-xl\@#{$breakpoint} { height: var(--size-xl, 4rem); }
+ .height-xxl\@#{$breakpoint} { height: var(--size-xxl, 6rem); }
+ .height-xxxl\@#{$breakpoint} { height: var(--size-xxxl, 8rem); }
+ .height-xxxxl\@#{$breakpoint} { height: var(--size-xxxxl, 16rem); }
+ .height-0\@#{$breakpoint} { height: 0; }
+ .height-10\%\@#{$breakpoint} { height: 10%; }
+ .height-20\%\@#{$breakpoint} { height: 20%; }
+ .height-25\%\@#{$breakpoint} { height: 25%; }
+ .height-30\%\@#{$breakpoint} { height: 30%; }
+ .height-33\%\@#{$breakpoint} { height: calc(100% / 3); }
+ .height-40\%\@#{$breakpoint} { height: 40%; }
+ .height-50\%\@#{$breakpoint} { height: 50%; }
+ .height-60\%\@#{$breakpoint} { height: 60%; }
+ .height-66\%\@#{$breakpoint} { height: calc(100% / 1.5); }
+ .height-70\%\@#{$breakpoint} { height: 70%; }
+ .height-75\%\@#{$breakpoint} { height: 75%; }
+ .height-80\%\@#{$breakpoint} { height: 80%; }
+ .height-90\%\@#{$breakpoint} { height: 90%; }
+ .height-100\%\@#{$breakpoint} { height: 100%; }
+ .height-100vh\@#{$breakpoint} { height: 100vh; }
+ .height-auto\@#{$breakpoint} { height: auto; }
+
+ // max-width
+ .max-width-xxxxs\@#{$breakpoint} { max-width: var(--max-width-xxxxs); }
+ .max-width-xxxs\@#{$breakpoint} { max-width: var(--max-width-xxxs); }
+ .max-width-xxs\@#{$breakpoint} { max-width: var(--max-width-xxs); }
+ .max-width-xs\@#{$breakpoint} { max-width: var(--max-width-xs); }
+ .max-width-sm\@#{$breakpoint} { max-width: var(--max-width-sm); }
+ .max-width-md\@#{$breakpoint} { max-width: var(--max-width-md); }
+ .max-width-lg\@#{$breakpoint} { max-width: var(--max-width-lg); }
+ .max-width-xl\@#{$breakpoint} { max-width: var(--max-width-xl); }
+ .max-width-xxl\@#{$breakpoint} { max-width: var(--max-width-xxl); }
+ .max-width-xxxl\@#{$breakpoint} { max-width: var(--max-width-xxxl); }
+ .max-width-xxxxl\@#{$breakpoint} { max-width: var(--max-width-xxxxl); }
+ .max-width-100\%\@#{$breakpoint} { max-width: 100%; }
+ .max-width-none\@#{$breakpoint} { max-width: none; }
+
+ // position
+ .position-relative\@#{$breakpoint} { position: relative; }
+ .position-absolute\@#{$breakpoint} { position: absolute; }
+ .position-fixed\@#{$breakpoint} { position: fixed; }
+ .position-sticky\@#{$breakpoint} { position: sticky; }
+ .position-static\@#{$breakpoint} { position: static; }
+
+ .inset-0\@#{$breakpoint} { top: 0; right: 0; bottom: 0; left: 0; }
+
+ .top-0\@#{$breakpoint} { top: 0; }
+ .top-50\%\@#{$breakpoint} { top: 50%; }
+ .top-xxxxs\@#{$breakpoint} { top: var(--space-xxxxs); }
+ .top-xxxs\@#{$breakpoint} { top: var(--space-xxxs); }
+ .top-xxs\@#{$breakpoint} { top: var(--space-xxs); }
+ .top-xs\@#{$breakpoint} { top: var(--space-xs); }
+ .top-sm\@#{$breakpoint} { top: var(--space-sm); }
+ .top-md\@#{$breakpoint} { top: var(--space-md); }
+ .top-lg\@#{$breakpoint} { top: var(--space-lg); }
+ .top-xl\@#{$breakpoint} { top: var(--space-xl); }
+ .top-xxl\@#{$breakpoint} { top: var(--space-xxl); }
+ .top-xxxl\@#{$breakpoint} { top: var(--space-xxxl); }
+ .top-xxxxl\@#{$breakpoint} { top: var(--space-xxxxl); }
+
+ .bottom-0\@#{$breakpoint} { bottom: 0; }
+ .bottom-unset\@#{$breakpoint} { bottom: unset; }
+ .bottom-50\%\@#{$breakpoint} { bottom: 50%; }
+ .bottom-xxxxs\@#{$breakpoint} { bottom: var(--space-xxxxs); }
+ .bottom-xxxs\@#{$breakpoint} { bottom: var(--space-xxxs); }
+ .bottom-xxs\@#{$breakpoint} { bottom: var(--space-xxs); }
+ .bottom-xs\@#{$breakpoint} { bottom: var(--space-xs); }
+ .bottom-sm\@#{$breakpoint} { bottom: var(--space-sm); }
+ .bottom-md\@#{$breakpoint} { bottom: var(--space-md); }
+ .bottom-lg\@#{$breakpoint} { bottom: var(--space-lg); }
+ .bottom-xl\@#{$breakpoint} { bottom: var(--space-xl); }
+ .bottom-xxl\@#{$breakpoint} { bottom: var(--space-xxl); }
+ .bottom-xxxl\@#{$breakpoint} { bottom: var(--space-xxxl); }
+ .bottom-xxxxl\@#{$breakpoint} { bottom: var(--space-xxxxl); }
+
+ .right-0\@#{$breakpoint} { right: 0; }
+ .right-50\%\@#{$breakpoint} { right: 50%; }
+ .right-xxxxs\@#{$breakpoint} { right: var(--space-xxxxs); }
+ .right-xxxs\@#{$breakpoint} { right: var(--space-xxxs); }
+ .right-xxs\@#{$breakpoint} { right: var(--space-xxs); }
+ .right-xs\@#{$breakpoint} { right: var(--space-xs); }
+ .right-sm\@#{$breakpoint} { right: var(--space-sm); }
+ .right-md\@#{$breakpoint} { right: var(--space-md); }
+ .right-lg\@#{$breakpoint} { right: var(--space-lg); }
+ .right-xl\@#{$breakpoint} { right: var(--space-xl); }
+ .right-xxl\@#{$breakpoint} { right: var(--space-xxl); }
+ .right-xxxl\@#{$breakpoint} { right: var(--space-xxxl); }
+ .right-xxxxl\@#{$breakpoint} { right: var(--space-xxxxl); }
+
+ .left-0\@#{$breakpoint} { left: 0; }
+ .left-50\%\@#{$breakpoint} { left: 50%; }
+ .left-xxxxs\@#{$breakpoint} { left: var(--space-xxxxs); }
+ .left-xxxs\@#{$breakpoint} { left: var(--space-xxxs); }
+ .left-xxs\@#{$breakpoint} { left: var(--space-xxs); }
+ .left-xs\@#{$breakpoint} { left: var(--space-xs); }
+ .left-sm\@#{$breakpoint} { left: var(--space-sm); }
+ .left-md\@#{$breakpoint} { left: var(--space-md); }
+ .left-lg\@#{$breakpoint} { left: var(--space-lg); }
+ .left-xl\@#{$breakpoint} { left: var(--space-xl); }
+ .left-xxl\@#{$breakpoint} { left: var(--space-xxl); }
+ .left-xxxl\@#{$breakpoint} { left: var(--space-xxxl); }
+ .left-xxxxl\@#{$breakpoint} { left: var(--space-xxxxl); }
+
+ // overflow
+ .overflow-hidden\@#{$breakpoint} { overflow: hidden; }
+ .overflow-auto\@#{$breakpoint} { overflow: auto; }
+ .momentum-scrolling\@#{$breakpoint} { -webkit-overflow-scrolling: touch; }
+ .overscroll-contain\@#{$breakpoint} { overscroll-behavior: contain; }
+
+ // visibility
+ .visible\@#{$breakpoint} { visibility: visible; }
+ .invisible\@#{$breakpoint} { visibility: hidden; }
+ }
+
+ @include breakpoint(#{$breakpoint}, "not all") {
+ .display\@#{$breakpoint} { display: none !important; }
+ }
+}
diff --git a/apps/web-shared/src/styles/base/_visibility.scss b/apps/web-shared/src/styles/base/_visibility.scss
new file mode 100644
index 0000000..ab6a516
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_visibility.scss
@@ -0,0 +1,23 @@
+:root {
+ --display: block;
+}
+
+.is-visible {
+ display: var(--display) !important;
+}
+
+.is-hidden {
+ display: none !important;
+}
+
+html:not(.js) {
+ .no-js\:is-hidden {
+ display: none !important;
+ }
+}
+
+@media print {
+ .print\:is-hidden {
+ display: none !important;
+ }
+} \ No newline at end of file
diff --git a/apps/web-shared/src/styles/base/_z-index.scss b/apps/web-shared/src/styles/base/_z-index.scss
new file mode 100644
index 0000000..5af9ff3
--- /dev/null
+++ b/apps/web-shared/src/styles/base/_z-index.scss
@@ -0,0 +1,6 @@
+:root {
+ --z-index-header: 3; // e.g., main header
+ --z-index-popover: 5; // e.g., tooltips and dropdown
+ --z-index-fixed-element: 10; // e.g., 'back to top' button
+ --z-index-overlay: 15; // e.g., modals and dialogs
+} \ No newline at end of file
diff --git a/apps/web-shared/src/styles/components/alert.scss b/apps/web-shared/src/styles/components/alert.scss
new file mode 100644
index 0000000..9d9008d
--- /dev/null
+++ b/apps/web-shared/src/styles/components/alert.scss
@@ -0,0 +1,69 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_alert
+Title: Alert
+Descr: Feedback message
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.alert {
+ background-color: alpha(var(--color-primary), 0.2);
+ color: var(--color-contrast-higher);
+
+ // hide element
+ position: absolute;
+ clip: rect(1px, 1px, 1px, 1px);
+ clip-path: inset(50%);
+}
+
+.alert__icon {
+ color: var(--color-primary);
+}
+
+.alert__close-btn {
+ display: inline-block;
+
+ .icon {
+ display: block;
+ }
+
+ &:hover {
+ opacity: 0.7;
+ }
+}
+
+// themes
+.alert--success {
+ background-color: alpha(var(--color-success), 0.2);
+
+ .alert__icon {
+ color: var(--color-success);
+ }
+}
+
+.alert--error {
+ background-color: alpha(var(--color-error), 0.2);
+
+ .alert__icon {
+ color: var(--color-error);
+ }
+}
+
+.alert--warning {
+ background-color: alpha(var(--color-warning), 0.2);
+
+ .alert__icon {
+ color: var(--color-warning);
+ }
+}
+
+// toggle visibility
+.alert--is-visible {
+ position: static;
+ clip: auto;
+ clip-path: none;
+}
+
diff --git a/apps/web-shared/src/styles/components/autocomplete.scss b/apps/web-shared/src/styles/components/autocomplete.scss
new file mode 100644
index 0000000..cde3632
--- /dev/null
+++ b/apps/web-shared/src/styles/components/autocomplete.scss
@@ -0,0 +1,76 @@
+@use '../base' as *;
+@use 'circle-loader.scss' as *;
+
+/* --------------------------------
+
+File#: _2_autocomplete
+Title: Autocomplete
+Descr: Autocomplete plugin for input elements
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+:root {
+ --autocomplete-dropdown-vertical-gap: 4px; // gap between input and results list
+ --autocomplete-dropdown-max-height: 150px;
+ --autocomplete-dropdown-scrollbar-width: 6px; // custom scrollbar width - webkit browsers
+}
+
+// results dropdown
+.autocomplete__results {
+ position: absolute;
+ z-index: var(--z-index-popover, 5);
+ width: 100%;
+ left: 0;
+ top: calc(100% + var(--autocomplete-dropdown-vertical-gap));
+ background-color: var(--color-bg-light);
+ box-shadow: var(--inner-glow), var(--shadow-md);
+ border-radius: var(--radius-md);
+ opacity: 0;
+ visibility: hidden;
+ overflow: hidden;
+
+ .autocomplete--results-visible & {
+ opacity: 1;
+ visibility: visible;
+ }
+}
+
+.autocomplete__list {
+ max-height: var(--autocomplete-dropdown-max-height);
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+
+ // custom scrollbar
+ &::-webkit-scrollbar { // scrollbar width
+ width: var(--autocomplete-dropdown-scrollbar-width);
+ }
+
+ &::-webkit-scrollbar-track { // progress bar
+ background-color: alpha(var(--color-contrast-higher), 0.08);
+ border-radius: 0;
+ }
+
+ &::-webkit-scrollbar-thumb { // handle
+ background-color: alpha(var(--color-contrast-higher), 0.12);
+ border-radius: 0;
+
+ &:hover {
+ background-color: alpha(var(--color-contrast-higher), 0.2);
+ }
+ }
+}
+
+// single result item
+.autocomplete__item {
+ cursor: pointer;
+
+ &:hover {
+ background-color: alpha(var(--color-contrast-higher), 0.075);
+ }
+
+ &:focus {
+ outline: none;
+ background-color: alpha(var(--color-primary), 0.15);
+ }
+}
diff --git a/apps/web-shared/src/styles/components/btn-states.scss b/apps/web-shared/src/styles/components/btn-states.scss
new file mode 100644
index 0000000..a2fc6c5
--- /dev/null
+++ b/apps/web-shared/src/styles/components/btn-states.scss
@@ -0,0 +1,51 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_btn-states
+Title: Buttons states
+Descr: Multi-state button elements
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.btn__content-a {
+ display: inline-flex;
+}
+
+.btn__content-b {
+ display: none;
+}
+
+.btn__content-a, .btn__content-b {
+ align-items: center;
+}
+
+.btn--state-b {
+ .btn__content-a {
+ display: none;
+ }
+
+ .btn__content-b {
+ display: inline-block; // fallback
+ display: inline-flex;
+ }
+}
+
+/* preserve button width when switching from state A to state B */
+.btn--preserve-width {
+ .btn__content-b {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ }
+
+ &.btn--state-b .btn__content-a {
+ display: inline-block; // fallback
+ display: inline-flex;
+ visibility: hidden;
+ }
+}
diff --git a/apps/web-shared/src/styles/components/chip.scss b/apps/web-shared/src/styles/components/chip.scss
new file mode 100644
index 0000000..1bb93db
--- /dev/null
+++ b/apps/web-shared/src/styles/components/chip.scss
@@ -0,0 +1,117 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_chips
+Title: Chips
+Descr: A list of compact pieces of information
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.chip {
+ /* reset - in case the class is applied to a <button> or an <a> */
+ border: 0;
+ color: inherit;
+ line-height: 1;
+ appearance: none;
+
+ display: inline-flex;
+ align-items: center;
+ border-radius: var(--radius-sm);
+
+ background-color: alpha(var(--color-contrast-higher), 0.1);
+ padding: var(--space-xxxs) 0;
+}
+
+.chip--outline {
+ background-color: transparent;
+ box-shadow: inset 0 0 0 1px alpha(var(--color-contrast-higher), 0.25);
+}
+
+.chip--error {
+ background-color: alpha(var(--color-error), 0.2);
+ color: var(--color-contrast-higher);
+}
+
+.chip--success {
+ background-color: alpha(var(--color-success), 0.2);
+ color: var(--color-contrast-higher);
+}
+
+.chip--warning {
+ background-color: alpha(var(--color-warning), 0.2);
+ color: var(--color-contrast-higher);
+}
+
+.chip--interactive {
+ cursor: pointer;
+
+ &:hover {
+ background-color: alpha(var(--color-contrast-higher), 0.2);
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px alpha(var(--color-contrast-higher), 0.3);
+ }
+
+ &:focus:not(:focus-visible) {
+ box-shadow: none;
+ }
+}
+
+.chip__label {
+ padding: 0 var(--space-xxs);
+}
+
+.chip__img {
+ display: block;
+ width: 1.5em;
+ height: 1.5em;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.chip__icon-wrapper {
+ display: flex;
+ width: 1.5em;
+ height: 1.5em;
+ border-radius: 50%;
+ background-color: alpha(var(--color-contrast-higher), 0.95);
+ color: var(--color-bg); /* icon color */
+
+ .icon {
+ display: block;
+ margin: auto;
+ }
+}
+
+.chip__btn {
+ @include reset;
+ display: flex;
+ width: 1em;
+ height: 1em;
+ background-color: alpha(var(--color-contrast-higher), 0.2);
+ border-radius: 50%;
+ cursor: pointer;
+ margin-right: 7px;
+
+ .icon {
+ display: block;
+ margin: 0 auto;
+ }
+
+ &:hover {
+ background-color: alpha(var(--color-contrast-higher), 0.3);
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px alpha(var(--color-contrast-higher), 0.5);
+ }
+
+ &:focus:not(:focus-visible) {
+ box-shadow: none;
+ }
+}
diff --git a/apps/web-shared/src/styles/components/circle-loader.scss b/apps/web-shared/src/styles/components/circle-loader.scss
new file mode 100644
index 0000000..5116d39
--- /dev/null
+++ b/apps/web-shared/src/styles/components/circle-loader.scss
@@ -0,0 +1,315 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_circle-loader
+Title: Circle Loader
+Descr: A collection of animated circle loaders
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+:root {
+ // v1
+ --circle-loader-v1-size: 48px;
+ --circle-loader-v1-stroke-width: 4px;
+
+ // v2
+ --circle-loader-v2-size: 64px;
+ --circle-loader-v2-stroke-width: 2;
+
+ // v3
+ --circle-loader-v3-size: 64px;
+
+ // v4
+ --circle-loader-v4-size: 48px;
+
+ // v5
+ --circle-loader-v5-size: 64px;
+
+ // v6
+ --circle-loader-v6-size: 48px;
+}
+
+.circle-loader {
+ position: relative;
+ display: inline-block;
+}
+
+@supports (animation-name: this) {
+ .circle-loader__label {
+ @include srHide; // show label only to screen readers if animations are supported
+ }
+}
+
+// loader v1 - rotation
+@supports (animation-name: this) {
+ .circle-loader--v1 {
+ transform: rotate(45deg);
+ will-change: transform;
+ animation: circle-loader-1 0.75s infinite var(--ease-in-out);
+
+ .circle-loader__circle {
+ width: var(--circle-loader-v1-size); // loader width
+ height: var(--circle-loader-v1-size); // loader height
+ border-width: var(--circle-loader-v1-stroke-width); // loader stroke width
+ border-style: solid;
+ border-color: alpha(var(--color-primary), 0.2); // loader base color
+ border-radius: 50%;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-width: inherit;
+ border-style: inherit;
+ border-color: transparent;
+ border-top-color: var(--color-primary); // loader fill color
+ border-radius: inherit;
+ }
+ }
+ }
+}
+
+@keyframes circle-loader-1 {
+ 0% {
+ transform: rotate(45deg);
+ }
+
+ 100% {
+ transform: rotate(405deg);
+ }
+}
+
+// loader v2 - filling
+@supports (animation-name: this) {
+ .circle-loader--v2 {
+ will-change: transform;
+ animation: circle-loader-spinning-main 1.4s infinite linear;
+
+ .circle-loader__svg {
+ display: block;
+ width: var(--circle-loader-v2-size);
+ height: var(--circle-loader-v2-size);
+ color: var(--color-primary); // loader color
+
+ > * {
+ stroke-width: var(--circle-loader-v2-stroke-width); // loader stroke width
+ }
+ }
+
+ .circle-loader__base {
+ opacity: 0.2;
+ }
+
+ .circle-loader__fill {
+ stroke-linecap: round; // optional - remove if you prefer butt caps
+ stroke-dashoffset: 0;
+ stroke-dasharray: 90 120;
+ transform-origin: 50% 50%;
+ transform: rotate(45deg);
+ animation: circle-loader-dash 1.4s infinite;
+ }
+ }
+}
+
+@keyframes circle-loader-dash {
+ 0%, 20% {
+ stroke-dashoffset: 0;
+ transform: rotate(0);
+ }
+
+ 50%, 70% {
+ stroke-dashoffset: 80;
+ transform: rotate(270deg);
+ }
+
+ 100% {
+ stroke-dashoffset: 0;
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes circle-loader-spinning-main {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+// loader v3 - drop
+@supports (animation-name: this) {
+ .circle-loader--v3 {
+ width: var(--circle-loader-v3-size); // loader width
+ height: var(--circle-loader-v3-size); // loader height
+
+ .circle-loader__circle {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background-color: var(--color-primary); // loader color
+ transform: scale(0);
+ opacity: 0.8;
+ will-change: transform, opacity;
+ animation: circle-loader-3 1.2s infinite;
+ }
+
+ .circle-loader__circle--2nd {
+ animation-delay: 0.6s; // this should be half the duration of animation
+ }
+ }
+}
+
+@keyframes circle-loader-3 {
+ to {
+ transform: scale(1);
+ opacity: 0;
+ }
+}
+
+// loader v4 - eclipse
+@supports (animation-name: this) {
+ .circle-loader--v4 {
+ width: var(--circle-loader-v4-size); // loader width
+ height: var(--circle-loader-v4-size); // loader height
+ border-radius: 50%;
+ overflow: hidden;
+
+ .circle-loader__mask,
+ .circle-loader__circle {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: inherit;
+ }
+
+ .circle-loader__mask {
+ clip-path: circle(calc(0.5 * var(--circle-loader-v4-size))); // fix iOS issue - it needs to be = half size of loader
+ }
+
+ .circle-loader__circle--1st {
+ background-color: var(--color-contrast-low); // loader base color
+ }
+
+ .circle-loader__circle--2nd {
+ background-color: var(--color-primary); // loader fill color
+ will-change: transform;
+ transform-origin: 50% 100%;
+ animation: circle-loader-4 1.2s infinite cubic-bezier(.23, .9, .75, .1);
+ transform: translateX(-100%);
+ }
+ }
+}
+
+@keyframes circle-loader-4 {
+ to {
+ transform: translateX(100%);
+ }
+}
+
+// loader v5 - bounce
+@supports (animation-name: this) {
+ .circle-loader--v5 {
+ font-size: var(--circle-loader-v5-size); // loader size - if you edit this value all elements scale accordingly
+ width: 1em;
+ height: 1em;
+
+ .circle-loader__label {
+ font-size: 1rem;
+ }
+
+ .circle-loader__ball {
+ position: absolute;
+ top: 0;
+ left: calc(50% - 0.140625em);
+ width: 0.28125em;
+ height: 0.28125em;
+ background-color: var(--color-primary);
+ border-radius: 50%;
+ animation: circle-loader-5-ball 0.8s infinite;
+ }
+
+ .circle-loader__shadow {
+ position: absolute;
+ bottom: 0;
+ left: calc(50% - 0.15625em);
+ width: 0.3125em;
+ height: 0.3125em;
+ background-color: var(--color-contrast-lower);
+ border-radius: 50%;
+ transform: scaleY(0.4) scaleX(1.2);
+ animation: circle-loader-5-shadow 0.8s infinite;
+ }
+ }
+}
+
+@keyframes circle-loader-5-ball {
+ 0% {
+ transform: translateY(0);
+ animation-timing-function: cubic-bezier(.61, .12, .85, .4);
+ }
+
+ 50% {
+ transform: translateY(0.5625em);
+ animation-timing-function: cubic-bezier(.12, .59, .46, .95);
+ }
+
+ 100% {
+ transform: translateY(0);
+ }
+}
+
+@keyframes circle-loader-5-shadow {
+ 0% {
+ transform: scaleY(0.4) scaleX(1.2);
+ background-color: var(--color-contrast-lower);
+ animation-timing-function: cubic-bezier(.61, .12, .85, .4);
+ }
+
+ 50% {
+ transform: scaleY(0.2) scaleX(0.6);
+ background-color: var(--color-contrast-low);
+ animation-timing-function: cubic-bezier(.12, .59, .46, .95);
+ }
+
+ 100% {
+ transform: scaleY(0.4) scaleX(1.2);
+ background-color: var(--color-contrast-lower);
+ }
+}
+
+// loader v6 - worm
+@supports (animation-name: this) {
+ .circle-loader--v6 {
+ .circle-loader__svg {
+ display: block;
+ width: var(--circle-loader-v6-size);
+ height: var(--circle-loader-v6-size);
+ color: var(--color-primary); // loader color
+ }
+
+ .circle-loader__fill {
+ stroke-width: 8px; // loader stroke width
+ stroke-dashoffset: 35;
+ stroke-dasharray: 36 36;
+ animation: circle-loader-6 1.5s infinite;
+ }
+ }
+}
+
+@keyframes circle-loader-6 {
+ 0%, 100% {
+ stroke-dashoffset: 35;
+ }
+
+ 50% {
+ stroke-dashoffset: -35;
+ }
+}
diff --git a/apps/web-shared/src/styles/components/custom-checkbox.scss b/apps/web-shared/src/styles/components/custom-checkbox.scss
new file mode 100644
index 0000000..5722ee0
--- /dev/null
+++ b/apps/web-shared/src/styles/components/custom-checkbox.scss
@@ -0,0 +1,131 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_custom-checkbox
+Title: Custom Checkbox
+Descr: Replace the native checkbox button with a custom element (e.g., an icon)
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+:root {
+ --custom-checkbox-size: 20px;
+ --custom-checkbox-radius: 4px;
+ --custom-checkbox-border-width: 1px;
+ --custom-checkbox-marker-size: 18px;
+}
+
+.custom-checkbox {
+ position: relative;
+ z-index: 1;
+ display: inline-block;
+ font-size: var(--custom-checkbox-size);
+}
+
+.custom-checkbox__input {
+ position: relative;
+ /* hide native input */
+ margin: 0;
+ padding: 0;
+ opacity: 0;
+ height: 1em;
+ width: 1em;
+ display: block;
+ z-index: 1;
+}
+
+.custom-checkbox__control {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ z-index: -1;
+ pointer-events: none;
+ color: alpha(var(--color-contrast-low), 0.65); /* unchecked color */
+
+ &::before, &::after {
+ content: '';
+ position: absolute;
+ }
+
+ &::before { /* focus circle */
+ width: 160%;
+ height: 160%;
+ background-color: currentColor;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%) scale(0);
+ opacity: 0;
+ border-radius: 50%;
+ will-change: transform;
+ }
+
+ &::after { /* custom checkbox */
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ /* custom checkbox style */
+ background-color: var(--color-bg);
+ border-radius: var(--custom-checkbox-radius);
+ box-shadow: inset 0 0 0 var(--custom-checkbox-border-width) currentColor, var(--shadow-xs); /* border */
+ }
+}
+
+.custom-checkbox__input:checked ~ .custom-checkbox__control,
+.custom-checkbox__input:indeterminate ~ .custom-checkbox__control {
+ &::after {
+ background-color: currentColor;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: var(--custom-checkbox-marker-size);
+ box-shadow: none;
+ }
+}
+
+.custom-checkbox__input:checked ~ .custom-checkbox__control {
+ color: var(--color-primary); /* checked color */
+
+ &::after {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpolyline points='2.5 8 6.5 12 13.5 3' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5'/%3E%3C/svg%3E");
+ }
+}
+
+.custom-checkbox__input:indeterminate ~ .custom-checkbox__control {
+ &::after {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cline x1='2' y1='8' x2='14' y2='8' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'/%3E%3C/svg%3E");
+ }
+}
+
+.custom-checkbox__input:active ~ .custom-checkbox__control {
+ transform: scale(0.9);
+}
+
+.custom-checkbox__input:checked:active ~ .custom-checkbox__control,
+.custom-checkbox__input:indeterminate:active ~ .custom-checkbox__control {
+ transform: scale(1);
+}
+
+.custom-checkbox__input:focus ~ .custom-checkbox__control::before {
+ opacity: 0.2;
+ transform: translate(-50%, -50%) scale(1);
+}
+
+/* --icon */
+.custom-checkbox--icon {
+ --custom-checkbox-size: 32px;
+
+ .custom-checkbox__control::after {
+ display: none;
+ }
+
+ .icon {
+ display: block;
+ color: inherit;
+ position: relative;
+ z-index: 1;
+ }
+}
diff --git a/apps/web-shared/src/styles/components/custom-select.scss b/apps/web-shared/src/styles/components/custom-select.scss
new file mode 100644
index 0000000..9cd3b5e
--- /dev/null
+++ b/apps/web-shared/src/styles/components/custom-select.scss
@@ -0,0 +1,158 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_custom-select
+Title: Custom Select
+Descr: Custom Select Control
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+:root {
+ // --default variation only 👇
+ --select-icon-size: 16px;
+ --select-icon-right-margin: var(--space-sm); // icon margin right
+ --select-text-icon-gap: var(--space-xxxs); // gap between text and icon
+}
+
+.select {
+ position: relative;
+}
+
+.select__input {
+ width: 100%;
+ height: 100%;
+ padding-right: calc(var(--select-icon-size) + var(--select-icon-right-margin) + var(--select-text-icon-gap)) !important;
+}
+
+.select__icon {
+ width: var(--select-icon-size);
+ height: var(--select-icon-size);
+ pointer-events: none;
+ position: absolute;
+ right: var(--select-icon-right-margin);
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+// --custom-dropdown
+:root {
+ --select-dropdown-gap: 4px; // distance between select control and custom dropdown
+}
+
+.select__button { // created in JS - custom select control
+ width: 100%;
+}
+
+.select__button[aria-expanded="true"] {
+ // custom select control if dropdown = visible
+}
+
+.select__dropdown { // created in JS - custom select dropdown
+ position: absolute;
+ left: 0;
+ top: 100%;
+ min-width: 200px;
+ max-height: 1px; // updated in JS
+ background-color: var(--color-bg-light);
+ box-shadow: var(--inner-glow), var(--shadow-md);
+ padding: var(--space-xxxs) 0;
+ border-radius: var(--radius-md);
+ z-index: var(--z-index-popover, 5);
+ margin-top: var(--select-dropdown-gap);
+ margin-bottom: var(--select-dropdown-gap);
+ overflow: auto;
+
+ // use rem units
+ @include spaceUnit(1rem);
+ @include textUnit(1rem);
+
+ visibility: hidden;
+ opacity: 0;
+}
+
+.select__dropdown--right { // change dropdown position based on the available space
+ right: 0;
+ left: auto;
+}
+
+.select__dropdown--up {
+ bottom: 100%;
+ top: auto;
+}
+
+.select__button[aria-expanded="true"] + .select__dropdown {
+ visibility: visible;
+ opacity: 1;
+}
+
+// custom <optgroup> list - include all <option>s if no <optgroup> available
+.select__list {
+ list-style: none !important;
+}
+
+.select__list:not(:first-of-type) {
+ padding-top: var(--space-xxs);
+}
+
+.select__list:not(:last-of-type) {
+ border-bottom: 1px solid alpha(var(--color-contrast-higher), 0.1);
+ padding-bottom: var(--space-xxs);
+}
+
+.select__item { // single item inside .select__list
+ display: flex;
+ align-items: center;
+ padding: var(--space-xxs) var(--space-sm);
+ color: var(--color-contrast-high);
+ width: 100%;
+ text-align: left;
+ // truncate text
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.select__item--optgroup { // custom <optgroup> label
+ font-size: var(--text-sm);
+ color: var(--color-contrast-medium);
+}
+
+.select__item--option { // custom <option> label
+ cursor: pointer;
+
+ &:hover {
+ background-color: alpha(var(--color-contrast-higher), 0.075);
+ }
+
+ &:focus {
+ outline: none;
+ background-color: alpha(var(--color-primary), 0.15);
+ }
+
+ &[aria-selected=true] { // selected option
+ background-color: var(--color-primary);
+ color: var(--color-white);
+ position: relative;
+ @include fontSmooth;
+
+ &::after { // check icon next to the selected language
+ content: '';
+ display: block;
+ height: 1em;
+ width: 1em;
+ margin-left: auto;
+ background-color: currentColor;
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpolyline stroke-width='2' stroke='%23ffffff' fill='none' stroke-linecap='round' stroke-linejoin='round' points='1,9 5,13 15,3 '/%3E%3C/svg%3E");
+ }
+
+ &:focus {
+ box-shadow: inset 0 0 0 2px var(--color-primary-dark);
+ }
+ }
+}
+
+html:not(.js ) .select .icon { // hide icon if JS = disabled
+ display: none;
+}
diff --git a/apps/web-shared/src/styles/components/details.scss b/apps/web-shared/src/styles/components/details.scss
new file mode 100644
index 0000000..b4c122d
--- /dev/null
+++ b/apps/web-shared/src/styles/components/details.scss
@@ -0,0 +1,57 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_details
+Title: Details
+Descr: A button that toggles the visibility of additional information
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.details {}
+
+.details__summary {
+ display: inline-block;
+ cursor: pointer;
+ user-select: none;
+
+ &:hover {
+ color: var(--color-primary);
+ }
+
+ &:focus {
+ outline: 2px solid alpha(var(--color-primary), 0.2);
+ outline-offset: 4px;
+ }
+
+ .icon {
+ flex-shrink: 0;
+ }
+}
+
+// if JS = enabled
+.js {
+ .details__summary {
+ list-style: none; // remove summary default icon
+ }
+
+ .details__summary::-webkit-details-marker {
+ display: none; // remove default icon in webkit browsers
+ }
+
+ .details__summary[aria-expanded="true"] .icon {
+ transform: rotate(90deg); // rotate icon when content is visible
+ }
+
+ .details__content[aria-hidden="true"] {
+ display: none;
+ }
+}
+
+// if JS = disabled
+html:not(.js) .details__summary {
+ .icon {
+ display: none;
+ }
+}
diff --git a/apps/web-shared/src/styles/components/dropdown.scss b/apps/web-shared/src/styles/components/dropdown.scss
new file mode 100644
index 0000000..c5ded33
--- /dev/null
+++ b/apps/web-shared/src/styles/components/dropdown.scss
@@ -0,0 +1,98 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _2_dropdown
+Title: Dropdown
+Descr: A hoverable link that toggles the visibility of a dropdown list
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+:root {
+ --dropdown-item-padding: var(--space-xxs) var(--space-sm);
+}
+
+.dropdown {
+ position: relative;
+}
+
+.dropdown__menu {
+ border-radius: var(--radius-md);
+ background-color: var(--color-bg-light);
+ box-shadow: var(--inner-glow), var(--shadow-sm);
+ z-index: var(--z-index-popover, 5);
+ position: absolute;
+ left: 0;
+ top: 100%;
+ opacity: 0;
+ visibility: hidden;
+}
+
+.dropdown__wrapper {
+ max-height: 24px;
+}
+
+@media (pointer: fine) { // user has pointing device (e.g., mouse)
+ .dropdown__wrapper,
+ .open-dropdown {
+ &:hover .dropdown__menu,
+ &:focus .dropdown__menu {
+ opacity: 1;
+ visibility: visible;
+ }
+ }
+
+ .dropdown__sub-wrapper:hover > .dropdown__menu {
+ left: 100%;
+ }
+}
+
+@media not all and (pointer: fine) {
+ .dropdown__trigger-icon {
+ display: none;
+ }
+}
+
+.dropdown__item {
+ display: block;
+ text-decoration: none;
+ color: var(--color-contrast-high);
+ padding: var(--dropdown-item-padding);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:hover, &.dropdown__item--hover {
+ background-color: alpha(var(--color-contrast-higher), 0.075);
+ }
+}
+
+.dropdown__separator { // h line divider
+ height: 1px;
+ background-color: var(--color-contrast-lower);
+ margin: var(--dropdown-item-padding);
+}
+
+.dropdown__sub-wrapper {
+ position: relative;
+
+ > .dropdown__item { // item w/ right arrow
+ position: relative;
+ padding-right: calc(var(--space-sm) + 12px);
+
+ .icon { // right arrow
+ position: absolute;
+ display: block;
+ width: 12px;
+ height: 12px;
+ right: var(--space-xxs);
+ top: calc(50% - 6px);
+ }
+ }
+
+ > .dropdown__menu { // sub menu
+ top: calc(var(--space-xxs) * -1);
+ box-shadow: var(--inner-glow), var(--shadow-md);
+ }
+}
diff --git a/apps/web-shared/src/styles/components/form-validator.scss b/apps/web-shared/src/styles/components/form-validator.scss
new file mode 100644
index 0000000..cc9f9a3
--- /dev/null
+++ b/apps/web-shared/src/styles/components/form-validator.scss
@@ -0,0 +1,18 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_form-validator
+Title: Form Validator
+Descr: A plugin to validate form fields
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.form-validate__error-msg {
+ display: none; // hide error message by default
+
+ .form-validate__input-wrapper--error & {
+ display: block; // show error message
+ }
+}
diff --git a/apps/web-shared/src/styles/components/interactive-table.scss b/apps/web-shared/src/styles/components/interactive-table.scss
new file mode 100644
index 0000000..f239c62
--- /dev/null
+++ b/apps/web-shared/src/styles/components/interactive-table.scss
@@ -0,0 +1,156 @@
+@use '../base' as *;
+@use 'menu.scss' as *;
+@use 'menu-bar.scss' as *;
+
+/* --------------------------------
+
+File#: _3_interactive-table
+Title: Interactive Table
+Descr: Table with the option of sorting data and selecting rows to perform specific actions
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.int-table {
+ overflow: hidden;
+ border-bottom: 2px solid var(--color-contrast-lower);
+}
+
+.int-table__inner {
+ position: relative;
+ overflow: auto;
+
+ &::-webkit-scrollbar { // custom scrollbar style
+ height: 8px;
+ width: 8px;
+ }
+
+ &::-webkit-scrollbar-track { // progress bar
+ background-color: var(--color-contrast-lower);
+ }
+
+ &::-webkit-scrollbar-thumb { // handle
+ background-color: alpha(var(--color-contrast-higher), 0.9);
+ border-radius: 50em;
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background-color: var(--color-contrast-higher);
+ }
+}
+
+.int-table__table {
+ width: 100%;
+}
+
+.int-table__header {
+ .int-table__cell {
+ background-color: var(--color-bg);
+ box-shadow: 0 2px 0 var(--color-contrast-lower);
+ }
+}
+
+.int-table__body {
+ .int-table__row {
+ border-bottom: 1px solid var(--color-contrast-lower);
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ .int-table__row--checked {
+ background-color: alpha(var(--color-primary), 0.1);
+ border-color: alpha(var(--color-primary), 0.25);
+ }
+}
+
+.int-table__cell { // standard cell
+ padding: var(--space-xxxs);
+}
+
+.int-table__cell--th { // header cell
+ font-weight: 600;
+}
+
+.int-table__cell--sort { // header cell with sorting option
+ user-select: none;
+
+ &:hover, &:focus-within {
+ background-color: alpha(var(--color-contrast-higher), 0.075);
+ }
+
+ &:hover {
+ cursor: pointer;
+ }
+}
+
+.int-table__cell--focus {
+ background-color: alpha(var(--color-primary), 0.15);
+}
+
+.int-table__sort-icon { // sorting icon indicator
+ .arrow-up, .arrow-down {
+ fill: alpha(var(--color-contrast-higher), 0.3);
+ }
+}
+
+.int-table__cell--asc .int-table__sort-icon .arrow-up,
+.int-table__cell--desc .int-table__sort-icon .arrow-down {
+ fill: var(--color-contrast-higher);
+}
+
+.int-table__checkbox {
+ --custom-checkbox-size: 18px;
+ --custom-checkbox-marker-size: 16px;
+ display: block;
+ width: var(--custom-checkbox-size);
+ height: var(--custom-checkbox-size);
+}
+
+.int-table__menu-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2em;
+ cursor: pointer;
+
+ .icon {
+ display: block;
+ width: 16px;
+ height: 16px;
+ }
+}
+
+// --sticky-header
+.int-table--sticky-header {
+ position: relative;
+ z-index: 1;
+
+ .int-table__inner {
+ max-height: 605px;
+ }
+
+ .int-table__header {
+ .int-table__cell {
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ }
+ }
+}
+
+// actions
+.int-table-actions {
+ .menu-bar {
+ --menu-bar-button-size: 38px; // size of the menu buttons
+ --menu-bar-icon-size: 16px; // size of the icons inside the buttons
+ --menu-bar-horizontal-gap: var(--space-xxxxs); // horizontal gap between buttons
+ --menu-bar-vertical-gap: 4px; // vertical gap between buttons and labels (tooltips)
+ --menu-bar-label-size: var(--text-xs); // label font size
+ }
+
+ .menu-bar__icon {
+ color: alpha(var(--color-contrast-higher), 0.5);
+ }
+}
diff --git a/apps/web-shared/src/styles/components/list.scss b/apps/web-shared/src/styles/components/list.scss
new file mode 100644
index 0000000..df600a3
--- /dev/null
+++ b/apps/web-shared/src/styles/components/list.scss
@@ -0,0 +1,195 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_list
+Title: List
+Descr: Custom list component
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+:root {
+ --list-space-y: 0.375em; // vertical gaps
+ --list-offset: 1em; // sublist horizontal offset
+ --list-line-height-multiplier: 1; // line-height multiplier
+}
+
+.list, .text-component .list {
+ padding-left: 0;
+ list-style: none;
+
+ ul, ol {
+ list-style: none;
+ margin: 0; // reset
+ margin-top: calc((var(--list-space-y) / 2) * var(--text-space-y-multiplier, 1));
+ padding-top: calc((var(--list-space-y) / 2) * var(--text-space-y-multiplier, 1));
+ padding-left: var(--list-offset);
+ }
+
+ li {
+ padding-bottom: calc((var(--list-space-y) / 2) * var(--text-space-y-multiplier, 1));
+ margin-bottom: calc((var(--list-space-y) / 2) * var(--text-space-y-multiplier, 1));
+ line-height: calc(var(--body-line-height) * var(--list-line-height-multiplier));
+ }
+
+ > li:last-child, ul > li:last-child, ol > li:last-child {
+ margin-bottom: 0;
+ }
+
+ &:not(.list--border) > li:last-child, ul > li:last-child, ol > li:last-child {
+ padding-bottom: 0;
+ }
+}
+
+/* #region (ul + ol) */
+.list--ul, .text-component .list--ul,
+.list--ol, .text-component .list--ol {
+ --list-offset: calc(var(--list-bullet-size) + var(--list-bullet-margin-right));
+
+ ul, ol {
+ padding-left: 0;
+ }
+
+ li {
+ @supports (--css: variables) {
+ padding-left: var(--list-offset) !important;
+ }
+ }
+
+ li::before {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ vertical-align: middle;
+ position: relative;
+ top: -0.1em;
+
+ @supports (--css: variables) {
+ width: var(--list-bullet-size) !important;
+ height: var(--list-bullet-size) !important;
+ margin-left: calc(var(--list-bullet-size) * -1) !important;
+ left: calc(var(--list-bullet-margin-right) * -1) !important;
+ }
+ }
+}
+
+// unordered list
+.list--ul, .text-component .list--ul {
+ --list-bullet-size: 7px; // dot width and height
+ --list-bullet-margin-right: 12px; // gap between bullet and content
+
+ > li {
+ padding-left: 19px; // IE fallback
+ }
+
+ > li::before { // bullet
+ content: '';
+ border-radius: 50%;
+ color: var(--color-contrast-lower); // bullet color
+ background-color: currentColor;
+
+ // IE fallback
+ width: 7px;
+ height: 7px;
+ margin-left: -7px;
+ left: -12px;
+ // end - IE fallback
+ }
+
+ ul li::before {
+ background-color: transparent;
+ box-shadow: inset 0 0 0 2px currentColor;
+ }
+}
+
+// ordered list
+.list--ol, .text-component .list--ol {
+ --list-bullet-size: 26px; // ⚠️ use px or rem units - circle width and height
+ --list-bullet-margin-right: 6px; // ⚠️ use px or rem units - gap between circle and content
+ --list-bullet-font-size: 14px; // ⚠️ use px or rem units - bullet font size
+ counter-reset: list-items;
+
+ > li {
+ counter-increment: list-items;
+ padding-left: 32px; // IE fallback
+ }
+
+ ol {
+ counter-reset: list-items;
+ }
+
+ > li::before {
+ content: counter(list-items);
+ font-size: var(--list-bullet-font-size, 14px);
+ background-color: var(--color-contrast-lower);
+ color: var(--color-contrast-high);
+ line-height: 1;
+ border-radius: 50%;
+
+ // IE fallback
+ width: 26px;
+ height: 26px;
+ margin-left: -26px;
+ left: -6px;
+ // end - IE fallback
+ }
+
+ ol > li::before {
+ background-color: transparent;
+ box-shadow: inset 0 0 0 2px var(--color-contrast-lower);
+ }
+}
+/* #endregion */
+
+/* #region (border) */
+.list--border, .text-component .list--border { // show border divider among list items
+ li:not(:last-child) {
+ border-bottom: 1px solid var(--color-contrast-lower);
+ }
+
+ ul, ol {
+ border-top: 1px solid var(--color-contrast-lower);
+ }
+}
+/* #endregion */
+
+/* #region (icons) */
+.list--icons, .text-component .list--icons { // use icons as bullet points
+ --list-bullet-size: 24px;
+ --list-bullet-margin-right: 8px; // gap between icon and text
+ --list-offset: calc(var(--list-bullet-size) + var(--list-bullet-margin-right));
+
+ ul, ol {
+ padding-left: 32px; // IE fallback
+
+ @supports (--css: variables) {
+ padding-left: var(--list-offset);
+ }
+ }
+}
+
+.list__icon {
+ position: relative;
+
+ // IE fallback
+ width: 24px;
+ height: 24px;
+ margin-right: 8px;
+
+ &:not(.top-0) {
+ top: calc((1em * var(--body-line-height) - 24px) / 2);
+ }
+ // end - IE fallback
+
+ @supports (--css: variables) {
+ width: var(--list-bullet-size);
+ height: var(--list-bullet-size);
+ margin-right: var(--list-bullet-margin-right);
+
+ &:not(.top-0) {
+ top: calc((1em * var(--body-line-height) * var(--list-line-height-multiplier) - var(--list-bullet-size)) / 2);
+ }
+ }
+}
+/* #endregion */
diff --git a/apps/web-shared/src/styles/components/menu-bar.scss b/apps/web-shared/src/styles/components/menu-bar.scss
new file mode 100644
index 0000000..3f70fbe
--- /dev/null
+++ b/apps/web-shared/src/styles/components/menu-bar.scss
@@ -0,0 +1,139 @@
+@use '../base' as *;
+@use 'menu.scss' as *;
+
+/* --------------------------------
+
+File#: _2_menu-bar
+Title: Menu Bar
+Descr: Application menu with a list of common actions that users can perform
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+:root {
+ --menu-bar-button-size: 2.5em; // size of the menu buttons
+ --menu-bar-icon-size: 1em; // size of the icons inside the buttons
+ --menu-bar-horizontal-gap: var(--space-xxs); // horizontal gap between buttons
+ --menu-bar-vertical-gap: 4px; // vertical gap between buttons and labels (tooltips)
+ --menu-bar-label-size: var(--text-xs); // label font size
+}
+
+.menu-bar {
+ list-style: none;
+ display: inline-flex;
+ align-items: center;
+}
+
+.menu-bar__item { // menu button
+ position: relative;
+ display: inline-block; // flex fallback
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: var(--menu-bar-button-size);
+ width: var(--menu-bar-button-size);
+ border-radius: 50%;
+ cursor: pointer;
+
+ &:not(:last-child) {
+ margin-right: var(--menu-bar-horizontal-gap);
+ }
+
+ &:hover,
+ &.menu-control--active {
+ background-color: alpha(var(--color-contrast-higher), 0.1);
+
+ > .menu-bar__icon {
+ color: var(--color-contrast-higher);
+ }
+
+ > .menu-bar__label { // show label
+ clip: auto;
+ clip-path: none;
+ height: auto;
+ width: auto;
+ }
+ }
+
+ &:focus {
+ outline: none;
+ background-color: alpha(var(--color-primary), 0.1);
+ }
+
+ &:active {
+ background-color: var(--color-contrast-low);
+ }
+
+ &:focus:active {
+ background-color: alpha(var(--color-primary), 0.2);
+ }
+}
+
+.menu-bar__item--trigger { // button used to show hidden actions - visibile only if menu = collapsed
+ display: none;
+}
+
+.menu-bar__icon {
+ display: block;
+ color: var(--color-contrast-high);
+ font-size: var(--menu-bar-icon-size); // set icon size
+}
+
+.menu-bar__label { // label visible on :hover
+ // hide
+ position: absolute;
+ z-index: var(--z-index-popover, 5);
+ clip: rect(1px, 1px, 1px, 1px);
+ clip-path: inset(50%);
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ white-space: nowrap;
+ // style
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%) translateY(var(--menu-bar-vertical-gap));
+ padding: var(--space-xxs) var(--space-xs);
+ color: var(--color-bg);
+ background-color: alpha(var(--color-contrast-higher), 0.95);
+ border-radius: var(--radius-md);
+ font-size: var(--menu-bar-label-size);
+ @include fontSmooth;
+ pointer-events: none;
+ user-select: none;
+}
+
+.menu-bar--collapsed { // mobile layout style
+ .menu-bar__item--hide { // hide buttons
+ display: none;
+ }
+
+ .menu-bar__item--trigger { // show submenu trigger
+ display: inline-block; // flex fallback
+ display: flex;
+ }
+}
+
+// detect when the menu needs to switch from the mobile layout to an expanded one - used in JS
+.js {
+ .menu-bar {
+ opacity: 0; // hide menu bar while it is initialized in JS
+
+ &::before {
+ display: none;
+ content: 'collapsed';
+ }
+ }
+
+ .menu-bar--loaded {
+ opacity: 1;
+ }
+
+ @each $breakpoint, $value in $breakpoints {
+ .menu-bar--expanded\@#{$breakpoint}::before {
+ @include breakpoint(#{$breakpoint}) {
+ content: 'expanded';
+ }
+ }
+ }
+}
diff --git a/apps/web-shared/src/styles/components/menu.scss b/apps/web-shared/src/styles/components/menu.scss
new file mode 100644
index 0000000..8e211a5
--- /dev/null
+++ b/apps/web-shared/src/styles/components/menu.scss
@@ -0,0 +1,81 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_menu
+Title: Menu
+Descr: Application menu that provides access to a set of functionalities
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.menu {
+ --menu-vertical-gap: 5px; // vertical gap between the Menu element and its control
+ --menu-item-padding: var(--space-xxxs) var(--space-xs);
+ list-style: none;
+ position: fixed; // top/left position set in JS
+ background-color: var(--color-bg-light);
+ //padding: var(--space-xxs) 0;
+ border-radius: var(--radius-md);
+ z-index: var(--z-index-popover, 5);
+ user-select: none;
+ margin-top: var(--menu-vertical-gap);
+ margin-bottom: var(--menu-vertical-gap);
+ overflow: auto;
+
+
+ // use rem units
+ @include spaceUnit(1rem);
+ @include textUnit(1rem);
+
+ visibility: hidden;
+ opacity: 0;
+}
+
+.menu--is-visible {
+ visibility: visible;
+ opacity: 1;
+}
+
+.menu--overlay {
+ z-index: var(--z-index-overlay, 15);
+}
+
+.menu__content {
+ display: block; // fallback
+ display: flex;
+ align-items: center;
+ text-decoration: none; // reset link style
+ padding: var(--menu-item-padding);
+ color: var(--color-contrast-high);
+ cursor: pointer;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:hover {
+ background-color: alpha(var(--color-contrast-higher), 0.075);
+ }
+
+ &:focus {
+ outline: none;
+ background-color: alpha(var(--color-primary), 0.15);
+ }
+}
+
+.menu__label {
+ padding: var(--menu-item-padding);
+ font-size: var(--text-sm);
+ color: var(--color-contrast-medium);
+}
+
+.menu__separator {
+ height: 1px;
+ background-color: var(--color-contrast-lower);
+ margin: var(--menu-item-padding);
+}
+
+.menu__icon {
+ color: alpha(var(--color-contrast-higher), 0.5);
+ margin-right: var(--space-xxs);
+}
diff --git a/apps/web-shared/src/styles/components/modal.scss b/apps/web-shared/src/styles/components/modal.scss
new file mode 100644
index 0000000..1beec76
--- /dev/null
+++ b/apps/web-shared/src/styles/components/modal.scss
@@ -0,0 +1,105 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_modal-window
+Title: Modal Window
+Descr: A modal dialog used to display critical information
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.modal {
+ position: fixed;
+ z-index: var(--z-index-overlay, 15);
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ opacity: 0;
+ visibility: hidden;
+
+ &:not(.modal--is-visible) {
+ pointer-events: none;
+ background-color: transparent;
+ }
+}
+
+.modal--is-visible {
+ opacity: 1;
+ visibility: visible;
+}
+
+// close button
+.modal__close-btn {
+ display: flex;
+ flex-shrink: 0;
+ border-radius: 50%;
+ cursor: pointer;
+
+ .icon {
+ display: block;
+ margin: auto;
+ }
+}
+
+.modal__close-btn--outer { // close button - outside the modal__content
+ width: 48px;
+ height: 48px;
+ position: fixed;
+ top: var(--space-sm);
+ right: var(--space-sm);
+ z-index: var(--z-index-fixed-element, 10);
+ background-color: alpha(var(--color-black), 0.9);
+
+ .icon {
+ color: var(--color-white); // icon color
+ }
+
+ &:hover {
+ background-color: alpha(var(--color-black), 1);
+
+ .icon {
+ transform: scale(1.1);
+ }
+ }
+}
+
+.modal__close-btn--inner { // close button - inside the modal__content
+ width: 2em;
+ height: 2em;
+ background-color: var(--color-bg-light);
+ box-shadow: var(--inner-glow), var(--shadow-sm);
+
+ .icon {
+ color: inherit; // icon color
+ }
+
+ &:hover {
+ background-color: var(--color-bg-lighter);
+ box-shadow: var(--inner-glow), var(--shadow-md);
+ }
+}
+
+// load content - optional
+.modal--is-loading {
+ .modal__content {
+ visibility: hidden;
+ }
+
+ .modal__loader {
+ display: flex;
+ }
+}
+
+.modal__loader { // loader icon
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ align-items: center;
+ display: none;
+ pointer-events: none;
+}
diff --git a/apps/web-shared/src/styles/components/pagination.scss b/apps/web-shared/src/styles/components/pagination.scss
new file mode 100644
index 0000000..0a09210
--- /dev/null
+++ b/apps/web-shared/src/styles/components/pagination.scss
@@ -0,0 +1,77 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_pagination
+Title: Pagination
+Descr: Component used to navigate through pages of related content
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.pagination {}
+
+.pagination__list > li {
+ display: inline-block; // flex fallback
+}
+
+// --split - push first + last item to sides
+.pagination--split {
+ .pagination__list {
+ width: 100%;
+
+ > *:first-child {
+ margin-right: auto;
+ }
+
+ > *:last-child {
+ margin-left: auto;
+ }
+ }
+}
+
+.pagination__item {
+ display: inline-block; // flex fallback
+ display: inline-flex;
+ height: 100%;
+ align-items: center;
+ padding: var(--space-xs) calc(1.355 * var(--space-xs));
+
+ white-space: nowrap;
+ line-height: 1;
+ border-radius: var(--radius-md);
+
+ text-decoration: none;
+ color: var(--color-contrast-high);
+ @include fontSmooth;
+
+ will-change: transform;
+
+ &:hover:not(.pagination__item--selected):not(.pagination__item--ellipsis) {
+ background-color: alpha(var(--color-contrast-higher), 0.1);
+ }
+}
+
+.pagination__item--selected {
+ background-color: var(--color-contrast-higher);
+ color: var(--color-bg);
+ box-shadow: var(--shadow-sm);
+}
+
+.pagination__item--disabled {
+ opacity: 0.5;
+ pointer-events: none;
+}
+
+// --jumper
+.pagination__jumper {
+ .form-control {
+ width: 3em;
+ margin-right: var(--space-xs);
+ }
+
+ em {
+ flex-shrink: 0;
+ white-space: nowrap;
+ }
+}
diff --git a/apps/web-shared/src/styles/components/popover.scss b/apps/web-shared/src/styles/components/popover.scss
new file mode 100644
index 0000000..7f423a0
--- /dev/null
+++ b/apps/web-shared/src/styles/components/popover.scss
@@ -0,0 +1,38 @@
+@use '../base'as *;
+
+/* --------------------------------
+
+File#: _1_popover
+Title: Popover
+Descr: A pop-up box controlled by a trigger element
+Usage: codyhouse.co/license
+
+-------------------------------- */
+:root {
+ --popover-width: 250px;
+ --popover-control-gap: 4px; // ⚠️ use px units - vertical gap between the popover and its control
+ --popover-viewport-gap: 20px; // ⚠️ use px units - vertical gap between the popover and the viewport - visible if popover height > viewport height
+ --popover-transition-duration: 0.2s;
+}
+
+.popover {
+ position: fixed; // top/left position set in JS
+ width: var(--popover-width);
+ z-index: var(--z-index-popover, 5);
+ margin-top: var(--popover-control-gap);
+ margin-bottom: var(--popover-control-gap);
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+
+ visibility: hidden;
+ opacity: 0;
+}
+
+.popover--is-visible {
+ visibility: visible;
+ opacity: 1;
+}
+
+.popover-control--active {
+ // class added to the trigger when popover is visible
+}
diff --git a/apps/web-shared/src/styles/components/pre-header.scss b/apps/web-shared/src/styles/components/pre-header.scss
new file mode 100644
index 0000000..1e803e7
--- /dev/null
+++ b/apps/web-shared/src/styles/components/pre-header.scss
@@ -0,0 +1,46 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_pre-header
+Title: Pre-header
+Descr: Pre-header (top) banner
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.pre-header {
+ display: block;
+ background-color: var(--color-contrast-higher);
+ color: var(--color-bg);
+ @include fontSmooth;
+}
+
+.pre-header--is-hidden {
+ display: none;
+}
+
+.pre-header__close-btn {
+ position: absolute;
+ right: 0;
+ top: calc(50% - 0.5em);
+ will-change: transform;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+
+ .icon {
+ display: block;
+ }
+}
+
+// --link
+a.pre-header {
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ background-color: var(--color-contrast-high);
+ }
+}
diff --git a/apps/web-shared/src/styles/components/radios-checkboxes.scss b/apps/web-shared/src/styles/components/radios-checkboxes.scss
new file mode 100644
index 0000000..c4009f9
--- /dev/null
+++ b/apps/web-shared/src/styles/components/radios-checkboxes.scss
@@ -0,0 +1,134 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_radios-checkboxes
+Title: Radios and Checkboxes
+Descr: Custom radio and checkbox buttons
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+:root {
+ // radios and checkboxes
+ --checkbox-radio-size: 18px;
+ --checkbox-radio-gap: var(--space-xxs); // gap between button and label
+ --checkbox-radio-border-width: 1px;
+ --checkbox-radio-line-height: var(--body-line-height);
+
+ // radio buttons
+ --radio-marker-size: 8px;
+
+ // checkboxes
+ --checkbox-marker-size: 12px;
+ --checkbox-radius: 4px;
+}
+
+// hide native buttons
+.radio,
+.checkbox {
+ position: absolute;
+ padding: 0;
+ margin: 0;
+ margin-top: calc((1em * var(--checkbox-radio-line-height) - var(--checkbox-radio-size)) / 2);
+ opacity: 0;
+ height: var(--checkbox-radio-size);
+ width: var(--checkbox-radio-size);
+ pointer-events: none;
+}
+
+// label
+.radio + label,
+.checkbox + label {
+ display: inline-block;
+ line-height: var(--checkbox-radio-line-height);
+ user-select: none;
+ cursor: pointer;
+ padding-left: calc(var(--checkbox-radio-size) + var(--checkbox-radio-gap));
+}
+
+// custom inputs - basic style
+.radio + label::before,
+.checkbox + label::before {
+ content: '';
+ box-sizing: border-box;
+ display: inline-block;
+ position: relative;
+ vertical-align: middle;
+ top: -0.1em;
+ margin-left: calc(-1 * (var(--checkbox-radio-size) + var(--checkbox-radio-gap)));
+ flex-shrink: 0;
+ width: var(--checkbox-radio-size);
+ height: var(--checkbox-radio-size);
+ background-color: var(--color-bg);
+ border-width: var(--checkbox-radio-border-width);
+ border-color: alpha(var(--color-contrast-low), 0.65);
+ border-style: solid;
+ box-shadow: var(--shadow-xs);
+ background-repeat: no-repeat;
+ background-position: center;
+ margin-right: var(--checkbox-radio-gap);
+}
+
+// :hover
+.radio:not(:checked):not(:focus) + label:hover::before,
+.checkbox:not(:checked):not(:focus) + label:hover::before {
+ border-color: alpha(var(--color-contrast-low), 1);
+}
+
+// radio only style
+.radio + label::before {
+ border-radius: 50%;
+}
+
+// checkbox only style
+.checkbox + label::before {
+ border-radius: var(--checkbox-radius);
+}
+
+// :checked
+.radio:checked + label::before,
+.checkbox:checked + label::before {
+ background-color: var(--color-primary);
+ box-shadow: var(--shadow-xs);
+ border-color: var(--color-primary);
+}
+
+// radio button icon
+.radio:checked + label::before {
+ background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg class='nc-icon-wrapper' fill='%23ffffff'%3E%3Ccircle cx='8' cy='8' r='8' fill='%23ffffff'%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
+ background-size: var(--radio-marker-size);
+}
+
+// checkbox button icon
+.checkbox:checked + label::before {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpolyline points='1 6.5 4 9.5 11 2.5' fill='none' stroke='%23FFFFFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'/%3E%3C/svg%3E");
+ background-size: var(--checkbox-marker-size);
+}
+
+// :focus
+.radio:checked:active + label::before,
+.checkbox:checked:active + label::before,
+.radio:focus + label::before,
+.checkbox:focus + label::before {
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 3px alpha(var(--color-primary), 0.2);
+}
+
+// --radio--bg, --checkbox--bg -> variation with background color
+.radio--bg + label, .checkbox--bg + label {
+ padding: var(--space-xxxxs) var(--space-xxxs);
+ padding-left: calc(var(--checkbox-radio-size) + var(--checkbox-radio-gap) + var(--space-xxxs));
+ border-radius: var(--radius-md);
+}
+
+.radio--bg + label:hover, .checkbox--bg + label:hover {
+ background-color: alpha(var(--color-contrast-higher), 0.075);
+}
+
+.radio--bg:active + label,
+.checkbox--bg:active + label,
+.radio--bg:focus + label,
+.checkbox--bg:focus + label {
+ background-color: alpha(var(--color-primary), 0.1);
+}
diff --git a/apps/web-shared/src/styles/components/select-autocomplete.scss b/apps/web-shared/src/styles/components/select-autocomplete.scss
new file mode 100644
index 0000000..78a0fb0
--- /dev/null
+++ b/apps/web-shared/src/styles/components/select-autocomplete.scss
@@ -0,0 +1,173 @@
+@use '../base' as *;
+@use 'autocomplete.scss' as *;
+
+/* --------------------------------
+
+File#: _3_select-autocomplete
+Title: Select Autocomplete
+Descr: Selection dropdown with autocomplete
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.select-auto {
+ &.autocomplete {
+ --autocomplete-dropdown-vertical-gap: 4px; // gap between input and results list
+ --autocomplete-dropdown-max-height: 250px;
+ --autocomplete-dropdown-scrollbar-width: 6px; // custom scrollbar - webkit browsers
+ }
+}
+
+// input
+.select-auto__input-wrapper {
+ --input-btn-size: 1.25em; // btn/icon size
+ --input-btn-icon-size: 16px; // btn/icon size
+ --input-btn-text-gap: var(--space-xxs); // gap between button/icon and text
+
+ position: relative;
+ background: var(--color-bg-dark);
+ line-height: 1.2;
+ box-shadow: inset 0 0 0 1px var(--color-contrast-lower);
+
+ &.multiple {
+ display: flex;
+ flex-direction: row;
+ flex-flow: wrap;
+
+ .chip {
+ white-space: nowrap;
+ margin-right: 1px;
+ }
+
+ input[type="text"] {
+ width: auto;
+ }
+
+ @media (max-width: 756px) {
+ flex-flow: column !important;
+
+ &.has-selection {
+ input[type="text"] {
+ margin-top: 5px;
+ }
+
+ .chip {
+ justify-content: space-between;
+
+ .chip__btn {
+ margin-right: 0 !important;;
+ }
+ }
+ }
+ }
+ }
+
+ &::placeholder {
+ opacity: 1;
+ color: var(--color-contrast-low);
+ }
+
+ &:focus-within {
+ background: var(--color-bg);
+ box-shadow: inset 0 0 0 1px alpha(var(--color-contrast-lower), 0), 0px 0px 0px 1px var(--color-primary);
+ outline: none;
+ }
+
+ .form-control {
+ width: 100%;
+ height: 100%;
+ padding-right: calc(var(--form-control-padding-x) + var(--input-btn-size) + var(--input-btn-text-gap));
+ }
+}
+
+
+.select-auto__input-icon-wrapper {
+ width: var(--input-btn-size);
+ height: var(--input-btn-size);
+
+ position: absolute;
+ bottom: calc(var(--input-btn-size) / 3);
+ right: var(--form-control-padding-x);
+ display: flex;
+ pointer-events: none;
+
+ .icon {
+ display: block;
+ margin: auto;
+ width: var(--input-btn-icon-size, 16px);
+ height: var(--input-btn-icon-size, 16px);
+ }
+}
+
+.select-auto__input-btn {
+ display: none;
+ justify-content: center;
+ align-items: center;
+ width: inherit;
+ height: inherit;
+ pointer-events: auto;
+ cursor: pointer;
+ color: var(--color-contrast-medium); // icon color
+
+ &:hover {
+ color: var(--color-contrast-high);
+ }
+}
+
+.select-auto--selection-done {
+ .select-auto__input-icon-wrapper > .icon {
+ display: none;
+ }
+
+ .select-auto__input-btn {
+ display: flex;
+ }
+}
+
+// dropdown
+.select-auto__results {
+ // reset spacing and typography
+ @include spaceUnit(1rem);
+ @include textUnit(1rem);
+}
+
+// single result item
+.select-auto__option {
+ position: relative;
+ cursor: pointer;
+
+ &:hover {
+ background-color: alpha(var(--color-contrast-higher), 0.05);
+ }
+
+ &:focus {
+ outline: none;
+ background-color: alpha(var(--color-primary), 0.12);
+ }
+
+ &.select-auto__option--selected {
+ background-color: var(--color-primary);
+ color: var(--color-white);
+ padding-right: calc(1em + var(--space-sm));
+ @include fontSmooth;
+
+ &:focus {
+ background-color: var(--color-primary-dark);
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ right: var(--space-sm);
+ top: calc(50% - 0.5em);
+ height: 1em;
+ width: 1em;
+ background-color: currentColor;
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpolyline stroke-width='2' stroke='%23ffffff' fill='none' stroke-linecap='round' stroke-linejoin='round' points='1,9 5,13 15,3 '/%3E%3C/svg%3E");
+ }
+ }
+}
+
+.select-auto__group-title, .select-auto__no-results-msg {
+ outline: none;
+}
diff --git a/apps/web-shared/src/styles/components/tabbed-navigation.scss b/apps/web-shared/src/styles/components/tabbed-navigation.scss
new file mode 100644
index 0000000..4090fca
--- /dev/null
+++ b/apps/web-shared/src/styles/components/tabbed-navigation.scss
@@ -0,0 +1,133 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_tabbed-navigation-v2
+Title: Tabbed Navigation v2
+Descr: Tabbed (secondary) navigation
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.tabs-nav-v2 {
+ display: flex;
+ flex-wrap: wrap;
+
+ .tab-v2 {
+ display: inline-block; // flexbox fallback
+ display: inline-flex;
+ align-items: center;
+ }
+}
+
+.tabs-nav-v2__item {
+ display: inline-block;
+ padding: var(--space-xxs) var(--space-sm);
+ color: inherit;
+ white-space: nowrap;
+ text-decoration: none;
+}
+
+.tabs-nav-v2__item--selected,
+.tabs-nav-v2__item[aria-selected="true"] {
+ color: var(--color-bg);
+ background-color: var(--color-contrast-higher);
+}
+
+@include breakpoint(md) {
+ .tabs-nav-v2 {
+ .tab-v2 {
+ margin: 0;
+ }
+ }
+
+ .tabs-nav-v2__item {
+ background-color: transparent;
+ margin: var(--space-xxs) var(--space-sm);
+ padding: var(--space-xxxs) var(--space-xxs) !important;
+ border-radius: var(--radius-md);
+
+ &:hover {
+ background-color: alpha(var(--color-primary), 0.035);
+ color: var(--color-primary);
+ }
+ }
+
+ .tabs-nav-v2__item--selected,
+ .tabs-nav-v2__item[aria-selected="true"] {
+
+ background-color: alpha(var(--color-primary), 0.075);
+ color: var(--color-primary);
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: calc(var(--tabs-nav-border-width) * -1);
+ left: 0;
+ width: 100%;
+ height: var(--tabs-nav-border-width);
+ background-color: var(--color-bg);
+ }
+
+ &:hover {
+ background-color: alpha(var(--color-primary), 0.075);
+ }
+ }
+}
+
+:root {
+ --s-tabs-border-bottom-width: 1px;
+ --s-tabs-selected-item-border-bottom-width: 1px;
+}
+
+.s-tabs {
+ position: relative;
+
+ &::after { /* gradient - truncate text */
+ content: '';
+ position: absolute;
+ right: -1px;
+ top: 0;
+ height: calc(100% - var(--s-tabs-border-bottom-width));
+ width: 2em;
+ pointer-events: none;
+ z-index: 1;
+ }
+}
+
+.s-tabs__list {
+ display: flex;
+ overflow: auto;
+ -webkit-overflow-scrolling: auto;
+
+ &::after { /* border bottom */
+ content: '';
+ position: absolute;
+ width: 100%;
+ height: var(--s-tabs-border-bottom-width);
+ left: 0;
+ bottom: 0;
+ background-color: var(--color-contrast-lower);
+ }
+}
+
+.s-tabs__link {
+ color: var(--color-contrast-medium);
+ text-decoration: none;
+ display: inline-block;
+ padding: var(--space-xs) var(--space-sm);
+ white-space: nowrap;
+ border-bottom: var(--s-tabs-selected-item-border-bottom-width) solid transparent;
+ z-index: 1;
+
+ &:hover:not(.s-tabs__link--current) {
+ color: var(--color-contrast-high);
+ }
+}
+
+.s-tabs__link--current {
+ position: relative;
+ color: var(--color-primary);
+ border-bottom-color: var(--color-primary);
+}
diff --git a/apps/web-shared/src/styles/components/table.scss b/apps/web-shared/src/styles/components/table.scss
new file mode 100644
index 0000000..af8207f
--- /dev/null
+++ b/apps/web-shared/src/styles/components/table.scss
@@ -0,0 +1,147 @@
+@use '../base'as *;
+
+/* --------------------------------
+
+File#: _1_table
+Title: Table
+Descr: Data tables used to organize and display information in rows and columns
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+// >>> style affecting all (block + expanded) versions 👇
+.table {
+ position: relative;
+ z-index: 1;
+}
+
+// <<< end style affecting all versions
+
+// >>> block version only (mobile) 👇
+.table:not(.table--expanded) {
+ border-collapse: separate;
+ border-spacing: 0 var(--space-md); // row gap
+ margin-top: calc(-2 * var(--space-md)); // set spacing variable = row gap ☝️
+
+ .table__header {
+ // hide table header - but keep it accessible to SR
+ @include srHide;
+ }
+
+ .table__row {
+ .table__cell:first-child {
+ border-radius: var(--radius-md) var(--radius-md) 0 0;
+ }
+
+ .table__cell:last-child {
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
+
+ &::after {
+ // hide border bottom
+ display: none;
+ }
+ }
+ }
+
+ .table__cell {
+ position: relative;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ text-align: right;
+ padding: var(--space-md);
+ background-color: var(--color-bg-light);
+
+ &::after {
+ // border bottom
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: var(--space-md);
+ width: calc(100% - (2 * var(--space-md)));
+ height: 1px;
+ background-color: var(--color-contrast-lower);
+ }
+ }
+
+ .table__label {
+ // inline labels -> visible when table header is hidden
+ font-weight: bold;
+ text-align: left;
+ color: var(--color-contrast-higher);
+ margin-right: var(--space-md);
+ }
+}
+
+// <<< end block version
+
+// >>> expanded version only (desktop) 👇 -> show standard rows and cols
+.table--expanded {
+ border-bottom: 1px solid var(--color-contrast-lower); // table border bottom
+
+ .table__header {
+ .table__cell {
+ // header cell style
+ position: relative;
+ z-index: 10;
+ background-color: var(--color-bg);
+ border-bottom: 1px solid var(--color-contrast-lower); // header border bottom
+ font-weight: bold;
+ color: var(--color-contrast-higher);
+ }
+ }
+
+ .table__body {
+ .table__row {
+ &:nth-child(odd) {
+ background-color: alpha(var(--color-bg-dark), 0.85);
+ }
+ }
+ }
+
+ .table__cell {
+ padding: var(--space-xxxs);
+ }
+
+ .table__label {
+ // hide inline labels
+ display: none;
+ }
+
+ // --header-sticky
+ .table__header--sticky {
+ .table__cell {
+ // header cell style
+ position: sticky;
+ top: 0;
+ }
+ }
+}
+
+// <<< end expanded version
+
+.js {
+ .table {
+ opacity: 0; // hide table while it is initialized in JS
+ }
+
+ .table--loaded {
+ opacity: 1;
+ }
+}
+
+// detect when the table needs to switch from the mobile layout to an expanded one - used in JS
+[class*="table--expanded"]::before {
+ display: none;
+}
+
+@each $breakpoint,
+$value in $breakpoints {
+ .table--expanded\@#{$breakpoint}::before {
+ content: 'collapsed';
+
+ @include breakpoint(#{$breakpoint}) {
+ content: 'expanded';
+ }
+ }
+}
diff --git a/apps/web-shared/src/styles/components/user-menu.scss b/apps/web-shared/src/styles/components/user-menu.scss
new file mode 100644
index 0000000..1b5c1d5
--- /dev/null
+++ b/apps/web-shared/src/styles/components/user-menu.scss
@@ -0,0 +1,81 @@
+@use '../base' as *;
+@use 'menu.scss' as *;
+
+/* --------------------------------
+
+File#: _2_user-menu
+Title: User Menu
+Descr: A menu controlled by the user profile image
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.user-menu-control {
+ --profile-figure-size: 40px;
+
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ text-align: left;
+
+ &:hover {
+ .user-menu-control__img-wrapper {
+ opacity: 0.8;
+ }
+
+ .user-menu__meta-title {
+ color: var(--color-primary);
+ }
+ }
+
+ &:focus, &.menu-control--active {
+ outline: none;
+
+ .user-menu-control__img-wrapper::after {
+ opacity: 1;
+ transform: scale(1);
+ }
+ }
+}
+
+.user-menu-control__img-wrapper {
+ width: var(--profile-figure-size);
+ height: var(--profile-figure-size);
+ position: relative;
+ transition: opacity 0.2s;
+
+ &::after {
+ content: '';
+ position: absolute;
+ z-index: -1;
+ left: -4px;
+ top: -4px;
+ width: 100%;
+ height: 100%;
+ border-radius: inherit;
+ width: calc(var(--profile-figure-size) + 8px);
+ height: calc(var(--profile-figure-size) + 8px);
+ border: 2px solid var(--color-primary);
+ pointer-events: none;
+
+ opacity: 0;
+ transform: scale(0.8);
+
+ transition: all 0.2s;
+ }
+}
+
+.user-menu-control__img {
+ display: block;
+ width: 100%;
+ object-fit: cover;
+ border-radius: inherit;
+}
+
+.user-menu__meta {
+ max-width: 100px;
+}
+
+.user-menu__meta-title {
+ transition: color 0.2s;
+}
diff --git a/apps/web-shared/src/styles/custom-style/_buttons.scss b/apps/web-shared/src/styles/custom-style/_buttons.scss
new file mode 100644
index 0000000..e396f8d
--- /dev/null
+++ b/apps/web-shared/src/styles/custom-style/_buttons.scss
@@ -0,0 +1,111 @@
+@use '../base' as *;
+
+// --------------------------------
+
+// (START) Global editor code https://codyhouse.co/ds/globals/buttons
+
+// --------------------------------
+
+:root {
+ --btn-font-size: 1em;
+ --btn-padding-x: var(--space-xxs);
+ --btn-padding-y: var(--space-xxxs);
+ --btn-radius: var(--radius-sm);
+}
+
+.btn {
+ background: var(--color-bg-dark);
+ color: var(--color-contrast-higher);
+ cursor: pointer;
+ text-decoration: none;
+ line-height: 1.2;
+ @include fontSmooth;
+ will-change: transform;
+
+ &:focus {
+ box-shadow: 0px 0px 0px 2px alpha(var(--color-contrast-higher), 0.15);
+ outline: none;
+ }
+}
+
+.btn--link {
+ @include reset;
+ color: inherit;
+ cursor: pointer;
+ text-decoration: none;
+
+ &:hover {
+ color: var(--color-primary);
+ }
+}
+
+// themes
+.btn--primary {
+ background: var(--color-primary);
+ color: var(--color-white);
+ box-shadow: inset 0px 1px 0px alpha(var(--color-white), 0.15), var(--shadow-xs);
+
+ &:hover {
+ background: var(--color-primary-light);
+ box-shadow: inset 0px 1px 0px alpha(var(--color-white), 0.15), var(--shadow-sm);
+ }
+
+ &:focus {
+ box-shadow: inset 0px 1px 0px alpha(var(--color-white), 0.15), 0px 0px 0px 2px alpha(var(--color-primary), 0.2);
+ }
+}
+
+.btn--subtle {
+ background: var(--color-bg-light);
+ color: var(--color-contrast-higher);
+ box-shadow: inset 0px 0px 0px 1px var(--color-contrast-lower), var(--shadow-xs);
+
+ &:hover {
+ background: var(--color-bg-lighter);
+ box-shadow: inset 0px 0px 0px 1px var(--color-contrast-lower), var(--shadow-sm);
+ }
+
+ &:focus {
+ box-shadow: inset 0px 0px 0px 1px var(--color-contrast-lower), 0px 0px 0px 2px alpha(var(--color-contrast-higher), 0.05);
+ }
+}
+
+.btn--accent {
+ background: var(--color-accent);
+ color: var(--color-white);
+ box-shadow: inset 0px 1px 0px alpha(var(--color-white), 0.15), var(--shadow-xs);
+
+ &:hover {
+ background: var(--color-accent-light);
+ box-shadow: inset 0px 1px 0px alpha(var(--color-white), 0.15), var(--shadow-sm);
+ }
+
+ &:focus {
+ box-shadow: inset 0px 1px 0px alpha(var(--color-white), 0.15), 0px 0px 0px 2px alpha(var(--color-accent), 0.2);
+ }
+}
+
+// feedback
+.btn--disabled, .btn[disabled], .btn[readonly] {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+// size
+.btn--sm {
+ font-size: 0.8em;
+}
+
+.btn--md {
+ font-size: 1.2em;
+}
+
+.btn--lg {
+ font-size: 1.4em;
+}
+
+// --------------------------------
+
+// (END) Global editor code
+
+// --------------------------------
diff --git a/apps/web-shared/src/styles/custom-style/_colors.scss b/apps/web-shared/src/styles/custom-style/_colors.scss
new file mode 100644
index 0000000..76d3fa6
--- /dev/null
+++ b/apps/web-shared/src/styles/custom-style/_colors.scss
@@ -0,0 +1,119 @@
+@use '../base' as *;
+
+// --------------------------------
+
+// (START) Global editor code https://codyhouse.co/ds/globals/colors
+
+// --------------------------------
+
+:root, [data-theme="default"] {
+ // main
+ @include defineColorHSL(--color-primary-darker, 250, 84%, 38%);
+ @include defineColorHSL(--color-primary-dark, 250, 84%, 46%);
+ @include defineColorHSL(--color-primary, 250, 84%, 54%);
+ @include defineColorHSL(--color-primary-light, 250, 84%, 60%);
+ @include defineColorHSL(--color-primary-lighter, 250, 84%, 67%);
+
+ @include defineColorHSL(--color-accent-darker, 342, 89%, 38%);
+ @include defineColorHSL(--color-accent-dark, 342, 89%, 43%);
+ @include defineColorHSL(--color-accent, 342, 89%, 48%);
+ @include defineColorHSL(--color-accent-light, 342, 89%, 56%);
+ @include defineColorHSL(--color-accent-lighter, 342, 89%, 62%);
+
+ @include defineColorHSL(--color-black, 204, 28%, 7%);
+ @include defineColorHSL(--color-white, 0, 0%, 100%);
+
+ // feedback
+ @include defineColorHSL(--color-warning-darker, 46, 100%, 47%);
+ @include defineColorHSL(--color-warning-dark, 46, 100%, 50%);
+ @include defineColorHSL(--color-warning, 46, 100%, 61%);
+ @include defineColorHSL(--color-warning-light, 46, 100%, 71%);
+ @include defineColorHSL(--color-warning-lighter, 46, 100%, 80%);
+
+ @include defineColorHSL(--color-success-darker, 122, 50%, 47%);
+ @include defineColorHSL(--color-success-dark, 122, 50%, 52%);
+ @include defineColorHSL(--color-success, 122, 50%, 60%);
+ @include defineColorHSL(--color-success-light, 122, 50%, 69%);
+ @include defineColorHSL(--color-success-lighter, 122, 50%, 76%);
+
+ @include defineColorHSL(--color-error-darker, 342, 89%, 38%);
+ @include defineColorHSL(--color-error-dark, 342, 89%, 43%);
+ @include defineColorHSL(--color-error, 342, 89%, 48%);
+ @include defineColorHSL(--color-error-light, 342, 89%, 56%);
+ @include defineColorHSL(--color-error-lighter, 342, 89%, 62%);
+
+ // background
+ @include defineColorHSL(--color-bg-darker, 210, 4%, 89%);
+ @include defineColorHSL(--color-bg-dark, 180, 3%, 94%);
+ @include defineColorHSL(--color-bg, 210, 17%, 98%);
+ @include defineColorHSL(--color-bg-light, 180, 3%, 100%);
+ @include defineColorHSL(--color-bg-lighter, 210, 4%, 100%);
+
+ // color contrasts
+ @include defineColorHSL(--color-contrast-lower, 180, 1%, 84%);
+ @include defineColorHSL(--color-contrast-low, 210, 2%, 64%);
+ @include defineColorHSL(--color-contrast-medium, 204, 2%, 46%);
+ @include defineColorHSL(--color-contrast-high, 210, 7%, 21%);
+ @include defineColorHSL(--color-contrast-higher, 204, 28%, 7%);
+}
+
+[data-theme="dark"] {
+ // main
+ @include defineColorHSL(--color-primary-darker, 250, 93%, 57%);
+ @include defineColorHSL(--color-primary-dark, 250, 93%, 61%);
+ @include defineColorHSL(--color-primary, 250, 93%, 65%);
+ @include defineColorHSL(--color-primary-light, 250, 93%, 69%);
+ @include defineColorHSL(--color-primary-lighter, 250, 93%, 72%);
+
+ @include defineColorHSL(--color-accent-darker, 342, 92%, 41%);
+ @include defineColorHSL(--color-accent-dark, 342, 92%, 47%);
+ @include defineColorHSL(--color-accent, 342, 92%, 54%);
+ @include defineColorHSL(--color-accent-light, 342, 92%, 60%);
+ @include defineColorHSL(--color-accent-lighter, 342, 92%, 65%);
+
+ @include defineColorHSL(--color-black, 204, 28%, 7%);
+ @include defineColorHSL(--color-white, 0, 0%, 100%);
+
+ // feedback
+ @include defineColorHSL(--color-warning-darker, 46, 100%, 47%);
+ @include defineColorHSL(--color-warning-dark, 46, 100%, 50%);
+ @include defineColorHSL(--color-warning, 46, 100%, 61%);
+ @include defineColorHSL(--color-warning-light, 46, 100%, 71%);
+ @include defineColorHSL(--color-warning-lighter, 46, 100%, 80%);
+
+ @include defineColorHSL(--color-success-darker, 122, 50%, 47%);
+ @include defineColorHSL(--color-success-dark, 122, 50%, 52%);
+ @include defineColorHSL(--color-success, 122, 50%, 60%);
+ @include defineColorHSL(--color-success-light, 122, 50%, 69%);
+ @include defineColorHSL(--color-success-lighter, 122, 50%, 76%);
+
+ @include defineColorHSL(--color-error-darker, 342, 92%, 41%);
+ @include defineColorHSL(--color-error-dark, 342, 92%, 47%);
+ @include defineColorHSL(--color-error, 342, 92%, 54%);
+ @include defineColorHSL(--color-error-light, 342, 92%, 60%);
+ @include defineColorHSL(--color-error-lighter, 342, 92%, 65%);
+
+ // background
+ @include defineColorHSL(--color-bg-darker, 204, 15%, 6%);
+ @include defineColorHSL(--color-bg-dark, 203, 18%, 9%);
+ @include defineColorHSL(--color-bg, 203, 24%, 13%);
+ @include defineColorHSL(--color-bg-light, 203, 18%, 17%);
+ @include defineColorHSL(--color-bg-lighter, 204, 15%, 20%);
+
+ // color contrasts
+ @include defineColorHSL(--color-contrast-lower, 208, 12%, 24%);
+ @include defineColorHSL(--color-contrast-low, 208, 6%, 40%);
+ @include defineColorHSL(--color-contrast-medium, 213, 5%, 56%);
+ @include defineColorHSL(--color-contrast-high, 223, 8%, 82%);
+ @include defineColorHSL(--color-contrast-higher, 240, 100%, 99%);
+
+ // font rendering
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+// --------------------------------
+
+// (END) Global editor code
+
+// --------------------------------
diff --git a/apps/web-shared/src/styles/custom-style/_forms.scss b/apps/web-shared/src/styles/custom-style/_forms.scss
new file mode 100644
index 0000000..0048941
--- /dev/null
+++ b/apps/web-shared/src/styles/custom-style/_forms.scss
@@ -0,0 +1,56 @@
+@use '../base' as *;
+
+// --------------------------------
+
+// (START) Global editor code https://codyhouse.co/ds/globals/forms
+
+// --------------------------------
+
+:root {
+ --form-control-font-size: 1em;
+ --form-control-padding-x: var(--space-xs);
+ --form-control-padding-y: var(--space-xxs);
+ --form-control-radius: var(--radius-sm);
+}
+
+.form-control {
+ background: var(--color-bg-dark);
+ line-height: 1.2;
+ box-shadow: inset 0 0 0 1px var(--color-contrast-lower);
+
+ &::placeholder {
+ opacity: 1;
+ color: var(--color-contrast-low);
+ }
+
+ &:focus {
+ background: var(--color-bg);
+ box-shadow: inset 0 0 0 1px alpha(var(--color-contrast-lower), 0), 0px 0px 0px 1px var(--color-primary);
+ outline: none;
+ }
+}
+
+.form-control--disabled, .form-control[disabled], .form-control[readonly] {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.form-control[aria-invalid="true"], .form-control.form-control--error {
+ box-shadow: inset 0 0 0 1px alpha(var(--color-contrast-lower), 0), 0px 0px 0px 2px var(--color-error);
+
+ &:focus {
+ box-shadow: inset 0 0 0 1px alpha(var(--color-contrast-lower), 0), 0px 0px 0px 2px var(--color-error), var(--shadow-sm);
+ }
+}
+
+.form-legend {
+}
+
+.form-label {
+}
+
+// --------------------------------
+
+// (END) Global editor code
+
+// --------------------------------
diff --git a/apps/web-shared/src/styles/custom-style/_icons.scss b/apps/web-shared/src/styles/custom-style/_icons.scss
new file mode 100644
index 0000000..a9fcb46
--- /dev/null
+++ b/apps/web-shared/src/styles/custom-style/_icons.scss
@@ -0,0 +1,19 @@
+@use '../base' as *;
+
+:root {
+ // size - 👇 uncomment to modify default icon sizes
+ // --icon-xxxs: 8px;
+ // --icon-xxs: 12px;
+ // --icon-xs: 16px;
+ // --icon-sm: 24px;
+ // --icon-md: 32px;
+ // --icon-lg: 48px;
+ // --icon-xl: 64px;
+ // --icon-xxl: 96px;
+ // --icon-xxxl: 128px;
+}
+
+.icon {
+ // 👇 include the font-family declaration here if you are using an icon font
+ // font-family: 'fontName';
+} \ No newline at end of file
diff --git a/apps/web-shared/src/styles/custom-style/_shared-styles.scss b/apps/web-shared/src/styles/custom-style/_shared-styles.scss
new file mode 100644
index 0000000..e9a32b8
--- /dev/null
+++ b/apps/web-shared/src/styles/custom-style/_shared-styles.scss
@@ -0,0 +1,59 @@
+@use '../base' as *;
+
+:root {
+ --radius: 0.25em; // border radius base size
+ --radius-sm: calc(var(--radius) / 2);
+ --radius-md: var(--radius);
+ --radius-lg: calc(var(--radius) * 2);
+ --shadow-xs: 0;
+ --shadow-sm: 0;
+ --shadow-md: 0;
+ --shadow-lg: 0;
+ --shadow-xl: 0;
+}
+
+// --------------------------------
+
+// (START) Global editor code https://codyhouse.co/ds/globals/shared-styles
+
+// --------------------------------
+
+.hover\:reduce-opacity {
+ opacity: 1;
+
+ &:hover {
+ opacity: 0.8;
+ }
+}
+
+.hover\:scale {
+
+ &:hover {
+ transform: scale(1.1);
+ }
+}
+
+.hover\:elevate {
+ box-shadow: var(--shadow-sm);
+
+ &:hover {
+ box-shadow: var(--shadow-md);
+ }
+}
+
+// text styles
+.link-subtle {
+ color: inherit;
+ cursor: pointer;
+ text-decoration: none;
+
+ &:hover {
+ color: var(--color-primary);
+ }
+}
+
+// --------------------------------
+
+// (END) Global editor code
+
+// --------------------------------
diff --git a/apps/web-shared/src/styles/custom-style/_spacing.scss b/apps/web-shared/src/styles/custom-style/_spacing.scss
new file mode 100644
index 0000000..56cd451
--- /dev/null
+++ b/apps/web-shared/src/styles/custom-style/_spacing.scss
@@ -0,0 +1,49 @@
+@use '../base' as *;
+
+// --------------------------------
+
+// (START) Global editor code https://codyhouse.co/ds/globals/spacing
+
+// --------------------------------
+
+// 👇 uncomment to modify default spacing scale
+// :root {
+// --space-unit: 1rem;
+// }
+
+ :root, * {
+ --space-xxxxs: calc(0.125 * var(--space-unit));
+ --space-xxxs: calc(0.25 * var(--space-unit));
+ --space-xxs: calc(0.375 * var(--space-unit));
+ --space-xs: calc(0.5 * var(--space-unit));
+ --space-sm: calc(0.75 * var(--space-unit));
+ --space-md: calc(1.25 * var(--space-unit));
+ --space-lg: calc(2 * var(--space-unit));
+ --space-xl: calc(3.25 * var(--space-unit));
+ --space-xxl: calc(5.25 * var(--space-unit));
+ --space-xxxl: calc(8.5 * var(--space-unit));
+ --space-xxxxl: calc(13.75 * var(--space-unit));
+ --component-padding: var(--space-sm);
+ }
+
+@include breakpoint(md) {
+ :root, * {
+ --space-xxxxs: calc(0.1875 * var(--space-unit));
+ --space-xxxs: calc(0.375 * var(--space-unit));
+ --space-xxs: calc(0.5625 * var(--space-unit));
+ --space-xs: calc(0.75 * var(--space-unit));
+ --space-sm: calc(1.125 * var(--space-unit));
+ --space-md: calc(2 * var(--space-unit));
+ --space-lg: calc(3.125 * var(--space-unit));
+ --space-xl: calc(5.125 * var(--space-unit));
+ --space-xxl: calc(8.25 * var(--space-unit));
+ --space-xxxl: calc(13.25 * var(--space-unit));
+ --space-xxxxl: calc(21.5 * var(--space-unit));
+ }
+}
+
+// --------------------------------
+
+// (END) Global editor code
+
+// --------------------------------
diff --git a/apps/web-shared/src/styles/custom-style/_typography.scss b/apps/web-shared/src/styles/custom-style/_typography.scss
new file mode 100644
index 0000000..d0bb431
--- /dev/null
+++ b/apps/web-shared/src/styles/custom-style/_typography.scss
@@ -0,0 +1,92 @@
+@use '../base' as *;
+
+// --------------------------------
+
+// (START) Global editor code https://codyhouse.co/ds/globals/typography
+
+// --------------------------------
+
+:root {
+ // font family
+ //--font-primary: Inter, system-ui, sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
+
+ // font size
+ --text-base-size: 1.25rem; // body font-size
+ --text-scale-ratio: 1.1; // multiplier used to generate the type scale values 👇
+
+ // line-height
+ --body-line-height: 1.2;
+ --heading-line-height: 1.34;
+
+ // capital letters - used in combo with the lhCrop mixin
+ --font-primary-capital-letter: 1;
+
+ // unit - don't modify unless you want to change the typography unit (e.g., from Rem to Em units)
+ --text-unit: var(--text-base-size); // if Em units → --text-unit: var(--text-base-size);
+}
+
+:root, * {
+ --text-xs: calc((var(--text-unit) / var(--text-scale-ratio)) / var(--text-scale-ratio));
+ --text-sm: calc(var(--text-xs) * var(--text-scale-ratio));
+ --text-md: calc(var(--text-sm) * var(--text-scale-ratio) * var(--text-scale-ratio));
+ --text-lg: calc(var(--text-md) * var(--text-scale-ratio));
+ --text-xl: calc(var(--text-lg) * var(--text-scale-ratio));
+ --text-xxl: calc(var(--text-xl) * var(--text-scale-ratio));
+ --text-xxxl: calc(var(--text-xxl) * var(--text-scale-ratio));
+ --text-xxxxl: calc(var(--text-xxxl) * var(--text-scale-ratio));
+}
+
+body {
+ font-family: var(--font-primary);
+}
+
+h1, h2, h3, h4 {
+ font-family: var(--font-primary);
+ --heading-font-weight: 600;
+}
+
+// font family
+.font-primary { font-family: var(--font-primary); }
+
+// --------------------------------
+
+// (END) Global editor code
+
+// --------------------------------
+
+// link style
+a, .link {}
+
+mark {
+ background-color: alpha(var(--color-accent), 0.2);
+ color: inherit;
+}
+
+.text-component {
+ --text-unit: 1em;
+ --space-unit: 1em;
+ --line-height-multiplier: 1;
+ --text-space-y-multiplier: 1;
+
+ blockquote {
+ padding-left: 1em;
+ border-left: 4px solid var(--color-contrast-lower);
+ font-style: italic;
+ }
+
+ hr {
+ background: var(--color-contrast-lower);
+ height: 1px;
+ }
+
+ figcaption {
+ font-size: var(--text-sm);
+ color: var(--color-contrast-low);
+ }
+}
+
+.article { // e.g., blog posts
+ --body-line-height: 1.58; // set body line-height
+ --text-space-y-multiplier: 1.2; // control vertical spacing
+}
diff --git a/apps/web-shared/src/styles/custom-style/_util.scss b/apps/web-shared/src/styles/custom-style/_util.scss
new file mode 100644
index 0000000..5677630
--- /dev/null
+++ b/apps/web-shared/src/styles/custom-style/_util.scss
@@ -0,0 +1,41 @@
+@use '../base' as *;
+
+// --------------------------------
+
+// How to create custom utility classes 👇
+
+// --------------------------------
+
+.border-none {
+ border: none !important;
+}
+
+@each $breakpoint, $value in $breakpoints {
+ @include breakpoint(#{$breakpoint}) {
+ .border-none\@#{$breakpoint} {
+ border: none !important;
+ }
+ }
+}
+
+.left-unset {
+ left: unset !important;
+}
+
+.cursor-wait {
+ cursor: wait !important;
+}
+
+.bg-error-lighter\@hover {
+ &:hover,
+ &:active {
+ background-color: var(--color-error-lighter) !important;
+ }
+}
+
+.color-white\@hover {
+ &:hover,
+ &:active {
+ color: var(--color-white) !important;
+ }
+}