Files
kochwas/src/lib/components/ProfileSwitcher.svelte
hsiegeln 5a291a53dd
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m22s
refactor(ui): --pill-radius CSS-Variable (Item F)
border-radius: 999px war 15x im CSS dupliziert. Ausgelagert als
:root --pill-radius Variable im globalen :root-Block in +layout.svelte,
Call-Sites auf var(--pill-radius) umgestellt.

Bewusst NICHT angefasst (plan war "nur Werte die mehrfach vorkommen"):
- z-index: 10 Distinct Values in 14 Sites, bilden ein implizites
  Layer-System. Konsolidieren = behavior-change-Risiko ohne konkreten
  Nutzen. Wenn kuenftig einheitliche Modal-/Popover-Layer noetig,
  separate Phase.
- setTimeout(): 3 Sites, jeder mit eigener Semantik (Debounce/Print/
  Spinner). Kein DRY-Nutzen durch Extraktion.

Gate: svelte-check 0 Warnings, 184/184 Tests, Build clean, kein
sichtbarer Unterschied (einzige Aenderung: selber Wert ueber Variable).

Refs docs/superpowers/plans/2026-04-19-post-review-roadmap.md Item F.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:43:19 +02:00

219 lines
4.8 KiB
Svelte

<script lang="ts">
import { CircleUser } from 'lucide-svelte';
import { profileStore } from '$lib/client/profile.svelte';
import { alertAction } from '$lib/client/confirm.svelte';
let showModal = $state(false);
let newName = $state('');
let newEmoji = $state('');
async function createAndSelect() {
if (!newName.trim()) return;
try {
const p = await profileStore.create(newName.trim(), newEmoji || null);
profileStore.select(p.id);
newName = '';
showModal = false;
} catch (e) {
await alertAction({
title: 'Profil konnte nicht angelegt werden',
message: (e as Error).message
});
}
}
</script>
<button class="chip" onclick={() => (showModal = true)} aria-label="Profil wechseln">
<span class="icon"><CircleUser size={20} strokeWidth={1.75} /></span>
{#if profileStore.active}
<span class="name">{profileStore.active.name}</span>
{:else}
<span class="name">Profil wählen</span>
{/if}
</button>
{#if showModal}
<div
class="backdrop"
role="dialog"
aria-modal="true"
aria-label="Profil auswählen"
>
<button
class="backdrop-close"
aria-label="Schließen"
onclick={() => (showModal = false)}
></button>
<div class="modal" role="document">
<h2>Wer kocht heute?</h2>
<ul class="list">
{#each profileStore.profiles as p (p.id)}
<li>
<button
class="profile-btn"
class:active={profileStore.activeId === p.id}
onclick={() => {
profileStore.select(p.id);
showModal = false;
}}
>
{#if p.avatar_emoji}
<span class="emoji-lg">{p.avatar_emoji}</span>
{:else}
<span class="icon-lg"><CircleUser size={28} strokeWidth={1.5} /></span>
{/if}
<span>{p.name}</span>
</button>
</li>
{/each}
</ul>
<hr />
<div class="new">
<h3>Neues Profil</h3>
<div class="new-row">
<input
type="text"
placeholder="🙂"
aria-label="Emoji (optional)"
bind:value={newEmoji}
maxlength="8"
class="emoji-input"
/>
<input
type="text"
placeholder="Name"
bind:value={newName}
maxlength="50"
onkeydown={(e) => e.key === 'Enter' && createAndSelect()}
/>
<button class="primary" onclick={createAndSelect}>Anlegen</button>
</div>
</div>
</div>
</div>
{/if}
<style>
.chip {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.9rem;
border-radius: var(--pill-radius);
border: 1px solid #cfd9d1;
background: white;
font-size: 0.95rem;
cursor: pointer;
min-height: 44px;
}
.chip:hover {
background: #f4f8f5;
}
.icon {
display: inline-flex;
align-items: center;
color: #2b6a3d;
}
.backdrop {
position: fixed;
inset: 0;
display: grid;
place-items: center;
padding: 1rem;
z-index: 100;
}
.backdrop-close {
position: absolute;
inset: 0;
border: 0;
background: rgba(0, 0, 0, 0.45);
cursor: pointer;
}
.modal {
position: relative;
}
.modal {
background: white;
border-radius: 16px;
padding: 1.25rem;
width: min(420px, 100%);
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
}
.modal h2 {
margin: 0 0 0.75rem;
font-size: 1.2rem;
}
.list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.profile-btn {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #e4eae7;
border-radius: 12px;
background: white;
font-size: 1rem;
cursor: pointer;
min-height: 52px;
}
.profile-btn:hover {
background: #f4f8f5;
}
.profile-btn.active {
border-color: #2b6a3d;
background: #eaf4ed;
}
.emoji-lg {
font-size: 1.6rem;
}
.icon-lg {
display: inline-flex;
align-items: center;
color: #2b6a3d;
}
hr {
border: none;
border-top: 1px solid #e4eae7;
margin: 1rem 0;
}
.new h3 {
font-size: 0.95rem;
margin: 0 0 0.5rem;
color: #555;
}
.new-row {
display: flex;
gap: 0.5rem;
}
.emoji-input {
width: 3.5rem;
text-align: center;
}
.new-row input {
flex: 1;
padding: 0.6rem 0.8rem;
font-size: 1rem;
border: 1px solid #cfd9d1;
border-radius: 8px;
}
.primary {
padding: 0.6rem 1rem;
background: #2b6a3d;
color: white;
border: 0;
border-radius: 8px;
cursor: pointer;
font-size: 0.95rem;
}
</style>