Splits the flat 38-component ui/ folder into primitives/ (generic controls), overlay/ (menus/modals), media/ (music-domain display components), forms/ (composite entity forms) and layout/ (page scaffolding), and updates every import path accordingly.
61 lines
1.1 KiB
Svelte
61 lines
1.1 KiB
Svelte
<script lang="ts">
|
|
type Size = 'sm' | 'md' | 'lg' | 'hero';
|
|
|
|
interface Props {
|
|
name: string;
|
|
src?: string | null;
|
|
size: Size;
|
|
class?: string;
|
|
}
|
|
|
|
let { name, src, size, class: klass = '' }: Props = $props();
|
|
|
|
const DIMENSION: Record<Size, string> = {
|
|
sm: 'size-cover-xs',
|
|
md: 'size-cover-lg',
|
|
lg: 'size-cover-xl',
|
|
hero: 'size-cover-hero'
|
|
};
|
|
|
|
const TEXT: Record<Size, string> = {
|
|
sm: 'text-sm',
|
|
md: 'text-base',
|
|
lg: 'text-xl',
|
|
hero: 'text-3xl'
|
|
};
|
|
|
|
const initials = $derived(
|
|
name
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map((part) => part[0]?.toUpperCase() ?? '')
|
|
.join('')
|
|
);
|
|
|
|
let failed = $state(false);
|
|
$effect(() => {
|
|
src;
|
|
failed = false;
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
class="relative grid shrink-0 place-items-center overflow-hidden rounded-full bg-surface-2 {DIMENSION[
|
|
size
|
|
]} {klass}"
|
|
>
|
|
{#if src && !failed}
|
|
<img
|
|
{src}
|
|
alt=""
|
|
loading="lazy"
|
|
decoding="async"
|
|
onerror={() => (failed = true)}
|
|
class="absolute inset-0 h-full w-full object-cover"
|
|
/>
|
|
{:else}
|
|
<span class="font-semibold text-fg uppercase {TEXT[size]}">{initials}</span>
|
|
{/if}
|
|
</div>
|