mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 13:09:39 +02:00
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)
201 lines
4.1 KiB
Svelte
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>
|