managarten/packages/shared-ui/src/molecules/Textarea.svelte
Wuesteon d36b321d9d style: auto-format codebase with Prettier
Applied formatting to 1487+ files using pnpm format:write
  - TypeScript/JavaScript files
  - Svelte components
  - Astro pages
  - JSON configs
  - Markdown docs

  13 files still need manual review (Astro JSX comments)
2025-11-27 18:33:16 +01:00

201 lines
4.1 KiB
Svelte

<script lang="ts">
interface Props {
/** Current value */
value: string;
/** Called on input */
oninput?: (value: string) => void;
/** Called on change (blur) */
onchange?: (value: string) => void;
/** Label text */
label?: string;
/** Placeholder text */
placeholder?: string;
/** Number of visible rows */
rows?: number;
/** Maximum character count */
maxlength?: number;
/** Show character count */
showCount?: boolean;
/** Error message */
error?: string;
/** Disable the textarea */
disabled?: boolean;
/** Mark as required */
required?: boolean;
/** Enable auto-resize based on content */
autoResize?: boolean;
/** Additional CSS classes */
class?: string;
/** Unique ID for accessibility */
id?: string;
}
let {
value = $bindable(),
oninput,
onchange,
label,
placeholder,
rows = 3,
maxlength,
showCount = false,
error,
disabled = false,
required = false,
autoResize = false,
class: className = '',
id = `textarea-${Math.random().toString(36).slice(2, 9)}`,
}: Props = $props();
let textareaElement: HTMLTextAreaElement | null = $state(null);
const charCount = $derived(value?.length ?? 0);
const isOverLimit = $derived(maxlength ? charCount > maxlength : false);
function handleInput(e: Event) {
const target = e.target as HTMLTextAreaElement;
value = target.value;
oninput?.(target.value);
if (autoResize && textareaElement) {
textareaElement.style.height = 'auto';
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
}
}
function handleChange(e: Event) {
const target = e.target as HTMLTextAreaElement;
onchange?.(target.value);
}
</script>
<div class="textarea-wrapper {className}">
{#if label}
<label for={id} class="textarea-label">
{label}
{#if required}
<span class="textarea-required">*</span>
{/if}
</label>
{/if}
<textarea
{id}
bind:this={textareaElement}
{value}
{placeholder}
{rows}
{maxlength}
{disabled}
{required}
oninput={handleInput}
onchange={handleChange}
class="textarea-input {error || isOverLimit ? 'textarea-input--error' : ''} {autoResize
? 'textarea-input--auto-resize'
: ''}"
></textarea>
<div class="textarea-footer">
{#if error}
<p class="textarea-error">{error}</p>
{:else}
<span></span>
{/if}
{#if showCount || maxlength}
<span class="textarea-count {isOverLimit ? 'textarea-count--error' : ''}">
{charCount}{#if maxlength}/{maxlength}{/if}
</span>
{/if}
</div>
</div>
<style>
.textarea-wrapper {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.textarea-label {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--color-foreground));
}
.textarea-required {
color: hsl(var(--color-error));
margin-left: 0.125rem;
}
.textarea-input {
width: 100%;
padding: 0.625rem 1rem;
font-size: 0.875rem;
font-family: inherit;
line-height: 1.5;
color: hsl(var(--color-foreground));
background-color: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-border));
border-radius: 0.5rem;
resize: vertical;
transition: all 0.15s ease;
}
.textarea-input:hover:not(:disabled) {
border-color: hsl(var(--color-border-strong));
}
.textarea-input:focus {
outline: none;
border-color: hsl(var(--color-primary));
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.1);
}
.textarea-input:disabled {
opacity: 0.5;
cursor: not-allowed;
resize: none;
}
.textarea-input--error {
border-color: hsl(var(--color-error));
}
.textarea-input--error:focus {
border-color: hsl(var(--color-error));
box-shadow: 0 0 0 3px hsl(var(--color-error) / 0.1);
}
.textarea-input--auto-resize {
resize: none;
overflow: hidden;
}
.textarea-input::placeholder {
color: hsl(var(--color-muted-foreground));
}
.textarea-footer {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 1.25rem;
}
.textarea-error {
font-size: 0.75rem;
color: hsl(var(--color-error));
margin: 0;
}
.textarea-count {
font-size: 0.75rem;
color: hsl(var(--color-muted-foreground));
margin-left: auto;
}
.textarea-count--error {
color: hsl(var(--color-error));
}
</style>