refactor: Tooltips ueber tippy statt title-Attribute, appweit
title war fuer Hinweise die falsche Wahl: Es erscheint erst nach einer
Sekunde, sieht auf jedem System anders aus und laesst sich auf Touch-Geraeten
gar nicht aufrufen. Tippy war laengst als Abhaengigkeit da, aber kaum
benutzt.
<Button> nimmt jetzt eine tooltip-Prop, die zugleich als aria-label dient -
ohne das haetten die vielen Icon-Knoepfe beim Wegfall von title ihren Namen
fuer Screenreader verloren. Alle anderen Elemente nutzen die Action direkt.
Die Action selbst konnte bisher nur erzeugen. Sie beherrscht jetzt
Aktualisieren und Aufraeumen: Ein wechselnder Text ("Erledigt" / "Wieder
oeffnen") muss auch im Tooltip wechseln, und eine Instanz, die ihr Element
ueberlebt, haenge als leere Blase im Dokument.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
This commit is contained in:
parent
4fa0de8753
commit
45a4191f01
11 changed files with 92 additions and 39 deletions
|
|
@ -115,6 +115,12 @@ The app uses Svelte 5 runes for state management with a custom store pattern:
|
||||||
|
|
||||||
**Hotkeys**: Use `app.hotkey(event, condition, callback)` which auto-ignores input fields and contenteditable elements.
|
**Hotkeys**: Use `app.hotkey(event, condition, callback)` which auto-ignores input fields and contenteditable elements.
|
||||||
|
|
||||||
|
**Tooltips**: Never use the `title` attribute for tooltips — appwide. `<Button>`
|
||||||
|
takes a `tooltip="…"` prop (it also becomes the `aria-label`, so icon-only
|
||||||
|
buttons keep an accessible name); every other element uses the action:
|
||||||
|
`use:tooltip={{ content: '…' }}` from `app.svelte.ts`. Both render through
|
||||||
|
tippy.js, which the action initializes, updates and destroys.
|
||||||
|
|
||||||
### Project Configuration
|
### Project Configuration
|
||||||
|
|
||||||
- **Repo-Struktur**: Monorepo mit `frontend/` (SvelteKit) und `backend/`
|
- **Repo-Struktur**: Monorepo mit `frontend/` (SvelteKit) und `backend/`
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
* beide dieselbe Auswahl zeigen.
|
* beide dieselbe Auswahl zeigen.
|
||||||
*/
|
*/
|
||||||
import FlagIcon from './FlagIcon.svelte'
|
import FlagIcon from './FlagIcon.svelte'
|
||||||
|
import { tooltip } from '$lib/stores/app.svelte'
|
||||||
import { FLAG_ICON_SUGGESTIONS, iconExists } from '$lib/flagIcons'
|
import { FLAG_ICON_SUGGESTIONS, iconExists } from '$lib/flagIcons'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
|
@ -48,8 +49,8 @@
|
||||||
{#each FLAG_ICON_SUGGESTIONS as name (name)}
|
{#each FLAG_ICON_SUGGESTIONS as name (name)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title={name}
|
|
||||||
aria-label={name}
|
aria-label={name}
|
||||||
|
use:tooltip={{ content: name }}
|
||||||
aria-pressed={value === name}
|
aria-pressed={value === name}
|
||||||
class="grid place-items-center rounded-md p-1.5 hover:bg-accent"
|
class="grid place-items-center rounded-md p-1.5 hover:bg-accent"
|
||||||
class:bg-accent={value === name}
|
class:bg-accent={value === name}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="ml-auto size-7 shrink-0"
|
class="ml-auto size-7 shrink-0"
|
||||||
title={expanded ? 'Einklappen' : 'Alle anzeigen'}
|
tooltip={expanded ? 'Einklappen' : 'Alle anzeigen'}
|
||||||
onclick={() => (expanded = !expanded)}
|
onclick={() => (expanded = !expanded)}
|
||||||
>
|
>
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
|
|
@ -128,7 +128,7 @@
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
title="Zeit erfassen"
|
tooltip="Zeit erfassen"
|
||||||
onclick={() => openTimeDialog(t.rider, t.run)}
|
onclick={() => openTimeDialog(t.rider, t.run)}
|
||||||
>
|
>
|
||||||
<Timer class="size-4" />
|
<Timer class="size-4" />
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,18 @@
|
||||||
WithElementRef<HTMLAnchorAttributes> & {
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
|
/**
|
||||||
|
* Hinweistext als Tooltip. Ersetzt `title` appweit und dient
|
||||||
|
* zugleich als Beschriftung, wenn der Knopf nur ein Icon zeigt —
|
||||||
|
* ohne das bliebe er für Screenreader namenlos.
|
||||||
|
*/
|
||||||
|
tooltip?: string;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { tooltip as tooltipAction } from "$lib/stores/app.svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
class: className,
|
class: className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
|
|
@ -50,6 +58,7 @@
|
||||||
href = undefined,
|
href = undefined,
|
||||||
type = "button",
|
type = "button",
|
||||||
disabled,
|
disabled,
|
||||||
|
tooltip,
|
||||||
children,
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: ButtonProps = $props();
|
}: ButtonProps = $props();
|
||||||
|
|
@ -64,7 +73,9 @@
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
role={disabled ? "link" : undefined}
|
role={disabled ? "link" : undefined}
|
||||||
tabindex={disabled ? -1 : undefined}
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
aria-label={tooltip}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
use:tooltipAction={{ content: tooltip }}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -75,7 +86,9 @@
|
||||||
class={cn(buttonVariants({ variant, size }), className)}
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
{type}
|
{type}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
aria-label={tooltip}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
use:tooltipAction={{ content: tooltip }}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -160,20 +160,46 @@ export const utils = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tooltip für ein Element. Appweit die einzige Art, einen Hinweis an einen
|
||||||
|
* Knopf zu hängen — `title` überlassen wir dem Browser nicht mehr: Es
|
||||||
|
* erscheint erst nach einer Sekunde, sieht auf jedem System anders aus und
|
||||||
|
* lässt sich auf Touch-Geräten gar nicht aufrufen.
|
||||||
|
*
|
||||||
|
* Die Action beherrscht Aktualisieren und Aufräumen: Ein Text, der sich
|
||||||
|
* ändert (»Erledigt« / »Wieder öffnen«), muss auch im Tooltip wechseln, und
|
||||||
|
* eine Instanz, die ein entferntes Element überlebt, hinge als leere Blase
|
||||||
|
* im Dokument.
|
||||||
|
*/
|
||||||
export const tooltip = (
|
export const tooltip = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
props: { active?: boolean; content?: string; showOnCreate?: boolean },
|
props: { active?: boolean; content?: string; showOnCreate?: boolean } = {},
|
||||||
) => {
|
) => {
|
||||||
let defaultProps = {
|
const options = (p: typeof props) => ({
|
||||||
active: true,
|
|
||||||
animation: 'shift-toward-subtle',
|
animation: 'shift-toward-subtle',
|
||||||
allowHTML: true,
|
allowHTML: false,
|
||||||
|
content: p.content ?? '',
|
||||||
|
showOnCreate: p.showOnCreate,
|
||||||
|
})
|
||||||
|
|
||||||
|
const wanted = (p: typeof props) => p.active !== false && !!p.content?.trim()
|
||||||
|
|
||||||
|
let instance = wanted(props) ? tippy(element, options(props)) : null
|
||||||
|
|
||||||
|
return {
|
||||||
|
update(next: typeof props) {
|
||||||
|
if (!wanted(next)) {
|
||||||
|
instance?.destroy()
|
||||||
|
instance = null
|
||||||
|
return
|
||||||
}
|
}
|
||||||
props = {...defaultProps, ...props}
|
|
||||||
if (props.showOnCreate) {
|
if (instance) instance.setProps(options(next))
|
||||||
props = {
|
else instance = tippy(element, options(next))
|
||||||
...props,
|
},
|
||||||
|
destroy() {
|
||||||
|
instance?.destroy()
|
||||||
|
instance = null
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (props.active) tippy(element, props)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { auth } from '$lib/stores/pocketbase.svelte'
|
import { auth } from '$lib/stores/pocketbase.svelte'
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { app } from '$lib/stores/app.svelte'
|
import { app, tooltip } from '$lib/stores/app.svelte'
|
||||||
import { setTeamContext } from '$lib/stores/teams.svelte'
|
import { setTeamContext } from '$lib/stores/teams.svelte'
|
||||||
import { setEventContext } from '$lib/stores/events.svelte'
|
import { setEventContext } from '$lib/stores/events.svelte'
|
||||||
import { setRunContext } from '$lib/stores/runs.svelte'
|
import { setRunContext } from '$lib/stores/runs.svelte'
|
||||||
|
|
@ -146,8 +146,8 @@
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<a
|
<a
|
||||||
href="/dashboard/settings"
|
href="/dashboard/settings"
|
||||||
title="Profil"
|
|
||||||
class="flex min-w-0 items-center gap-2"
|
class="flex min-w-0 items-center gap-2"
|
||||||
|
use:tooltip={{ content: 'Profil' }}
|
||||||
>
|
>
|
||||||
<Avatar user={auth.user} size={24} />
|
<Avatar user={auth.user} size={24} />
|
||||||
<span class="truncate text-sm font-medium leading-none">
|
<span class="truncate text-sm font-medium leading-none">
|
||||||
|
|
@ -161,7 +161,7 @@
|
||||||
size="icon"
|
size="icon"
|
||||||
class="size-7"
|
class="size-7"
|
||||||
onclick={toggleMode}
|
onclick={toggleMode}
|
||||||
title="Design wechseln"
|
tooltip="Design wechseln"
|
||||||
>
|
>
|
||||||
{#if mode.current === 'dark'}
|
{#if mode.current === 'dark'}
|
||||||
<Sun class="h-4 w-4" />
|
<Sun class="h-4 w-4" />
|
||||||
|
|
@ -174,7 +174,7 @@
|
||||||
size="icon"
|
size="icon"
|
||||||
class="size-7"
|
class="size-7"
|
||||||
onclick={handleLogout}
|
onclick={handleLogout}
|
||||||
title="Abmelden"
|
tooltip="Abmelden"
|
||||||
>
|
>
|
||||||
<LogOut class="h-4 w-4" />
|
<LogOut class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -191,7 +191,7 @@
|
||||||
size="icon"
|
size="icon"
|
||||||
class="md:hidden"
|
class="md:hidden"
|
||||||
onclick={() => (isMobileMenuOpen = !isMobileMenuOpen)}
|
onclick={() => (isMobileMenuOpen = !isMobileMenuOpen)}
|
||||||
title="Menü"
|
tooltip="Menü"
|
||||||
>
|
>
|
||||||
{#if isMobileMenuOpen}
|
{#if isMobileMenuOpen}
|
||||||
<X class="h-4 w-4" />
|
<X class="h-4 w-4" />
|
||||||
|
|
@ -229,14 +229,14 @@
|
||||||
<TeamSwitcher />
|
<TeamSwitcher />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<Button variant="ghost" size="icon" onclick={toggleMode} title="Design wechseln">
|
<Button variant="ghost" size="icon" onclick={toggleMode} tooltip="Design wechseln">
|
||||||
{#if mode.current === 'dark'}
|
{#if mode.current === 'dark'}
|
||||||
<Sun class="h-4 w-4" />
|
<Sun class="h-4 w-4" />
|
||||||
{:else}
|
{:else}
|
||||||
<Moon class="h-4 w-4" />
|
<Moon class="h-4 w-4" />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" onclick={handleLogout} title="Abmelden">
|
<Button variant="ghost" size="icon" onclick={handleLogout} tooltip="Abmelden">
|
||||||
<LogOut class="h-4 w-4" />
|
<LogOut class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -142,10 +142,10 @@
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</a>
|
</a>
|
||||||
<div class="px-6 pb-4 flex items-center justify-end gap-1 border-t pt-3">
|
<div class="px-6 pb-4 flex items-center justify-end gap-1 border-t pt-3">
|
||||||
<Button variant="ghost" size="icon" onclick={() => openEdit(event)} title="Bearbeiten">
|
<Button variant="ghost" size="icon" onclick={() => openEdit(event)} tooltip="Bearbeiten">
|
||||||
<Pencil class="h-4 w-4" />
|
<Pencil class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" onclick={() => events.remove(event)} title="Löschen">
|
<Button variant="ghost" size="icon" onclick={() => events.remove(event)} tooltip="Löschen">
|
||||||
<Trash2 class="h-4 w-4 text-destructive" />
|
<Trash2 class="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -229,18 +229,18 @@
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell class="text-right whitespace-nowrap">
|
<Table.Cell class="text-right whitespace-nowrap">
|
||||||
{#if r.user}
|
{#if r.user}
|
||||||
<Button variant="ghost" size="icon" title="Verknüpfung lösen" onclick={() => doUnlink(r)}>
|
<Button variant="ghost" size="icon" tooltip="Verknüpfung lösen" onclick={() => doUnlink(r)}>
|
||||||
<Unlink class="size-4" />
|
<Unlink class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button variant="ghost" size="icon" title="Konto verknüpfen" onclick={() => openLogin(r)}>
|
<Button variant="ghost" size="icon" tooltip="Konto verknüpfen" onclick={() => openLogin(r)}>
|
||||||
<KeyRound class="size-4" />
|
<KeyRound class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button variant="ghost" size="icon" title="Bearbeiten" onclick={() => openEdit(r)}>
|
<Button variant="ghost" size="icon" tooltip="Bearbeiten" onclick={() => openEdit(r)}>
|
||||||
<Pencil class="size-4" />
|
<Pencil class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" title="Löschen" onclick={() => riders.remove(r)}>
|
<Button variant="ghost" size="icon" tooltip="Löschen" onclick={() => riders.remove(r)}>
|
||||||
<Trash2 class="size-4 text-destructive" />
|
<Trash2 class="size-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@
|
||||||
Verlassen
|
Verlassen
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button variant="ghost" size="icon" title="Team löschen" onclick={() => teams.remove(t)}>
|
<Button variant="ghost" size="icon" tooltip="Team löschen" onclick={() => teams.remove(t)}>
|
||||||
<Trash2 class="size-4 text-destructive" />
|
<Trash2 class="size-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -286,7 +286,7 @@
|
||||||
Zu Admin
|
Zu Admin
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button variant="ghost" size="icon" title="Entfernen" onclick={() => doRemoveMember(t, userId)}>
|
<Button variant="ghost" size="icon" tooltip="Entfernen" onclick={() => doRemoveMember(t, userId)}>
|
||||||
<UserMinus class="size-4 text-destructive" />
|
<UserMinus class="size-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { getTrailMarkerContext } from '$lib/stores/trailMarkers.svelte'
|
import { getTrailMarkerContext } from '$lib/stores/trailMarkers.svelte'
|
||||||
import TrailMap from '$lib/components/TrailMap.svelte'
|
import TrailMap from '$lib/components/TrailMap.svelte'
|
||||||
import { trailStatus } from '$lib/trailStatus'
|
import { trailStatus } from '$lib/trailStatus'
|
||||||
|
import { tooltip } from '$lib/stores/app.svelte'
|
||||||
import { getTrailCommentContext } from '$lib/stores/trailComments.svelte'
|
import { getTrailCommentContext } from '$lib/stores/trailComments.svelte'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
|
@ -160,7 +161,9 @@
|
||||||
{#if open > 0}
|
{#if open > 0}
|
||||||
<span
|
<span
|
||||||
class="flex items-center gap-1 text-amber-600 ml-auto"
|
class="flex items-center gap-1 text-amber-600 ml-auto"
|
||||||
title="{open} offene Meldung{open === 1 ? '' : 'en'}"
|
use:tooltip={{
|
||||||
|
content: `${open} offene Meldung${open === 1 ? '' : 'en'}`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<AlertTriangle class="size-4" />
|
<AlertTriangle class="size-4" />
|
||||||
{open}
|
{open}
|
||||||
|
|
@ -177,9 +180,11 @@
|
||||||
class="flex items-center gap-1.5"
|
class="flex items-center gap-1.5"
|
||||||
class:text-muted-foreground={!freshMarkers}
|
class:text-muted-foreground={!freshMarkers}
|
||||||
class:font-medium={freshMarkers}
|
class:font-medium={freshMarkers}
|
||||||
title={freshMarkers
|
use:tooltip={{
|
||||||
|
content: freshMarkers
|
||||||
? `Marker, neue in den letzten ${RECENT_DAYS} Tagen`
|
? `Marker, neue in den letzten ${RECENT_DAYS} Tagen`
|
||||||
: 'Marker'}
|
: 'Marker',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MapPin class="size-4" />
|
<MapPin class="size-4" />
|
||||||
{markerCount}
|
{markerCount}
|
||||||
|
|
@ -191,9 +196,11 @@
|
||||||
class="flex items-center gap-1.5"
|
class="flex items-center gap-1.5"
|
||||||
class:text-muted-foreground={!freshComments}
|
class:text-muted-foreground={!freshComments}
|
||||||
class:font-medium={freshComments}
|
class:font-medium={freshComments}
|
||||||
title={freshComments
|
use:tooltip={{
|
||||||
|
content: freshComments
|
||||||
? `Kommentare, neue in den letzten ${RECENT_DAYS} Tagen`
|
? `Kommentare, neue in den letzten ${RECENT_DAYS} Tagen`
|
||||||
: 'Kommentare'}
|
: 'Kommentare',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MessageSquare class="size-4" />
|
<MessageSquare class="size-4" />
|
||||||
{commentCount}
|
{commentCount}
|
||||||
|
|
|
||||||
|
|
@ -459,7 +459,7 @@
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="size-8"
|
class="size-8"
|
||||||
title={m.resolved ? 'Wieder öffnen' : 'Als erledigt markieren'}
|
tooltip={m.resolved ? 'Wieder öffnen' : 'Als erledigt markieren'}
|
||||||
onclick={async () => {
|
onclick={async () => {
|
||||||
try {
|
try {
|
||||||
await markers.toggleResolved(m.id)
|
await markers.toggleResolved(m.id)
|
||||||
|
|
@ -474,7 +474,7 @@
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="size-8"
|
class="size-8"
|
||||||
title="Marker löschen"
|
tooltip="Marker löschen"
|
||||||
onclick={() => removeMarker(m)}
|
onclick={() => removeMarker(m)}
|
||||||
>
|
>
|
||||||
<Trash2 class="size-4 text-destructive" />
|
<Trash2 class="size-4 text-destructive" />
|
||||||
|
|
@ -583,7 +583,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if v.gpx}
|
{#if v.gpx}
|
||||||
<Button variant="ghost" size="icon" class="size-8" href={getFileURL(v, v.gpx)} title="GPX herunterladen">
|
<Button variant="ghost" size="icon" class="size-8" href={getFileURL(v, v.gpx)} tooltip="GPX herunterladen">
|
||||||
<Download class="size-4" />
|
<Download class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue