managarten/packages/shared-ui/src/molecules/Select.svelte
Till JS ce923bbdc7 shared-ui: Sync auf mana/shared-ui v1.0.0 + AppSlider tot weg
Workspace-Kopie in packages/shared-ui synchronisiert mit
mana@1dc8a98 (Compat-Layer für alle v0.1.x-Patterns). 219 Files
geändert — alter Code (Charts, Quick-Input-Originale, Help, Onboarding,
Settings, Bottom-Stack, Search-Core, ColorPicker, Actions) entfällt;
neue v1.0.0-Komponenten kommen rein.

tsconfig.json self-contained (kein extends auf nicht-existierenden
managarten/tsconfig.base.json).

pnpm check ergibt jetzt 0 Errors über alle 10086 Files
(Stand vorher: 204 Errors mit dem unverarbeiteten Sync). Zwei
non-blocking Warnings stehen offen (SSR-nested-button bei TagChip,
ARIA-Role bei Pill mit click-handler).

AppSlider toter Code in apps/mana/apps/web/src/lib/components/
AppSlider.svelte entfernt — der Wrapper hatte keine Aufrufer mehr.

mana-internal Configs (Storybook, lost-pixel, vite.config, Dockerfile,
infrastructure, PORTING_PLAN.md) bewusst NICHT gesynced — die wandern
nur im mana-Repo. managarten-shared-ui ist eingefrorene Kopie, kein
publish-target.

scripts/validate-disziplin.mjs: ungenutzte lines-Variable entfernt
(ESLint no-unused-vars).
2026-05-21 14:56:54 +02:00

174 lines
3.2 KiB
Svelte

<script lang="ts">
type Size = 'sm' | 'md' | 'lg';
interface Option {
value: string;
label: string;
disabled?: boolean;
}
interface Props {
value?: string;
options: Option[];
label?: string;
placeholder?: string;
hint?: string;
error?: string;
disabled?: boolean;
required?: boolean;
size?: Size;
id?: string;
name?: string;
ariaLabel?: string;
onchange?: (e: Event) => void;
}
let {
value = $bindable(''),
options,
label,
placeholder,
hint,
error,
disabled = false,
required = false,
size = 'md',
id,
name,
ariaLabel,
onchange,
}: Props = $props();
const inputId = $derived(id ?? `select-${Math.random().toString(36).slice(2, 9)}`);
const hintId = $derived(hint || error ? `${inputId}-hint` : undefined);
</script>
<div class="field">
{#if label}
<label for={inputId}>
{label}
{#if required}<span class="required" aria-hidden="true">*</span>{/if}
</label>
{/if}
<div class="wrap size-{size}" class:disabled class:has-error={!!error}>
<select
id={inputId}
{name}
{disabled}
{required}
aria-label={ariaLabel}
aria-invalid={error ? 'true' : undefined}
aria-describedby={hintId}
bind:value
{onchange}
>
{#if placeholder}
<option value="" disabled selected hidden>{placeholder}</option>
{/if}
{#each options as opt}
<option value={opt.value} disabled={opt.disabled}>{opt.label}</option>
{/each}
</select>
<span class="caret" aria-hidden="true"></span>
</div>
{#if error}
<p class="hint error" id={hintId} role="alert">{error}</p>
{:else if hint}
<p class="hint" id={hintId}>{hint}</p>
{/if}
</div>
<style>
.field {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
label {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--color-foreground));
}
.required {
color: hsl(var(--color-error));
margin-left: 0.125rem;
}
.wrap {
position: relative;
display: flex;
align-items: center;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-border));
border-radius: 0.5rem;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease;
}
.wrap:focus-within {
border-color: hsl(var(--color-primary));
box-shadow: 0 0 0 2px hsl(var(--color-primary) / 0.2);
}
.wrap.disabled {
opacity: 0.6;
background: hsl(var(--color-muted));
}
.wrap.has-error {
border-color: hsl(var(--color-error));
}
.size-sm select {
padding: 0.25rem 2rem 0.25rem 0.625rem;
}
.size-md select {
padding: 0.5rem 2rem 0.5rem 0.75rem;
}
.size-lg select {
padding: 0.625rem 2rem 0.625rem 0.875rem;
}
select {
appearance: none;
flex: 1;
min-width: 0;
border: none;
background: transparent;
color: hsl(var(--color-foreground));
font: inherit;
outline: none;
cursor: pointer;
}
select:disabled {
cursor: not-allowed;
}
.caret {
position: absolute;
right: 0.625rem;
pointer-events: none;
color: hsl(var(--color-muted-foreground));
font-size: 0.875rem;
}
.hint {
margin: 0;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
.hint.error {
color: hsl(var(--color-error));
}
@media (prefers-reduced-motion: reduce) {
.wrap {
transition: none;
}
}
</style>