Lib: Add non-lazy variants for card, image and dropdown

This commit is contained in:
2024-12-23 01:16:48 +01:00
parent a1fd50a3f6
commit a23f12b69f
5 changed files with 210 additions and 0 deletions

View File

@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLImgAttributes } from "svelte/elements";
import { fetch_image_base64 } from "$lib/image";
interface ImageProps extends HTMLImgAttributes {
/** The URL to the image resource to load */
src: string;
}
let { src, ...restProps }: ImageProps = $props();
</script>
{#await fetch_image_base64(src) then data}
<img src={data} class="bg-surface-100 transition-opacity" {...restProps} />
{/await}

View File

@ -0,0 +1,47 @@
<script lang="ts">
import type { Snippet } from "svelte";
import { Image } from "$lib/components";
interface CardProps {
children: Snippet;
/** The URL for a possible header image. Leave undefined for no header image. Set to empty string for an image not yet loaded. */
imgsrc?: string | undefined;
/** The id of the header image element for JS access. */
imgid?: string | undefined;
/** Hide the header image element. It can be shown by removing the "hidden" property using JS and the imgid. */
imghidden?: boolean;
/** The width class for the card, defaults to [w-auto] */
width?: string;
}
let {
children,
imgsrc = undefined,
imgid = undefined,
imghidden = false,
width = "w-auto",
...restProps
}: CardProps = $props();
</script>
<div class="card {width} overflow-hidden bg-white shadow">
<!-- Allow empty strings for images that only appear after user action -->
{#if imgsrc !== undefined}
<Image
id={imgid}
src={imgsrc}
alt="Card header"
draggable="false"
class="select-none shadow"
hidden={imghidden}
/>
{/if}
<div class="p-2" {...restProps}>
{@render children()}
</div>
</div>

View File

@ -0,0 +1,130 @@
<script lang="ts">
import { ListBox, ListBoxItem, popup, type PopupSettings } from "@skeletonlabs/skeleton";
import type { Snippet } from "svelte";
import type { Action } from "svelte/action";
import type { HTMLInputAttributes } from "svelte/elements";
import { v4 as uuid } from "uuid";
import { type DropdownOption, Image } from "$lib/components";
interface DropdownProps extends HTMLInputAttributes {
children: Snippet;
/** Placeholder for the empty input element */
placeholder?: string;
/** Form name of the input element, to reference input data after form submission */
name?: string;
/** Manually set the label width, to align multiple inputs vertically. Supply value in CSS units. */
labelwidth?: string;
/** The variable to bind to the input element. Has to be a [$state] so its value can be updated with the input element's contents. */
input_variable: string;
/** Any action to bind to the input element */
action?: Action;
/** The ID of the popup to trigger. UUID by default. */
popup_id?: string;
/** The [PopupSettings] object for the popup to trigger. */
popup_settings?: PopupSettings;
/** The options this autocomplete component allows to choose from.
* Example: [[{ label: "Aston", value: "0" }, { label: "VCARB", value: "1" }]].
*/
options: DropdownOption[];
}
let {
children,
placeholder = "",
name = "",
labelwidth = "auto",
input_variable,
action = undefined,
popup_id = uuid(),
popup_settings = {
event: "click",
target: popup_id,
placement: "bottom",
closeQuery: ".listbox-item",
},
options,
...restProps
}: DropdownProps = $props();
/** Find the "label" of an option by its "value" */
const get_label = (value: string): string | undefined => {
return options.find((o) => o.value === value)?.label;
};
// Use an action to fill the "input" variable
// required to dispatch the custom event using $effect
let input: HTMLInputElement | undefined = undefined;
const obtain_input: Action = (node: HTMLElement) => {
input = node as HTMLInputElement;
};
// This will run everyting "input_variable" changes.
// The event is fired when the input's value is updated via JavaScript.
$effect(() => {
// Just list this so SvelteKit picks it up as dependency
input_variable;
if (input) input.dispatchEvent(new CustomEvent("DropdownChange"));
});
</script>
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
<div
class="input-group-shim select-none text-nowrap text-neutral-900"
style="width: {labelwidth};"
>
{@render children()}
</div>
<!-- TODO: How to assign use: conditionally? I don't wan't to repeat the entire input... -->
{#if action}
<input
use:popup={popup_settings}
type="button"
autocomplete="off"
style="height: 42px; text-align: start; text-indent: 12px; border-top-left-radius: 0; border-bottom-left-radius: 0;"
use:obtain_input
use:action
onkeypress={(event: Event) => event.preventDefault()}
value={get_label(input_variable) ?? placeholder}
{...restProps}
/>
{:else}
<input
use:popup={popup_settings}
type="button"
autocomplete="off"
style="height: 42px; text-align: start; text-indent: 12px; border-top-left-radius: 0; border-bottom-left-radius: 0;"
use:obtain_input
onkeypress={(event: Event) => event.preventDefault()}
value={get_label(input_variable) ?? placeholder}
{...restProps}
/>
{/if}
</div>
<div
data-popup={popup_id}
class="card z-10 w-auto overflow-y-scroll p-2 shadow"
style="max-height: 350px;"
>
<ListBox>
{#each options as option}
<ListBoxItem bind:group={input_variable} {name} value={option.value}>
<div class="flex flex-nowrap">
{#if option.icon_url}
<Image src={option.icon_url} alt="" class="rounded" style="height: 24px;" />
{/if}
<span class="ml-2">{option.label}</span>
</div>
</ListBoxItem>
{/each}
</ListBox>
</div>

View File

@ -0,0 +1,10 @@
export interface DropdownOption {
/** The label displayed in the list of options. */
label: string;
/** The value assigned to the dropdown value variable */
value: string;
/** An optional icon displayed left to the label */
icon_url?: string;
}

View File

@ -1,18 +1,22 @@
import Image from "./Image.svelte";
import LazyImage from "./LazyImage.svelte"; import LazyImage from "./LazyImage.svelte";
import LoadingIndicator from "./LoadingIndicator.svelte"; import LoadingIndicator from "./LoadingIndicator.svelte";
import Table from "./Table.svelte"; import Table from "./Table.svelte";
import Button from "./form/Button.svelte"; import Button from "./form/Button.svelte";
import Dropdown from "./form/Dropdown.svelte";
import Input from "./form/Input.svelte"; import Input from "./form/Input.svelte";
import LazyDropdown from "./form/LazyDropdown.svelte"; import LazyDropdown from "./form/LazyDropdown.svelte";
import Search from "./form/Search.svelte"; import Search from "./form/Search.svelte";
import Card from "./cards/Card.svelte";
import DriverCard from "./cards/DriverCard.svelte"; import DriverCard from "./cards/DriverCard.svelte";
import LazyCard from "./cards/LazyCard.svelte"; import LazyCard from "./cards/LazyCard.svelte";
import RaceCard from "./cards/RaceCard.svelte"; import RaceCard from "./cards/RaceCard.svelte";
import SubstitutionCard from "./cards/SubstitutionCard.svelte"; import SubstitutionCard from "./cards/SubstitutionCard.svelte";
import TeamCard from "./cards/TeamCard.svelte"; import TeamCard from "./cards/TeamCard.svelte";
import type { DropdownOption } from "./form/Dropdown";
import type { LazyDropdownOption } from "./form/LazyDropdown"; import type { LazyDropdownOption } from "./form/LazyDropdown";
import type { TableColumn } from "./Table"; import type { TableColumn } from "./Table";
@ -22,17 +26,20 @@ import UserIcon from "./svg/UserIcon.svelte";
export { export {
// Components // Components
Image,
LazyImage, LazyImage,
LoadingIndicator, LoadingIndicator,
Table, Table,
// Form // Form
Button, Button,
Dropdown,
Input, Input,
LazyDropdown, LazyDropdown,
Search, Search,
// Cards // Cards
Card,
DriverCard, DriverCard,
LazyCard, LazyCard,
RaceCard, RaceCard,
@ -40,6 +47,7 @@ export {
TeamCard, TeamCard,
// Types // Types
type DropdownOption,
type LazyDropdownOption, type LazyDropdownOption,
type TableColumn, type TableColumn,