Initial SvelteKit frontend port of windwiderstand.de
Full parity with Astro site: content rows, post/tag routes, pagination, event badges + OSM map, comments, Live-Search via /api/search-index, CMS image proxy, RSS, sitemap. Deploy: Dockerfile + docker-compose.prod.yml + Gitea Actions workflow to build + push to git.pm86.de registry and ssh-deploy to Contabo.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
type Variant = 'default' | 'primary' | 'accent' | 'inactive' | 'date';
|
||||
|
||||
let {
|
||||
label = '',
|
||||
variant = 'default',
|
||||
href = null as string | null,
|
||||
active = false,
|
||||
onclick = null as (() => void) | null,
|
||||
} = $props();
|
||||
|
||||
const baseClass =
|
||||
'inline-flex shrink-0 whitespace-nowrap rounded-full py-1 px-2.5 text-[0.6875rem] font-medium transition-all ring-1 no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1';
|
||||
|
||||
const variantClasses = $derived(
|
||||
{
|
||||
default: 'bg-stein-0 text-stein-800 ring-stein-200 hover:bg-stein-50',
|
||||
primary: 'bg-wald-500 text-white ring-wald-600/30 hover:bg-wald-600',
|
||||
accent: 'bg-himmel-400 text-white ring-himmel-500/30 hover:bg-himmel-500',
|
||||
inactive: 'bg-stein-100 text-stein-500 ring-stein-200',
|
||||
date: 'bg-wald-600 text-white ring-wald-700/40',
|
||||
}[variant]
|
||||
);
|
||||
|
||||
const shadowClass = $derived(
|
||||
href || onclick
|
||||
? variant === 'date'
|
||||
? 'shadow-[0_0_6px_rgba(0,0,0,0.25)]'
|
||||
: 'shadow-[0_2px_6px_rgba(0,0,0,0.12)]'
|
||||
: ''
|
||||
);
|
||||
|
||||
const classes = $derived(
|
||||
`${baseClass} ${variantClasses} ${shadowClass} ${active ? 'pointer-events-none ring-2' : ''}`
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
href={href}
|
||||
class={classes}
|
||||
aria-current={active ? 'true' : undefined}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
{:else if onclick}
|
||||
<button
|
||||
type="button"
|
||||
class={classes}
|
||||
aria-current={active ? 'true' : undefined}
|
||||
onclick={onclick}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
{:else}
|
||||
<span class={classes}>{label}</span>
|
||||
{/if}
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import "$lib/iconify-offline";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
/** @type {{ href?: string; label: string }[]} */
|
||||
let { items = [] } = $props();
|
||||
</script>
|
||||
|
||||
<nav aria-label="Breadcrumb" class="overflow-x-auto">
|
||||
<ol class="list-none flex flex-nowrap items-center gap-x-1 text-xs py-4 pl-0 m-0!">
|
||||
{#each items as item, i}
|
||||
<li class="flex items-center gap-x-1.5 shrink-0 whitespace-nowrap">
|
||||
{#if item.href && i < items.length - 1}
|
||||
<a
|
||||
href={item.href}
|
||||
class="inline-flex font-medium no-underline items-center gap-1.5 text-stein-500 hover:text-wald-600 transition-colors duration-150 rounded px-1 -mx-1 py-0.5 -my-0.5"
|
||||
aria-label={i === 0 ? item.label : undefined}
|
||||
>
|
||||
{#if i === 0}
|
||||
<Icon icon="mdi:home" class="size-4 shrink-0" aria-hidden="true" />
|
||||
{:else}
|
||||
<span>{item.label}</span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<span
|
||||
class="font-medium text-stein-800"
|
||||
aria-current={i === items.length - 1 ? 'page' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
{/if}
|
||||
{#if i < items.length - 1}
|
||||
<span aria-hidden="true" class="shrink-0 text-stein-300">
|
||||
<Icon icon="mdi:chevron-right" class="size-3.5" />
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</nav>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
type Variant = 'primary' | 'secondary' | 'ghost';
|
||||
type Size = 'sm' | 'md' | 'large';
|
||||
|
||||
let {
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
type = 'button' as 'button' | 'submit' | 'reset',
|
||||
label = '',
|
||||
} = $props();
|
||||
|
||||
const base =
|
||||
'inline-flex items-center justify-center gap-2 rounded-lg font-semibold transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wald-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-40';
|
||||
|
||||
const sizeClasses = $derived(
|
||||
{ sm: 'h-9 px-4 py-2 text-sm', md: 'h-12 min-w-[120px] px-6 py-3 text-base', large: 'h-14 px-8 py-4 text-lg' }[
|
||||
size
|
||||
]
|
||||
);
|
||||
|
||||
const variantClasses = $derived(
|
||||
{
|
||||
primary: 'bg-wald-500 text-white hover:bg-wald-600 active:bg-wald-700',
|
||||
secondary:
|
||||
'border-[1.5px] border-wald-500 bg-transparent text-wald-500 hover:bg-wald-50 active:bg-wald-100',
|
||||
ghost: 'bg-transparent px-4 py-3 text-stein-700 hover:bg-stein-100 active:bg-stein-200',
|
||||
}[variant]
|
||||
);
|
||||
|
||||
const allClasses = $derived([base, sizeClasses, variantClasses].filter(Boolean).join(' '));
|
||||
</script>
|
||||
|
||||
<button
|
||||
{type}
|
||||
{disabled}
|
||||
class={allClasses}
|
||||
>
|
||||
{#if loading}
|
||||
<span class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></span>
|
||||
<span>Laden...</span>
|
||||
{:else}
|
||||
<slot>{label}</slot>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
checked = $bindable(false),
|
||||
disabled = false,
|
||||
label = '',
|
||||
name = '',
|
||||
value = '',
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center {disabled ? 'cursor-not-allowed opacity-60' : ''}">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked
|
||||
{disabled}
|
||||
{name}
|
||||
{value}
|
||||
class="h-5 w-5 rounded border-[1.5px] border-stein-400 text-wald-500 hover:border-wald-400 focus:ring-2 focus:ring-wald-100 focus:ring-offset-0 checked:border-wald-500 checked:bg-wald-500"
|
||||
/>
|
||||
{#if label}
|
||||
<span class="ml-2 text-base text-stein-700">{label}</span>
|
||||
{/if}
|
||||
</label>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
/** Gleicher name für alle Radios in einer Gruppe; bind:group vom Parent hält den gewählten value. */
|
||||
let {
|
||||
name = '',
|
||||
value = '',
|
||||
/** Gebundener Wert der Gruppe – entspricht dem value des ausgewählten Radios. */
|
||||
group = $bindable(''),
|
||||
disabled = false,
|
||||
label = '',
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center {disabled ? 'cursor-not-allowed opacity-60' : ''}">
|
||||
<input
|
||||
type="radio"
|
||||
{name}
|
||||
{value}
|
||||
bind:group={group}
|
||||
{disabled}
|
||||
class="h-5 w-5 rounded-full border-[1.5px] border-stein-400 text-wald-500 hover:border-wald-400 focus:ring-2 focus:ring-wald-100 focus:ring-offset-0 checked:border-wald-500 checked:bg-wald-500"
|
||||
/>
|
||||
{#if label}
|
||||
<span class="ml-2 text-base text-stein-700">{label}</span>
|
||||
{/if}
|
||||
</label>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
/** @type {{ value: string; label: string }[]} */
|
||||
let {
|
||||
options = [],
|
||||
value = $bindable(''),
|
||||
label = '',
|
||||
placeholder = 'Bitte wählen',
|
||||
disabled = false,
|
||||
} = $props();
|
||||
|
||||
const selectId = $derived(`select-${Math.random().toString(36).slice(2)}`);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<label
|
||||
for={selectId}
|
||||
class="mb-1.5 block text-sm font-medium text-stein-700"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
{/if}
|
||||
<div class="relative">
|
||||
<select
|
||||
id={selectId}
|
||||
bind:value
|
||||
{disabled}
|
||||
class="h-12 w-full appearance-none rounded-lg border border-stein-300 bg-white px-4 py-3 pr-10 text-base text-stein-800 outline-none hover:border-stein-400 focus:border-wald-500 focus:ring-2 focus:ring-wald-100 disabled:bg-stein-100 disabled:opacity-60"
|
||||
>
|
||||
<option value="" disabled>{placeholder}</option>
|
||||
{#each options as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span
|
||||
class="pointer-events-none absolute right-3 top-1/2 h-5 w-5 -translate-y-1/2 text-stein-500"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* @type {{ label: string; links: { href: string; label: string; active?: boolean }[] }[]}
|
||||
*/
|
||||
let { groups = [] } = $props();
|
||||
</script>
|
||||
|
||||
<nav aria-label="Seitennavigation" class="w-60 bg-stein-50 p-4">
|
||||
{#each groups as group, i}
|
||||
<div class="{i > 0 ? 'mt-6' : ''}">
|
||||
<p
|
||||
class="mb-2 text-xs font-medium uppercase tracking-wider text-stein-500"
|
||||
>
|
||||
{group.label}
|
||||
</p>
|
||||
<ul class="space-y-0">
|
||||
{#each group.links as link}
|
||||
<li>
|
||||
<a
|
||||
href={link.href}
|
||||
class="block rounded-lg px-3 py-2 text-sm {link.active
|
||||
? 'bg-wald-50 font-medium text-wald-500'
|
||||
: 'text-stein-700 hover:bg-wald-50'}"
|
||||
aria-current={link.active ? 'page' : undefined}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
min = 0,
|
||||
max = 100,
|
||||
value = $bindable(50),
|
||||
label = '',
|
||||
disabled = false,
|
||||
} = $props();
|
||||
|
||||
const inputId = $derived(`slider-${Math.random().toString(36).slice(2)}`);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<div class="mb-1.5 flex items-center justify-between">
|
||||
<label for={inputId} class="text-sm font-medium text-stein-700">
|
||||
{label}
|
||||
</label>
|
||||
<span class="text-sm font-semibold text-stein-700">{value}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<input
|
||||
type="range"
|
||||
id={inputId}
|
||||
{min}
|
||||
{max}
|
||||
bind:value
|
||||
{disabled}
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
aria-valuenow={value}
|
||||
aria-valuetext={String(value)}
|
||||
class="h-5 w-5 accent-wald-500 [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-wald-500 [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-runnable-track]:h-1 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-stein-200"
|
||||
/>
|
||||
<div class="mt-1 flex justify-between text-xs text-stein-500">
|
||||
<span>{min}</span>
|
||||
<span>{max}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
/** @type {{ id: string; label: string }[]} */
|
||||
let {
|
||||
tabs = [],
|
||||
selectedId = $bindable(''),
|
||||
labelledBy = '',
|
||||
} = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (tabs.length && !selectedId) selectedId = tabs[0].id;
|
||||
});
|
||||
|
||||
function selectTab(id: string) {
|
||||
selectedId = id;
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const idx = tabs.findIndex((t) => t.id === selectedId);
|
||||
if (e.key === 'ArrowRight' && idx < tabs.length - 1) {
|
||||
e.preventDefault();
|
||||
selectedId = tabs[idx + 1].id;
|
||||
} else if (e.key === 'ArrowLeft' && idx > 0) {
|
||||
e.preventDefault();
|
||||
selectedId = tabs[idx - 1].id;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={labelledBy || 'Tabs'}
|
||||
class="flex h-12 border-b border-stein-200"
|
||||
onkeydown={handleKeydown}
|
||||
>
|
||||
{#each tabs as tab}
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={selectedId === tab.id}
|
||||
aria-controls="panel-{tab.id}"
|
||||
id="tab-{tab.id}"
|
||||
class="border-b-2 px-4 text-sm font-medium transition-colors {selectedId === tab.id
|
||||
? 'border-wald-500 text-wald-600 font-semibold'
|
||||
: 'border-transparent text-stein-500 hover:text-stein-700'}"
|
||||
tabindex={selectedId === tab.id ? 0 : -1}
|
||||
onclick={() => selectTab(tab.id)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
id = '',
|
||||
name = '',
|
||||
type = 'text',
|
||||
value = $bindable(''),
|
||||
placeholder = '',
|
||||
label = '',
|
||||
required = false,
|
||||
disabled = false,
|
||||
error = '',
|
||||
helpText = '',
|
||||
invalid = false,
|
||||
} = $props();
|
||||
|
||||
const inputId = $derived(id || `input-${Math.random().toString(36).slice(2)}`);
|
||||
const errorId = $derived(`${inputId}-error`);
|
||||
const helpId = $derived(`${inputId}-help`);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<label
|
||||
for={inputId}
|
||||
class="mb-1.5 block text-sm font-medium text-stein-700"
|
||||
>
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-error">*</span>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
<input
|
||||
{type}
|
||||
{name}
|
||||
{placeholder}
|
||||
{required}
|
||||
{disabled}
|
||||
bind:value
|
||||
id={inputId}
|
||||
aria-required={required}
|
||||
aria-invalid={invalid || !!error}
|
||||
aria-describedby={[error ? errorId : '', helpText ? helpId : ''].filter(Boolean).join(' ')}
|
||||
class="h-12 w-full rounded-lg border bg-white px-4 py-3 text-base text-stein-800 outline-none placeholder:text-stein-400 {error || invalid
|
||||
? 'border-error ring-2 ring-error-subtle'
|
||||
: 'border-stein-300 hover:border-stein-400 focus:border-wald-500 focus:ring-2 focus:ring-wald-100'} disabled:bg-stein-100 disabled:opacity-60"
|
||||
/>
|
||||
{#if error}
|
||||
<p id={errorId} class="mt-1.5 text-[0.8125rem] text-error" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
{:else if helpText}
|
||||
<p id={helpId} class="mt-1.5 text-[0.8125rem] text-stein-500">
|
||||
{helpText}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
id = '',
|
||||
name = '',
|
||||
value = $bindable(''),
|
||||
placeholder = '',
|
||||
label = '',
|
||||
required = false,
|
||||
disabled = false,
|
||||
error = '',
|
||||
helpText = '',
|
||||
invalid = false,
|
||||
rows = 4,
|
||||
} = $props();
|
||||
|
||||
const inputId = $derived(id || `textarea-${Math.random().toString(36).slice(2)}`);
|
||||
const errorId = $derived(`${inputId}-error`);
|
||||
const helpId = $derived(`${inputId}-help`);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<label
|
||||
for={inputId}
|
||||
class="mb-1.5 block text-sm font-medium text-stein-700"
|
||||
>
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-error">*</span>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
<textarea
|
||||
{name}
|
||||
{placeholder}
|
||||
{required}
|
||||
{disabled}
|
||||
{rows}
|
||||
bind:value
|
||||
id={inputId}
|
||||
aria-required={required}
|
||||
aria-invalid={invalid || !!error}
|
||||
aria-describedby={[error ? errorId : '', helpText ? helpId : ''].filter(Boolean).join(' ')}
|
||||
class="min-h-[120px] w-full resize-y rounded-lg border bg-white px-4 py-3 text-base text-stein-800 outline-none placeholder:text-stein-400 {error || invalid
|
||||
? 'border-error ring-2 ring-error-subtle'
|
||||
: 'border-stein-300 hover:border-stein-400 focus:border-wald-500 focus:ring-2 focus:ring-wald-100'} disabled:bg-stein-100 disabled:opacity-60"
|
||||
/>
|
||||
{#if error}
|
||||
<p id={errorId} class="mt-1.5 text-[0.8125rem] text-error" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
{:else if helpText}
|
||||
<p id={helpId} class="mt-1.5 text-[0.8125rem] text-stein-500">
|
||||
{helpText}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
checked = $bindable(false),
|
||||
disabled = false,
|
||||
label = '',
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center gap-2 {disabled ? 'cursor-not-allowed opacity-60' : ''}">
|
||||
<span
|
||||
class="relative inline-block h-6 w-11 shrink-0 rounded-full transition-all duration-200 {checked ? 'bg-wald-500' : 'bg-stein-300'}"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
bind:checked
|
||||
{disabled}
|
||||
onkeydown={(e) => e.key === ' ' && (checked = !checked)}
|
||||
/>
|
||||
<span
|
||||
class="absolute top-0.5 inline-block h-5 w-5 rounded-full bg-white shadow-sm transition-transform duration-200 {checked ? 'left-[22px] translate-x-0.5' : 'left-0.5'}"
|
||||
/>
|
||||
</span>
|
||||
{#if label}
|
||||
<span class="text-base text-stein-700">{label}</span>
|
||||
{/if}
|
||||
</label>
|
||||
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
content,
|
||||
placement: preferredPlacement = "top",
|
||||
children,
|
||||
}: { content: string; placement?: "top" | "bottom"; children?: Snippet } = $props();
|
||||
|
||||
const TOOLTIP_GAP = 8;
|
||||
const TOOLTIP_MAX_WIDTH_PX = 352; // 22rem
|
||||
|
||||
let wrapperEl = $state<HTMLDivElement | null>(null);
|
||||
let showTooltip = $state(false);
|
||||
let placement = $state<"top" | "bottom">("top");
|
||||
let leftPx = $state<number | null>(null); // null = centered (CSS), number = clamped px from wrapper left
|
||||
|
||||
function updatePosition() {
|
||||
if (!wrapperEl) return;
|
||||
const rect = wrapperEl.getBoundingClientRect();
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
|
||||
const spaceAbove = rect.top;
|
||||
const spaceBelow = vh - rect.bottom;
|
||||
placement =
|
||||
preferredPlacement === "top"
|
||||
? spaceAbove >= spaceBelow
|
||||
? "top"
|
||||
: "bottom"
|
||||
: spaceBelow >= spaceAbove
|
||||
? "bottom"
|
||||
: "top";
|
||||
|
||||
const iconCenterX = rect.left + rect.width / 2;
|
||||
const idealLeft = iconCenterX - TOOLTIP_MAX_WIDTH_PX / 2;
|
||||
const clampedLeft = Math.max(
|
||||
TOOLTIP_GAP,
|
||||
Math.min(idealLeft, vw - TOOLTIP_MAX_WIDTH_PX - TOOLTIP_GAP),
|
||||
);
|
||||
leftPx = clampedLeft - rect.left;
|
||||
}
|
||||
|
||||
function handleShow() {
|
||||
updatePosition();
|
||||
showTooltip = true;
|
||||
}
|
||||
|
||||
function handleHide() {
|
||||
showTooltip = false;
|
||||
leftPx = null;
|
||||
}
|
||||
|
||||
const placementClasses = $derived(
|
||||
placement === "top"
|
||||
? "bottom-full mb-2"
|
||||
: "top-full mt-2",
|
||||
);
|
||||
const horizontalStyle = $derived(
|
||||
leftPx != null ? `left: ${leftPx}px; transform: none;` : "left: 50%; transform: translateX(-50%);",
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={wrapperEl}
|
||||
role="group"
|
||||
class="relative inline-flex focus-within:outline-none"
|
||||
onmouseenter={handleShow}
|
||||
onmouseleave={handleHide}
|
||||
onfocusin={handleShow}
|
||||
onfocusout={handleHide}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showTooltip}
|
||||
<div
|
||||
role="tooltip"
|
||||
class="absolute z-50 {placementClasses} px-3 py-2 text-xs font-normal text-stein-0 bg-stein-800 rounded-md shadow-lg min-w-56 max-w-88 text-center pointer-events-none whitespace-normal"
|
||||
style={horizontalStyle}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Windwiderstand UI components
|
||||
*/
|
||||
export { default as TabBar } from './TabBar.svelte';
|
||||
export { default as SidebarNav } from './SidebarNav.svelte';
|
||||
export { default as Breadcrumbs } from './Breadcrumbs.svelte';
|
||||
export { default as Button } from './Button.svelte';
|
||||
export { default as TextInput } from './TextInput.svelte';
|
||||
export { default as Textarea } from './Textarea.svelte';
|
||||
export { default as Select } from './Select.svelte';
|
||||
export { default as Toggle } from './Toggle.svelte';
|
||||
export { default as Checkbox } from './Checkbox.svelte';
|
||||
export { default as Radio } from './Radio.svelte';
|
||||
export { default as Slider } from './Slider.svelte';
|
||||
export { default as Badge } from './Badge.svelte';
|
||||
export { default as Tooltip } from './Tooltip.svelte';
|
||||
Reference in New Issue
Block a user