Migrates to svelte 5
This commit is contained in:
@@ -1,15 +1,38 @@
|
||||
<script lang="ts">
|
||||
export let type: "button" | "submit" | "reset" | null | undefined =
|
||||
undefined;
|
||||
export let icon: string = "";
|
||||
export let extraClasses: string = "";
|
||||
export let primary: boolean = false;
|
||||
export let secondary: boolean = false;
|
||||
export let large: boolean = false;
|
||||
export let loading: boolean = false;
|
||||
export let disabled: boolean = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let id: string | undefined = undefined
|
||||
import type { Snippet } from "svelte";
|
||||
import type { MouseEventHandler } from "svelte/elements";
|
||||
import { createBubbler } from "svelte/legacy";
|
||||
|
||||
const bubble = createBubbler();
|
||||
interface Props {
|
||||
type?: "button" | "submit" | "reset" | null | undefined;
|
||||
icon?: string;
|
||||
extraClasses?: string;
|
||||
primary?: boolean;
|
||||
secondary?: boolean;
|
||||
large?: boolean;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
tooltip?: string | undefined;
|
||||
id?: string | undefined;
|
||||
children?: Snippet;
|
||||
onclick?: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
let {
|
||||
type = undefined,
|
||||
icon = "",
|
||||
extraClasses = "",
|
||||
primary = false,
|
||||
secondary = false,
|
||||
large = false,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
tooltip = undefined,
|
||||
id = undefined,
|
||||
children,
|
||||
onclick = undefined,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
@@ -21,7 +44,7 @@
|
||||
disabled={disabled || loading}
|
||||
class:tooltip={(tooltip?.length ?? 0) > 0}
|
||||
data-title={tooltip}
|
||||
on:click
|
||||
{onclick}
|
||||
{type}
|
||||
{id}
|
||||
>
|
||||
@@ -29,7 +52,7 @@
|
||||
{#if icon}
|
||||
<i class="fa fa-{icon} mr-2"></i>
|
||||
{/if}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{#if loading}
|
||||
<div class="absolute aspect-square spinner"></div>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { SummitLog } from "$lib/models/summit_log";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { isSameDay, isToday } from "../../util/date_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
export let logs: SummitLog[] = [];
|
||||
export let colorMap: Record<string, string> = {};
|
||||
interface Props {
|
||||
logs?: SummitLog[];
|
||||
colorMap?: Record<string, string>;
|
||||
onforward?: (data: { start: Date; end: Date }) => void;
|
||||
onbackward?: (data: { start: Date; end: Date }) => void;
|
||||
onclick?: (date: Date) => void;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
forward: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
backward: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
click: Date;
|
||||
}>();
|
||||
let {
|
||||
logs = [],
|
||||
colorMap = {},
|
||||
onforward,
|
||||
onbackward,
|
||||
onclick,
|
||||
}: Props = $props();
|
||||
|
||||
const weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||
const months = [
|
||||
@@ -34,14 +34,13 @@
|
||||
"Dezember",
|
||||
];
|
||||
const today = new Date();
|
||||
let currentMonth = today.getMonth();
|
||||
let currentYear = today.getFullYear();
|
||||
let currentMonth = $state(today.getMonth());
|
||||
let currentYear = $state(today.getFullYear());
|
||||
let currentMonthArray: ({
|
||||
date: Date | undefined;
|
||||
today: boolean;
|
||||
log?: SummitLog;
|
||||
} | null)[];
|
||||
$: currentMonthArray = generateMonthArray(currentYear, currentMonth, logs);
|
||||
} | null)[] = $derived(generateMonthArray(currentYear, currentMonth, logs));
|
||||
|
||||
function calculateFirstDayOfMonthDayOfWeek(year: number, month: number) {
|
||||
const date = new Date(year, month, 1);
|
||||
@@ -94,7 +93,7 @@
|
||||
} else {
|
||||
currentMonth++;
|
||||
}
|
||||
dispatch("forward", {
|
||||
onforward?.({
|
||||
start: new Date(currentYear, currentMonth, 1),
|
||||
end: new Date(currentYear, currentMonth + 1, 0),
|
||||
});
|
||||
@@ -107,7 +106,7 @@
|
||||
} else {
|
||||
currentMonth--;
|
||||
}
|
||||
dispatch("backward", {
|
||||
onbackward?.({
|
||||
start: new Date(currentYear, currentMonth, 1),
|
||||
end: new Date(currentYear, currentMonth + 1, 0),
|
||||
});
|
||||
@@ -124,7 +123,7 @@
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
dispatch("click", date);
|
||||
onclick?.(date);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -133,10 +132,12 @@
|
||||
<span class="text-lg">{months[currentMonth]}</span>
|
||||
<span>{currentYear}</span>
|
||||
</div>
|
||||
<button class="btn-icon mr-2" on:click={monthMinus}
|
||||
><i class="fa fa-caret-left"></i></button
|
||||
<button
|
||||
aria-label="Previous month"
|
||||
class="btn-icon mr-2"
|
||||
onclick={monthMinus}><i class="fa fa-caret-left"></i></button
|
||||
>
|
||||
<button class="btn-icon" on:click={monthPlus}
|
||||
<button aria-label="Next month" class="btn-icon" onclick={monthPlus}
|
||||
><i class="fa fa-caret-right"></i></button
|
||||
>
|
||||
</div>
|
||||
@@ -157,7 +158,7 @@
|
||||
{#each { length: 42 } as _, i}
|
||||
<button
|
||||
class="calendar-day flex items-center justify-center rounded-xl"
|
||||
on:click={() => handleDateClick(currentMonthArray[i]?.date)}
|
||||
onclick={() => handleDateClick(currentMonthArray[i]?.date)}
|
||||
class:today={currentMonthArray[i]?.today}
|
||||
style="background-color: {colorMap[
|
||||
colorKey(currentMonthArray, i)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script context="module" lang="ts">
|
||||
<script module lang="ts">
|
||||
export type ComboboxItem = {
|
||||
text: string;
|
||||
value: any;
|
||||
@@ -7,22 +7,41 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import type { ChangeEventHandler } from "svelte/elements";
|
||||
import TextField from "./text_field.svelte";
|
||||
import { tick } from "svelte";
|
||||
|
||||
export let name: string = "";
|
||||
export let icon: string = "";
|
||||
export let label: string = "";
|
||||
export let value: string = "";
|
||||
export let items: ComboboxItem[] = [];
|
||||
export let placeholder: string = "";
|
||||
export let extraClasses: string = "";
|
||||
interface Props {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
items?: ComboboxItem[];
|
||||
placeholder?: string;
|
||||
extraClasses?: string;
|
||||
onchange?: ChangeEventHandler<HTMLInputElement>;
|
||||
onupdate?: (q: string) => void;
|
||||
onclick?: (item: ComboboxItem) => void;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let {
|
||||
name = "",
|
||||
icon = "",
|
||||
label = "",
|
||||
value = $bindable(""),
|
||||
items = [],
|
||||
placeholder = "",
|
||||
extraClasses = "",
|
||||
onchange,
|
||||
onupdate,
|
||||
onclick,
|
||||
}: Props = $props();
|
||||
|
||||
let searching: boolean = false;
|
||||
let searching: boolean = $state(false);
|
||||
|
||||
$: dropDownOpen = value.length > 0 && items.length > 0 && searching;
|
||||
let dropDownOpen = $derived(
|
||||
value.length > 0 && items.length > 0 && searching,
|
||||
);
|
||||
|
||||
async function onSearchType() {
|
||||
await tick();
|
||||
@@ -45,12 +64,13 @@
|
||||
}
|
||||
|
||||
function update(q: string) {
|
||||
dispatch("update", q);
|
||||
onupdate?.(q);
|
||||
}
|
||||
|
||||
function handleItemClick(item: ComboboxItem) {
|
||||
function handleItemClick(e: Event, item: ComboboxItem) {
|
||||
e.stopPropagation();
|
||||
value = item.value;
|
||||
dispatch("click", item);
|
||||
onclick?.(item);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -63,10 +83,10 @@
|
||||
{label}
|
||||
{placeholder}
|
||||
bind:value
|
||||
on:change
|
||||
on:input={onSearchType}
|
||||
on:focusin={() => (searching = true)}
|
||||
on:focusout={() => (searching = false)}
|
||||
{onchange}
|
||||
oninput={onSearchType}
|
||||
onfocusin={() => (searching = true)}
|
||||
onfocusout={() => (searching = false)}
|
||||
></TextField>
|
||||
|
||||
{#if dropDownOpen}
|
||||
@@ -76,13 +96,13 @@
|
||||
style="z-index: 1001"
|
||||
>
|
||||
{#each items as item}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<li
|
||||
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||
tabindex="0"
|
||||
on:mousedown|stopPropagation={() => handleItemClick(item)}
|
||||
on:keydown|stopPropagation={() => handleItemClick(item)}
|
||||
onmousedown={(e) => handleItemClick(e, item)}
|
||||
onkeydown={(e) => handleItemClick(e, item)}
|
||||
>
|
||||
<i class="fa fa-{item.icon} mr-6"></i>
|
||||
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import type { ChangeEventHandler } from "svelte/elements";
|
||||
|
||||
export let name: string = "";
|
||||
export let value: string | Date = "";
|
||||
export let label: string = "";
|
||||
export let error: string | string[] | null = "";
|
||||
interface Props {
|
||||
name?: string;
|
||||
value?: string | Date;
|
||||
label?: string;
|
||||
error?: string | string[] | null;
|
||||
onchange?: ChangeEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
let {
|
||||
name = "",
|
||||
value = $bindable(),
|
||||
label = "",
|
||||
error = "",
|
||||
onchange
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -21,7 +33,7 @@
|
||||
class:bg-input-background-error={(error?.length ?? 0) > 0}
|
||||
type="date"
|
||||
bind:value
|
||||
on:change
|
||||
{onchange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
<script lang="ts">
|
||||
import * as noUiSlider from "nouislider";
|
||||
import "nouislider/dist/nouislider.css";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let minValue = 0;
|
||||
export let maxValue = 100;
|
||||
interface Props {
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
currentMin?: any;
|
||||
currentMax?: any;
|
||||
onset?: (data: [number, number]) => void;
|
||||
}
|
||||
|
||||
export let currentMin = minValue;
|
||||
export let currentMax = maxValue;
|
||||
let {
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
currentMin = $bindable(minValue),
|
||||
currentMax = $bindable(maxValue),
|
||||
onset,
|
||||
}: Props = $props();
|
||||
|
||||
let sliderContainer: any;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let sliderContainer: any = $state();
|
||||
|
||||
onMount(() => {
|
||||
const updateValues = (values: string[]) => {
|
||||
@@ -31,7 +39,7 @@
|
||||
sliderContainer.noUiSlider.on("update", updateValues);
|
||||
|
||||
sliderContainer.noUiSlider.on("set", () => {
|
||||
dispatch("set", [currentMin, currentMax]);
|
||||
onset?.([currentMin, currentMax]);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
<script lang="ts">
|
||||
|
||||
import { browser } from "$app/environment";
|
||||
import { beforeNavigate } from "$app/navigation";
|
||||
import type { Snippet } from 'svelte';
|
||||
import { backIn, backOut } from "svelte/easing";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
export let open: boolean = false;
|
||||
|
||||
$: if (open && browser) {
|
||||
document.body.style.position = "fixed";
|
||||
document.body.style.top = `-${window.scrollY}px`;
|
||||
} else if (browser) {
|
||||
const scrollY = document.body.style.top;
|
||||
document.body.style.position = "";
|
||||
document.body.style.top = "";
|
||||
window.scrollTo(0, parseInt(scrollY || "0") * -1);
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { open = $bindable(false), children }: Props = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (open && browser) {
|
||||
document.body.style.position = "fixed";
|
||||
document.body.style.top = `-${window.scrollY}px`;
|
||||
} else if (browser) {
|
||||
const scrollY = document.body.style.top;
|
||||
document.body.style.position = "";
|
||||
document.body.style.top = "";
|
||||
window.scrollTo(0, parseInt(scrollY || "0") * -1);
|
||||
}
|
||||
});
|
||||
|
||||
beforeNavigate(() => {
|
||||
open = false;
|
||||
});
|
||||
@@ -24,8 +33,8 @@
|
||||
{#if open}
|
||||
<div
|
||||
class="fixed drawer-overlay h-screen w-screen backdrop-blur-md z-50 bg-gray-500/50 cursor-pointer"
|
||||
on:click={() => (open = false)}
|
||||
on:keydown={(e) => {
|
||||
onclick={() => (open = false)}
|
||||
onkeydown={(e) => {
|
||||
if (e.key == "Escape") open = false;
|
||||
}}
|
||||
role="button"
|
||||
@@ -36,6 +45,6 @@
|
||||
in:slide={{ axis: "x", easing: backOut }}
|
||||
out:slide={{ axis: "x", easing: backIn }}
|
||||
>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script context="module" lang="ts">
|
||||
<script module lang="ts">
|
||||
export type DropdownItem = {
|
||||
text: string;
|
||||
value: any;
|
||||
@@ -7,15 +7,19 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { type Snippet } from "svelte";
|
||||
import { fly } from "svelte/transition";
|
||||
|
||||
export let items: DropdownItem[] = [];
|
||||
export let size: string = "regular";
|
||||
interface Props {
|
||||
items?: DropdownItem[];
|
||||
size?: string;
|
||||
children?: Snippet<[any]>;
|
||||
onchange?: (item: DropdownItem) => void;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let { items = [], size = "regular", children, onchange }: Props = $props();
|
||||
|
||||
let isOpen = false;
|
||||
let isOpen = $state(false);
|
||||
|
||||
export function toggleMenu(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
@@ -27,13 +31,18 @@
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function handleItemClick(item: { text: string; value: any }) {
|
||||
dispatch("change", item);
|
||||
function handleItemClick(e: Event, item: { text: string; value: any }) {
|
||||
e.stopPropagation();
|
||||
onchange?.(item);
|
||||
closeMenu();
|
||||
}
|
||||
|
||||
function handleWindowClick(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).parentElement?.classList.contains("dropdown-toggle")) {
|
||||
if (
|
||||
(e.target as HTMLElement).parentElement?.classList.contains(
|
||||
"dropdown-toggle",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,19 +50,20 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:mouseup={handleWindowClick} />
|
||||
<svelte:window onmouseup={handleWindowClick} />
|
||||
|
||||
<div class="dropdown relative">
|
||||
<div class="dropdown-toggle">
|
||||
<slot {toggleMenu}>
|
||||
{#if children}{@render children({ toggleMenu })}{:else}
|
||||
<button
|
||||
aria-label="Toggle menu"
|
||||
class="btn-icon flex items-center justify-center"
|
||||
on:click={toggleMenu}
|
||||
onclick={toggleMenu}
|
||||
type="button"
|
||||
>
|
||||
<i class="fa fa-ellipsis-vertical text-{size}"></i>
|
||||
</button>
|
||||
</slot>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isOpen}
|
||||
@@ -68,7 +78,7 @@
|
||||
<li
|
||||
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||
role="presentation"
|
||||
on:mousedown|stopPropagation={() => handleItemClick(item)}
|
||||
onmousedown={(e) => handleItemClick(e, item)}
|
||||
>
|
||||
{#if item.icon}
|
||||
<i class="fa fa-{item.icon} mr-3"></i>
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
<script lang="ts">
|
||||
export let label: string = "";
|
||||
export let name: string = "";
|
||||
export let accept: string = "*";
|
||||
import { createBubbler } from 'svelte/legacy';
|
||||
|
||||
export let files: FileList | null = null;
|
||||
const bubble = createBubbler();
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
name?: string;
|
||||
accept?: string;
|
||||
files?: FileList | null;
|
||||
}
|
||||
|
||||
let {
|
||||
label = "",
|
||||
name = "",
|
||||
accept = "*",
|
||||
files = $bindable(null)
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -18,6 +30,6 @@
|
||||
bind:files
|
||||
class="cursor-pointer bg-input-background border border-input-border rounded-md p-3 transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0 w-full file:rounded-lg file:px-4 file:py-2 file:border file:border-input-border-focus file:font-medium file:mr-4"
|
||||
type="file"
|
||||
on:change
|
||||
onchange={bubble('change')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
<script lang="ts">
|
||||
export let id: string;
|
||||
export let title: string;
|
||||
export let size: string = "max-w-2xl";
|
||||
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
title: string;
|
||||
size?: string;
|
||||
children?: Snippet<[any]>;
|
||||
content?: Snippet;
|
||||
footer?: Snippet<[any]>;
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
title,
|
||||
size = "max-w-2xl",
|
||||
children,
|
||||
content,
|
||||
footer,
|
||||
}: Props = $props();
|
||||
|
||||
export function openModal() {
|
||||
document.body.style.top = `-${window.scrollY}px`;
|
||||
document.body.style.position = "fixed";
|
||||
@@ -21,7 +37,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot {openModal} />
|
||||
{@render children?.({ openModal })}
|
||||
<dialog
|
||||
{id}
|
||||
tabindex="-1"
|
||||
@@ -40,7 +56,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full btn-icon"
|
||||
on:click={closeModal}
|
||||
onclick={closeModal}
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
<span class="sr-only">Close modal</span>
|
||||
@@ -48,11 +64,11 @@
|
||||
</div>
|
||||
<!-- Modal body -->
|
||||
<div class="p-4 md:p-5 space-y-4">
|
||||
<slot name="content" />
|
||||
{@render content?.()}
|
||||
</div>
|
||||
<!-- Modal footer -->
|
||||
<div class="p-4 md:p-5 border-t border-separator rounded-b">
|
||||
<slot name="footer" {closeModal} />
|
||||
{@render footer?.({ closeModal })}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -1,45 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import type { SelectItem } from "./select.svelte";
|
||||
|
||||
export let items: SelectItem[] = [];
|
||||
export let value: SelectItem[] = [];
|
||||
export let label: string = "";
|
||||
export let name: string = "";
|
||||
export let placeholder: string = "";
|
||||
|
||||
let showDropdown = false;
|
||||
let dropdownRef;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function toggleItem(item: SelectItem) {
|
||||
if (value.includes(item)) {
|
||||
value = value.filter((i) => i !== item);
|
||||
} else {
|
||||
value = [...value, item];
|
||||
}
|
||||
|
||||
dispatch("change", value);
|
||||
interface Props {
|
||||
items?: SelectItem[];
|
||||
value?: SelectItem[];
|
||||
label?: string;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
onchange?: (value: SelectItem[]) => void
|
||||
}
|
||||
|
||||
function removeItem(item: SelectItem) {
|
||||
let {
|
||||
items = [],
|
||||
value = $bindable([]),
|
||||
label = "",
|
||||
name = "",
|
||||
placeholder = "",
|
||||
onchange
|
||||
}: Props = $props();
|
||||
|
||||
let showDropdown = $state(false);
|
||||
let dropdownRef = $state();
|
||||
|
||||
function toggleItem(item: SelectItem) {
|
||||
const itemIndex = value.findIndex((i) => i.value == item.value);
|
||||
|
||||
if (itemIndex >= 0) {
|
||||
value.splice(itemIndex, 1);
|
||||
} else {
|
||||
value.push(item);
|
||||
}
|
||||
|
||||
onchange?.(value);
|
||||
}
|
||||
|
||||
function removeItem(e: Event, item: SelectItem) {
|
||||
e.stopPropagation();
|
||||
value = value.filter((i) => i !== item);
|
||||
dispatch("change", value);
|
||||
onchange?.(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="relative max-w-full">
|
||||
{#if label.length}
|
||||
<label for={name} class="text-sm font-medium pb-1 block">
|
||||
{label}
|
||||
</label>
|
||||
{/if}
|
||||
<button
|
||||
<div
|
||||
role="presentation"
|
||||
class="min-w-44 w-full flex flex-wrap items-center gap-2 border border-input-border bg-input-background min-h-[50px] p-3 rounded-md transition-colors focus:border-input-border-focus focus:outline-none focus:ring-0"
|
||||
on:click={() => (showDropdown = !showDropdown)}
|
||||
onclick={() => (showDropdown = !showDropdown)}
|
||||
>
|
||||
{#if value.length === 0}
|
||||
<span class="text-gray-400">{placeholder}</span>
|
||||
@@ -50,14 +63,15 @@
|
||||
>
|
||||
<span class="text-sm">{$_(item.text)}</span>
|
||||
<button
|
||||
on:click|stopPropagation={() => removeItem(item)}
|
||||
aria-label="Close"
|
||||
onclick={(e) => removeItem(e, item)}
|
||||
class="text-white hover:bg-primary-hover rounded-full w-4 h-4 flex items-center justify-center"
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
{#if showDropdown}
|
||||
@@ -67,11 +81,11 @@
|
||||
>
|
||||
{#each items as item}
|
||||
<button
|
||||
on:click={() => toggleItem(item)}
|
||||
onclick={() => toggleItem(item)}
|
||||
class="px-3 py-2 hover:bg-menu-item-background-hover cursor-pointer flex justify-between items-center w-full"
|
||||
>
|
||||
<span>{$_(item.text)}</span>
|
||||
{#if value.includes(item)}
|
||||
{#if value.findIndex((i) => i.value == item.value) >= 0}
|
||||
<div class="ml-auto">
|
||||
<i class="fa fa-check"></i>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { range } from "$lib/util/array_util";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let page: number;
|
||||
export let totalPages: number;
|
||||
interface Props {
|
||||
page: number;
|
||||
totalPages: number;
|
||||
onpagination?: (page: number) => void;
|
||||
}
|
||||
|
||||
let { page, totalPages, onpagination }: Props = $props();
|
||||
|
||||
const maxPagesToRender = 6;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function update(clickedPage: number) {
|
||||
if (page !== clickedPage) {
|
||||
dispatch("pagination", clickedPage);
|
||||
onpagination?.(clickedPage);
|
||||
}
|
||||
}
|
||||
|
||||
$: numberGenerator = () => {
|
||||
let numberGenerator = $derived(() => {
|
||||
if (totalPages < 4) {
|
||||
return [];
|
||||
}
|
||||
@@ -31,21 +34,22 @@
|
||||
return range(page + Math.max(1, maxPagesToRender - 5), page - 2);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex gap-x-4 justify-center">
|
||||
{#if totalPages > 1}
|
||||
<button
|
||||
aria-label="Next page"
|
||||
class:text-gray-500={page == 1}
|
||||
disabled={page == 1}
|
||||
on:click={() => update(page - 1)}
|
||||
onclick={() => update(page - 1)}
|
||||
><i class="fa fa-caret-left"></i></button
|
||||
>
|
||||
<button
|
||||
class="w-8 aspect-square rounded-lg transition-colors border-input-border hover:bg-menu-item-background-hover"
|
||||
class:border={page == 1}
|
||||
on:click={() => update(1)}>1</button
|
||||
onclick={() => update(1)}>1</button
|
||||
>
|
||||
{#if totalPages > maxPagesToRender && page >= maxPagesToRender - 2 && page > 2}
|
||||
<span>...</span>
|
||||
@@ -53,14 +57,14 @@
|
||||
<button
|
||||
class="w-8 aspect-square rounded-lg transition-colors border-input-border hover:bg-menu-item-background-hover"
|
||||
class:border={page == 2}
|
||||
on:click={() => update(2)}>2</button
|
||||
onclick={() => update(2)}>2</button
|
||||
>
|
||||
{/if}
|
||||
{#each numberGenerator() as i}
|
||||
<button
|
||||
class="w-8 aspect-square rounded-lg transition-colors border-input-border hover:bg-menu-item-background-hover"
|
||||
class:border={page == i + 1}
|
||||
on:click={() => update(i + 1)}>{i + 1}</button
|
||||
onclick={() => update(i + 1)}>{i + 1}</button
|
||||
>
|
||||
{/each}
|
||||
{#if totalPages > maxPagesToRender && page < totalPages - 3}
|
||||
@@ -69,18 +73,19 @@
|
||||
<button
|
||||
class="w-8 aspect-square rounded-lg transition-colors border-input-border hover:bg-menu-item-background-hover"
|
||||
class:border={page == totalPages - 1}
|
||||
on:click={() => update(totalPages - 1)}>{totalPages - 1}</button
|
||||
onclick={() => update(totalPages - 1)}>{totalPages - 1}</button
|
||||
>
|
||||
{/if}
|
||||
<button
|
||||
class="w-8 aspect-square rounded-lg transition-colors border-input-border hover:bg-menu-item-background-hover"
|
||||
class:border={page == totalPages}
|
||||
on:click={() => update(totalPages)}>{totalPages}</button
|
||||
onclick={() => update(totalPages)}>{totalPages}</button
|
||||
>
|
||||
<button
|
||||
aria-label="Previous page"
|
||||
class:text-gray-500={page == totalPages}
|
||||
disabled={page == totalPages}
|
||||
on:click={() => update(page + 1)}
|
||||
onclick={() => update(page + 1)}
|
||||
><i class="fa fa-caret-right"></i></button
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script context="module" lang="ts">
|
||||
<script module lang="ts">
|
||||
export type RadioItem = {
|
||||
text: string;
|
||||
value: string;
|
||||
@@ -8,17 +8,19 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
interface Props {
|
||||
items: RadioItem[];
|
||||
name: string;
|
||||
selected?: number;
|
||||
onchange?: (item: RadioItem) => void
|
||||
}
|
||||
|
||||
export let items: RadioItem[];
|
||||
export let name: string;
|
||||
export let selected: number = 0;
|
||||
let { items, name, selected = $bindable(0), onchange }: Props = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleRadioChange(radioIndex: number) {
|
||||
selected = radioIndex;
|
||||
dispatch("change", items[selected]);
|
||||
onchange?.(items[selected]);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -31,7 +33,7 @@
|
||||
checked={i == selected}
|
||||
value={item.value}
|
||||
class="w-4 h-4 accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
||||
on:change={() => handleRadioChange(i)}
|
||||
onchange={() => handleRadioChange(i)}
|
||||
/>
|
||||
<div class="ms-2 text-sm">
|
||||
<label for="{name}-radio-{i}"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script context="module" lang="ts">
|
||||
<!-- @migration-task Error while migrating Svelte code: This migration would change the name of a slot making the component unusable -->
|
||||
<script module lang="ts">
|
||||
export type SearchItem = {
|
||||
text: string;
|
||||
description?: string;
|
||||
@@ -8,26 +9,43 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import TextField from "./text_field.svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
export let maxSearchLength: number = 5;
|
||||
export let value: string = "";
|
||||
export let items: SearchItem[] = [];
|
||||
export let placeholder: string = "Search...";
|
||||
export let large: boolean = false;
|
||||
export let extraClasses: string = "";
|
||||
export let label: string = "";
|
||||
export let clearAfterSelect: boolean = true;
|
||||
interface Props {
|
||||
maxSearchLength?: number;
|
||||
value?: string;
|
||||
items?: SearchItem[];
|
||||
placeholder?: string;
|
||||
large?: boolean;
|
||||
extraClasses?: string;
|
||||
label?: string;
|
||||
clearAfterSelect?: boolean;
|
||||
prepend?: Snippet<[any]>;
|
||||
onupdate?: (q: string) => void;
|
||||
onclick?: (item: SearchItem) => void;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let {
|
||||
maxSearchLength = 5,
|
||||
value = $bindable(""),
|
||||
items = $bindable([]),
|
||||
placeholder = "Search...",
|
||||
large = false,
|
||||
extraClasses = "",
|
||||
label = "",
|
||||
clearAfterSelect = true,
|
||||
prepend,
|
||||
onupdate,
|
||||
onclick
|
||||
}: Props = $props();
|
||||
|
||||
let lastSearch: string = "";
|
||||
let searching: boolean = false;
|
||||
let searching: boolean = $state(false);
|
||||
let typingTimer!: any;
|
||||
|
||||
$: dropDownOpen = value.length > 0 && items.length > 0 && searching;
|
||||
let dropDownOpen = $derived(value.length > 0 && items.length > 0 && searching);
|
||||
|
||||
function onSearchType() {
|
||||
clearTimeout(typingTimer);
|
||||
@@ -42,12 +60,13 @@
|
||||
|
||||
function update(q: string) {
|
||||
lastSearch = q;
|
||||
dispatch("update", q);
|
||||
onupdate?.(q);
|
||||
}
|
||||
|
||||
function handleItemClick(item: SearchItem) {
|
||||
function handleItemClick(e: Event, item: SearchItem) {
|
||||
e.stopPropagation();
|
||||
searching = false;
|
||||
dispatch("click", item);
|
||||
onclick?.(item);
|
||||
if (clearAfterSelect) {
|
||||
clear();
|
||||
}
|
||||
@@ -70,11 +89,12 @@
|
||||
</span>
|
||||
{#if value.length > 0}
|
||||
<button
|
||||
aria-label="Clear"
|
||||
type="button"
|
||||
class="btn-icon absolute {large
|
||||
? 'bottom-[31px]'
|
||||
: 'bottom-[25px]'} translate-y-1/2 right-0 mr-2"
|
||||
on:click={clear}
|
||||
onclick={clear}
|
||||
in:fade={{ duration: 150 }}
|
||||
out:fade={{ duration: 150 }}
|
||||
>
|
||||
@@ -91,9 +111,9 @@
|
||||
: "px-10"}
|
||||
{placeholder}
|
||||
bind:value
|
||||
on:input={onSearchType}
|
||||
on:focusin={() => (searching = true)}
|
||||
on:focusout={() => (searching = false)}
|
||||
oninput={onSearchType}
|
||||
onfocusin={() => (searching = true)}
|
||||
onfocusout={() => (searching = false)}
|
||||
></TextField>
|
||||
|
||||
{#if dropDownOpen}
|
||||
@@ -103,17 +123,17 @@
|
||||
style="z-index: 1001"
|
||||
>
|
||||
{#each items as item}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<li
|
||||
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||
tabindex="0"
|
||||
on:mousedown|stopPropagation={() => handleItemClick(item)}
|
||||
on:keydown|stopPropagation={() => handleItemClick(item)}
|
||||
onmousedown={(e) => handleItemClick(e, item)}
|
||||
onkeydown={(e) => handleItemClick(e, item)}
|
||||
>
|
||||
<slot name="item-header" {item}>
|
||||
{#if prepend}{@render prepend({ item, })}{:else}
|
||||
<i class="fa fa-{item.icon} mr-6"></i>
|
||||
</slot>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<p>{item.text}</p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script context="module" lang="ts">
|
||||
<script module lang="ts">
|
||||
export type SelectItem = {
|
||||
text: string;
|
||||
value: any;
|
||||
@@ -6,18 +6,26 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
interface Props {
|
||||
name?: string;
|
||||
items?: SelectItem[];
|
||||
value?: any;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
onchange?: (value: any) => void
|
||||
}
|
||||
|
||||
export let name: string = "";
|
||||
export let items: SelectItem[] = [];
|
||||
export let value: any = items.at(0)?.value ?? "";
|
||||
export let label: string = "";
|
||||
export let disabled: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let {
|
||||
name = "",
|
||||
items = [],
|
||||
value = $bindable(items.at(0)?.value ?? ""),
|
||||
label = "",
|
||||
disabled = false,
|
||||
onchange
|
||||
}: Props = $props();
|
||||
|
||||
function onChange(target: any) {
|
||||
dispatch("change", target?.value);
|
||||
onchange?.(target?.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -33,7 +41,7 @@
|
||||
class:text-gray-500={disabled}
|
||||
{disabled}
|
||||
bind:value
|
||||
on:change={(e) => onChange(e.target)}
|
||||
onchange={(e) => onChange(e.target)}
|
||||
>
|
||||
{#each items as item}
|
||||
<option value={item.value}>{item.text}</option>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<script lang="ts">
|
||||
import noUiSlider from "nouislider";
|
||||
import "nouislider/dist/nouislider.css";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let minValue = 0;
|
||||
export let maxValue = 100;
|
||||
interface Props {
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
currentValue?: any;
|
||||
onset?: (value: number) => void;
|
||||
}
|
||||
|
||||
export let currentValue = maxValue / 2;
|
||||
let {
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
currentValue = $bindable(maxValue / 2),
|
||||
onset,
|
||||
}: Props = $props();
|
||||
|
||||
let sliderContainer: any;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let sliderContainer: any = $state();
|
||||
|
||||
onMount(() => {
|
||||
const updateValues = (values: string[]) => {
|
||||
@@ -29,7 +36,7 @@
|
||||
sliderContainer.noUiSlider.on("update", updateValues);
|
||||
|
||||
sliderContainer.noUiSlider.on("set", () => {
|
||||
dispatch("set", currentValue);
|
||||
onset?.(currentValue);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import { tweened } from "svelte/motion";
|
||||
import { Tween } from "svelte/motion";
|
||||
|
||||
export let tabs: string[];
|
||||
export let activeTab: number;
|
||||
export let extraClasses: string = "";
|
||||
interface Props {
|
||||
tabs: string[];
|
||||
activeTab: number;
|
||||
extraClasses?: string;
|
||||
}
|
||||
|
||||
const indicatorPosition = tweened(0, {
|
||||
let { tabs, activeTab = $bindable(), extraClasses = "" }: Props = $props();
|
||||
|
||||
const indicatorPosition = new Tween(0, {
|
||||
duration: 300,
|
||||
easing: cubicOut,
|
||||
});
|
||||
|
||||
const indicatorWidth = tweened(0, {
|
||||
const indicatorWidth = new Tween(0, {
|
||||
duration: 300,
|
||||
easing: cubicOut,
|
||||
});
|
||||
@@ -37,13 +41,13 @@
|
||||
<div id="tabs" class="flex gap-2 overflow-x-auto relative {extraClasses}">
|
||||
<div
|
||||
class="absolute h-full bg-menu-item-background-hover rounded-t-lg top-0 z-0"
|
||||
style="width: {$indicatorWidth}px; left: {$indicatorPosition}px;"
|
||||
style="width: {indicatorWidth.current}px; left: {indicatorPosition.current}px;"
|
||||
></div>
|
||||
{#each tabs as tab, i}
|
||||
<button
|
||||
class="tab z-10"
|
||||
class:tab-active={activeTab == i}
|
||||
on:click={() => switchTabs(i)}>{tab}</button
|
||||
onclick={() => switchTabs(i)}>{tab}</button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import type { ChangeEventHandler, FocusEventHandler, FormEventHandler } from "svelte/elements";
|
||||
|
||||
export let name: string = "";
|
||||
export let value: string | number = "";
|
||||
export let placeholder: string = "";
|
||||
export let disabled: boolean = false;
|
||||
export let label: string = "";
|
||||
export let error: string | string[] | null = "";
|
||||
export let icon: string = "";
|
||||
export let extraClasses: string = "";
|
||||
export let type: "text" | "password" | "search" = "text";
|
||||
export let autocomplete: "on" | "off" = "on";
|
||||
interface Props {
|
||||
name?: string;
|
||||
value?: string | number;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
error?: string | string[] | null;
|
||||
icon?: string;
|
||||
extraClasses?: string;
|
||||
type?: "text" | "password" | "search";
|
||||
autocomplete?: "on" | "off";
|
||||
onchange?: ChangeEventHandler<HTMLInputElement>;
|
||||
oninput?: FormEventHandler<HTMLInputElement>;
|
||||
onfocusin?: FocusEventHandler<HTMLInputElement>;
|
||||
onfocusout?: FocusEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
let {
|
||||
name = "",
|
||||
value = $bindable(""),
|
||||
placeholder = "",
|
||||
disabled = false,
|
||||
label = "",
|
||||
error = "",
|
||||
icon = "",
|
||||
extraClasses = "",
|
||||
type = "text",
|
||||
autocomplete = "on",
|
||||
onchange,
|
||||
oninput,
|
||||
onfocusin,
|
||||
onfocusout
|
||||
}: Props = $props();
|
||||
function typeAction(node: HTMLInputElement) {
|
||||
node.type = type;
|
||||
}
|
||||
@@ -38,10 +62,10 @@
|
||||
{autocomplete}
|
||||
use:typeAction
|
||||
bind:value
|
||||
on:change
|
||||
on:input
|
||||
on:focusin
|
||||
on:focusout
|
||||
{onchange}
|
||||
{oninput}
|
||||
{onfocusin}
|
||||
{onfocusout}
|
||||
{placeholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { createBubbler } from 'svelte/legacy';
|
||||
|
||||
const bubble = createBubbler();
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let name: string = "";
|
||||
export let value: string | number = "";
|
||||
export let placeholder: string = "";
|
||||
export let rows: number = 3;
|
||||
export let label: string = "";
|
||||
export let error: string | string[] | null = "";
|
||||
export let extraClasses: string = "";
|
||||
interface Props {
|
||||
name?: string;
|
||||
value?: string | number;
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
label?: string;
|
||||
error?: string | string[] | null;
|
||||
extraClasses?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
name = "",
|
||||
value = $bindable(""),
|
||||
placeholder = "",
|
||||
rows = 3,
|
||||
label = "",
|
||||
error = "",
|
||||
extraClasses = ""
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -24,8 +39,8 @@
|
||||
class:border-red-400={(error?.length ?? 0) > 0}
|
||||
class:bg-input-background-error={(error?.length ?? 0) > 0}
|
||||
bind:value
|
||||
on:change
|
||||
/>
|
||||
onchange={bubble('change')}
|
||||
></textarea>
|
||||
{#if error}
|
||||
<span class="textfield-error text-xs text-red-400">
|
||||
{error instanceof Array ? $_(error[0]) : error}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white ml-6"
|
||||
on:click={closeToast}
|
||||
onclick={closeToast}
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
<span class="sr-only">Close modal</span>
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let name: string = "";
|
||||
export let value: boolean = false;
|
||||
export let label: string = "";
|
||||
export let error: string = "";
|
||||
interface Props {
|
||||
name?: string;
|
||||
value?: boolean;
|
||||
label?: string;
|
||||
error?: string;
|
||||
onchange?: (value: boolean) => void
|
||||
}
|
||||
|
||||
let {
|
||||
name = "",
|
||||
value = $bindable(false),
|
||||
label = "",
|
||||
error = "",
|
||||
onchange
|
||||
}: Props = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleToggleChange() {
|
||||
dispatch("change", value);
|
||||
onchange?.(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,7 +29,7 @@
|
||||
type="checkbox"
|
||||
class="sr-only peer"
|
||||
value="1"
|
||||
on:change={handleToggleChange}
|
||||
onchange={handleToggleChange}
|
||||
/>
|
||||
<div
|
||||
class="w-11 h-6 bg-input-background border border-input-border peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-input-ring rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"
|
||||
|
||||
Reference in New Issue
Block a user