@salmexio/ui 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +52 -3
  2. package/dist/dialogs/ContextMenu/ContextMenu.svelte +97 -94
  3. package/dist/dialogs/ContextMenu/ContextMenu.svelte.d.ts +3 -2
  4. package/dist/dialogs/ContextMenu/ContextMenu.svelte.d.ts.map +1 -1
  5. package/dist/dialogs/Modal/Modal.svelte +112 -116
  6. package/dist/dialogs/Modal/Modal.svelte.d.ts +1 -1
  7. package/dist/feedback/Alert/Alert.svelte +115 -221
  8. package/dist/feedback/Alert/Alert.svelte.d.ts +1 -1
  9. package/dist/feedback/ProgressBar/ProgressBar.svelte +246 -0
  10. package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts +40 -0
  11. package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts.map +1 -0
  12. package/dist/feedback/ProgressBar/index.d.ts +2 -0
  13. package/dist/feedback/ProgressBar/index.d.ts.map +1 -0
  14. package/dist/feedback/ProgressBar/index.js +1 -0
  15. package/dist/feedback/Skeleton/Skeleton.svelte +153 -0
  16. package/dist/feedback/Skeleton/Skeleton.svelte.d.ts +37 -0
  17. package/dist/feedback/Skeleton/Skeleton.svelte.d.ts.map +1 -0
  18. package/dist/feedback/Skeleton/index.d.ts +2 -0
  19. package/dist/feedback/Skeleton/index.d.ts.map +1 -0
  20. package/dist/feedback/Skeleton/index.js +1 -0
  21. package/dist/feedback/Spinner/Spinner.svelte +86 -151
  22. package/dist/feedback/Spinner/Spinner.svelte.d.ts +5 -3
  23. package/dist/feedback/Spinner/Spinner.svelte.d.ts.map +1 -1
  24. package/dist/feedback/Toast/Toaster.svelte +431 -0
  25. package/dist/feedback/Toast/Toaster.svelte.d.ts +22 -0
  26. package/dist/feedback/Toast/Toaster.svelte.d.ts.map +1 -0
  27. package/dist/feedback/Toast/index.d.ts +4 -0
  28. package/dist/feedback/Toast/index.d.ts.map +1 -0
  29. package/dist/feedback/Toast/index.js +2 -0
  30. package/dist/feedback/Toast/toastStore.d.ts +34 -0
  31. package/dist/feedback/Toast/toastStore.d.ts.map +1 -0
  32. package/dist/feedback/Toast/toastStore.js +43 -0
  33. package/dist/feedback/index.d.ts +4 -0
  34. package/dist/feedback/index.d.ts.map +1 -1
  35. package/dist/feedback/index.js +3 -0
  36. package/dist/forms/Checkbox/Checkbox.svelte +82 -104
  37. package/dist/forms/Checkbox/Checkbox.svelte.d.ts +1 -1
  38. package/dist/forms/Select/Select.svelte +137 -179
  39. package/dist/forms/Select/Select.svelte.d.ts +1 -1
  40. package/dist/forms/Slider/Slider.svelte +356 -0
  41. package/dist/forms/Slider/Slider.svelte.d.ts +50 -0
  42. package/dist/forms/Slider/Slider.svelte.d.ts.map +1 -0
  43. package/dist/forms/Slider/index.d.ts +2 -0
  44. package/dist/forms/Slider/index.d.ts.map +1 -0
  45. package/dist/forms/Slider/index.js +1 -0
  46. package/dist/forms/TextInput/TextInput.svelte +151 -167
  47. package/dist/forms/TextInput/TextInput.svelte.d.ts +1 -1
  48. package/dist/forms/Textarea/Textarea.svelte +615 -0
  49. package/dist/forms/Textarea/Textarea.svelte.d.ts +47 -0
  50. package/dist/forms/Textarea/Textarea.svelte.d.ts.map +1 -0
  51. package/dist/forms/Textarea/index.d.ts +2 -0
  52. package/dist/forms/Textarea/index.d.ts.map +1 -0
  53. package/dist/forms/Textarea/index.js +1 -0
  54. package/dist/forms/Toggle/Toggle.svelte +239 -0
  55. package/dist/forms/Toggle/Toggle.svelte.d.ts +39 -0
  56. package/dist/forms/Toggle/Toggle.svelte.d.ts.map +1 -0
  57. package/dist/forms/Toggle/index.d.ts +2 -0
  58. package/dist/forms/Toggle/index.d.ts.map +1 -0
  59. package/dist/forms/Toggle/index.js +1 -0
  60. package/dist/forms/index.d.ts +3 -0
  61. package/dist/forms/index.d.ts.map +1 -1
  62. package/dist/forms/index.js +3 -0
  63. package/dist/index.d.ts +0 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +0 -1
  66. package/dist/layout/Card/Card.svelte +64 -39
  67. package/dist/layout/Card/Card.svelte.d.ts +1 -1
  68. package/dist/layout/Card/Card.svelte.d.ts.map +1 -1
  69. package/dist/layout/Container/Container.svelte +71 -71
  70. package/dist/layout/Container/Container.svelte.d.ts +2 -2
  71. package/dist/navigation/CommandPalette/CommandPalette.svelte +407 -189
  72. package/dist/navigation/CommandPalette/CommandPalette.svelte.d.ts +8 -3
  73. package/dist/navigation/CommandPalette/CommandPalette.svelte.d.ts.map +1 -1
  74. package/dist/navigation/Tabs/Tabs.svelte +95 -181
  75. package/dist/navigation/Tabs/Tabs.svelte.d.ts +2 -2
  76. package/dist/primitives/Badge/Badge.svelte +83 -220
  77. package/dist/primitives/Badge/Badge.svelte.d.ts +2 -2
  78. package/dist/primitives/Badge/Badge.svelte.d.ts.map +1 -1
  79. package/dist/primitives/Button/Button.svelte +144 -195
  80. package/dist/primitives/Button/Button.svelte.d.ts +3 -3
  81. package/dist/primitives/Button/Button.svelte.d.ts.map +1 -1
  82. package/dist/primitives/Tooltip/Tooltip.svelte +260 -0
  83. package/dist/primitives/Tooltip/Tooltip.svelte.d.ts +36 -0
  84. package/dist/primitives/Tooltip/Tooltip.svelte.d.ts.map +1 -0
  85. package/dist/primitives/Tooltip/index.d.ts +2 -0
  86. package/dist/primitives/Tooltip/index.d.ts.map +1 -0
  87. package/dist/primitives/Tooltip/index.js +1 -0
  88. package/dist/primitives/index.d.ts +1 -0
  89. package/dist/primitives/index.d.ts.map +1 -1
  90. package/dist/primitives/index.js +1 -0
  91. package/dist/styles/tokens.css +197 -265
  92. package/package.json +5 -5
  93. package/dist/windowing/Window/Window.svelte +0 -637
  94. package/dist/windowing/Window/Window.svelte.d.ts +0 -65
  95. package/dist/windowing/Window/Window.svelte.d.ts.map +0 -1
  96. package/dist/windowing/Window/index.d.ts +0 -2
  97. package/dist/windowing/Window/index.d.ts.map +0 -1
  98. package/dist/windowing/Window/index.js +0 -1
  99. package/dist/windowing/WindowManager/WindowManager.svelte +0 -425
  100. package/dist/windowing/WindowManager/WindowManager.svelte.d.ts +0 -38
  101. package/dist/windowing/WindowManager/WindowManager.svelte.d.ts.map +0 -1
  102. package/dist/windowing/WindowManager/index.d.ts +0 -2
  103. package/dist/windowing/WindowManager/index.d.ts.map +0 -1
  104. package/dist/windowing/WindowManager/index.js +0 -1
  105. package/dist/windowing/index.d.ts +0 -5
  106. package/dist/windowing/index.d.ts.map +0 -1
  107. package/dist/windowing/index.js +0 -3
  108. package/dist/windowing/windowStore.svelte.d.ts +0 -49
  109. package/dist/windowing/windowStore.svelte.d.ts.map +0 -1
  110. package/dist/windowing/windowStore.svelte.js +0 -170
@@ -0,0 +1,615 @@
1
+ <!--
2
+ @component Textarea
3
+
4
+ Neo-Brutalist Dark — Multi-line text input with custom resize handle,
5
+ auto-resize, character count, validation states, and full accessibility.
6
+ Follows TextInput visual patterns.
7
+
8
+ @example
9
+ <Textarea label="Description" bind:value={desc} />
10
+ <Textarea label="Bio" autoResize maxRows={8} showCharCount maxLength={500} />
11
+ <Textarea label="Notes" resize="both" />
12
+ -->
13
+ <script lang="ts">
14
+ import { cn } from '../../utils/cn.js';
15
+ import { tick, onMount } from 'svelte';
16
+
17
+ type TextareaSize = 'sm' | 'md' | 'lg';
18
+ type ResizeMode = 'none' | 'vertical' | 'horizontal' | 'both';
19
+
20
+ interface Props {
21
+ id?: string;
22
+ name?: string;
23
+ label: string;
24
+ value?: string;
25
+ placeholder?: string;
26
+ required?: boolean;
27
+ disabled?: boolean;
28
+ readonly?: boolean;
29
+ error?: string;
30
+ hint?: string;
31
+ successMessage?: string;
32
+ maxLength?: number;
33
+ minLength?: number;
34
+ showCharCount?: boolean;
35
+ rows?: number;
36
+ minRows?: number;
37
+ maxRows?: number;
38
+ autoResize?: boolean;
39
+ /** Manual resize direction. Ignored when autoResize is true. */
40
+ resize?: ResizeMode;
41
+ size?: TextareaSize;
42
+ hideLabel?: boolean;
43
+ class?: string;
44
+ oninput?: (event: Event) => void;
45
+ onblur?: (event: FocusEvent) => void;
46
+ onfocus?: (event: FocusEvent) => void;
47
+ testId?: string;
48
+ }
49
+
50
+ let {
51
+ id = `textarea-${Math.random().toString(36).slice(2, 9)}`,
52
+ name,
53
+ label,
54
+ value = $bindable(''),
55
+ placeholder = '',
56
+ required = false,
57
+ disabled = false,
58
+ readonly = false,
59
+ error = '',
60
+ hint = '',
61
+ successMessage = '',
62
+ maxLength,
63
+ minLength,
64
+ showCharCount = false,
65
+ rows = 3,
66
+ minRows,
67
+ maxRows,
68
+ autoResize = false,
69
+ resize = 'vertical',
70
+ size = 'md',
71
+ hideLabel = false,
72
+ class: className = '',
73
+ oninput,
74
+ onblur,
75
+ onfocus,
76
+ testId
77
+ }: Props = $props();
78
+
79
+ let textareaEl = $state<HTMLTextAreaElement | null>(null);
80
+ let wrapperEl = $state<HTMLElement | null>(null);
81
+ let isFocused = $state(false);
82
+ let isDragging = $state(false);
83
+
84
+ const errorId = $derived(`${id}-error`);
85
+ const hintId = $derived(`${id}-hint`);
86
+ const successId = $derived(`${id}-success`);
87
+ const charCountId = $derived(`${id}-charcount`);
88
+
89
+ const charCount = $derived(value.length);
90
+ const charWarning = $derived(maxLength ? charCount >= maxLength * 0.9 : false);
91
+ const charExceeded = $derived(maxLength ? charCount > maxLength : false);
92
+
93
+ const effectiveResize = $derived<ResizeMode>(autoResize ? 'none' : resize);
94
+ const showHandle = $derived(effectiveResize !== 'none' && !disabled);
95
+ const resizeVertical = $derived(effectiveResize === 'vertical' || effectiveResize === 'both');
96
+ const resizeHorizontal = $derived(effectiveResize === 'horizontal' || effectiveResize === 'both');
97
+
98
+ const describedBy = $derived(
99
+ [
100
+ error ? errorId : null,
101
+ successMessage && !error ? successId : null,
102
+ hint && !error && !successMessage ? hintId : null,
103
+ showCharCount ? charCountId : null
104
+ ]
105
+ .filter(Boolean)
106
+ .join(' ') || undefined
107
+ );
108
+
109
+ function handleInput(e: Event) {
110
+ if (autoResize) adjustHeight();
111
+ oninput?.(e);
112
+ }
113
+
114
+ function handleFocus(e: FocusEvent) {
115
+ isFocused = true;
116
+ onfocus?.(e);
117
+ }
118
+
119
+ function handleBlur(e: FocusEvent) {
120
+ isFocused = false;
121
+ onblur?.(e);
122
+ }
123
+
124
+ // ── Auto-resize ──
125
+
126
+ function adjustHeight() {
127
+ if (!textareaEl) return;
128
+ textareaEl.style.height = 'auto';
129
+
130
+ const style = getComputedStyle(textareaEl);
131
+ const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5;
132
+ const paddingTop = parseFloat(style.paddingTop);
133
+ const paddingBottom = parseFloat(style.paddingBottom);
134
+ const borderTop = parseFloat(style.borderTopWidth);
135
+ const borderBottom = parseFloat(style.borderBottomWidth);
136
+ const extra = paddingTop + paddingBottom + borderTop + borderBottom;
137
+
138
+ const effectiveMinRows = minRows ?? rows;
139
+ const minH = lineHeight * effectiveMinRows + extra;
140
+ let maxH = Infinity;
141
+ if (maxRows) {
142
+ maxH = lineHeight * maxRows + extra;
143
+ }
144
+
145
+ const scrollH = textareaEl.scrollHeight;
146
+ const height = Math.max(minH, Math.min(scrollH, maxH));
147
+ textareaEl.style.height = `${height}px`;
148
+ }
149
+
150
+ $effect(() => {
151
+ if (autoResize && textareaEl) {
152
+ void value;
153
+ tick().then(adjustHeight);
154
+ }
155
+ });
156
+
157
+ // ── Custom resize handle ──
158
+
159
+ let startX = 0;
160
+ let startY = 0;
161
+ let startW = 0;
162
+ let startH = 0;
163
+ const MIN_HEIGHT = 48;
164
+ const MIN_WIDTH = 120;
165
+
166
+ function onHandlePointerDown(e: PointerEvent) {
167
+ if (disabled || effectiveResize === 'none') return;
168
+ e.preventDefault();
169
+
170
+ const wrapper = wrapperEl;
171
+ if (!wrapper) return;
172
+
173
+ isDragging = true;
174
+ startX = e.clientX;
175
+ startY = e.clientY;
176
+ startW = wrapper.offsetWidth;
177
+ startH = wrapper.offsetHeight;
178
+
179
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
180
+ }
181
+
182
+ function onHandlePointerMove(e: PointerEvent) {
183
+ if (!isDragging || !wrapperEl) return;
184
+
185
+ if (resizeHorizontal) {
186
+ const newW = Math.max(MIN_WIDTH, startW + (e.clientX - startX));
187
+ wrapperEl.style.width = `${newW}px`;
188
+ }
189
+
190
+ if (resizeVertical) {
191
+ const newH = Math.max(MIN_HEIGHT, startH + (e.clientY - startY));
192
+ wrapperEl.style.height = `${newH}px`;
193
+ if (textareaEl) {
194
+ textareaEl.style.height = '100%';
195
+ }
196
+ }
197
+ }
198
+
199
+ function onHandlePointerUp() {
200
+ isDragging = false;
201
+ }
202
+
203
+ // If resize mode includes vertical, set the wrapper to a measured initial height
204
+ // so the textarea fills it and the custom handle works naturally.
205
+ onMount(() => {
206
+ if (effectiveResize !== 'none' && wrapperEl && textareaEl) {
207
+ // Let the textarea render at its natural size, then pin the wrapper
208
+ const naturalH = wrapperEl.offsetHeight;
209
+ wrapperEl.style.height = `${naturalH}px`;
210
+ textareaEl.style.height = '100%';
211
+ }
212
+ });
213
+ </script>
214
+
215
+ <div class={cn('sx-textarea-wrapper', disabled && 'sx-textarea-disabled-wrap', className)}>
216
+ <label
217
+ for={id}
218
+ class={cn('sx-textarea-label', hideLabel && 'sx-sr-only')}
219
+ >
220
+ {label}
221
+ {#if required}
222
+ <span class="sx-textarea-required" aria-hidden="true">*</span>
223
+ {/if}
224
+ </label>
225
+
226
+ <div
227
+ bind:this={wrapperEl}
228
+ class={cn(
229
+ 'sx-textarea-field-wrapper',
230
+ `sx-textarea-${size}`,
231
+ isFocused && 'sx-textarea-focused',
232
+ error && 'sx-textarea-error-state',
233
+ successMessage && !error && 'sx-textarea-success-state',
234
+ disabled && 'sx-textarea-disabled',
235
+ showHandle && 'sx-textarea-resizable',
236
+ isDragging && 'sx-textarea-dragging'
237
+ )}
238
+ >
239
+ <textarea
240
+ bind:this={textareaEl}
241
+ bind:value
242
+ {id}
243
+ {name}
244
+ {placeholder}
245
+ {required}
246
+ {disabled}
247
+ {readonly}
248
+ {rows}
249
+ maxlength={maxLength}
250
+ minlength={minLength}
251
+ class={cn('sx-textarea-input', autoResize && 'sx-textarea-autoresize')}
252
+ aria-required={required || undefined}
253
+ aria-invalid={error ? 'true' : undefined}
254
+ aria-describedby={describedBy}
255
+ data-testid={testId}
256
+ oninput={handleInput}
257
+ onfocus={handleFocus}
258
+ onblur={handleBlur}
259
+ ></textarea>
260
+
261
+ {#if showHandle}
262
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
263
+ <span
264
+ class={cn(
265
+ 'sx-textarea-handle',
266
+ `sx-textarea-handle-${effectiveResize}`,
267
+ isDragging && 'sx-textarea-handle-active'
268
+ )}
269
+ aria-hidden="true"
270
+ onpointerdown={onHandlePointerDown}
271
+ onpointermove={onHandlePointerMove}
272
+ onpointerup={onHandlePointerUp}
273
+ onpointercancel={onHandlePointerUp}
274
+ >
275
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
276
+ {#if effectiveResize === 'horizontal'}
277
+ <!-- Horizontal dots -->
278
+ <circle cx="2" cy="5" r="1" fill="currentColor" />
279
+ <circle cx="5" cy="5" r="1" fill="currentColor" />
280
+ <circle cx="8" cy="5" r="1" fill="currentColor" />
281
+ {:else if effectiveResize === 'vertical'}
282
+ <!-- Vertical dots -->
283
+ <circle cx="5" cy="2" r="1" fill="currentColor" />
284
+ <circle cx="5" cy="5" r="1" fill="currentColor" />
285
+ <circle cx="5" cy="8" r="1" fill="currentColor" />
286
+ {:else}
287
+ <!-- Diagonal grip (both) -->
288
+ <circle cx="8" cy="2" r="1" fill="currentColor" />
289
+ <circle cx="5" cy="5" r="1" fill="currentColor" />
290
+ <circle cx="8" cy="5" r="1" fill="currentColor" />
291
+ <circle cx="2" cy="8" r="1" fill="currentColor" />
292
+ <circle cx="5" cy="8" r="1" fill="currentColor" />
293
+ <circle cx="8" cy="8" r="1" fill="currentColor" />
294
+ {/if}
295
+ </svg>
296
+ </span>
297
+ {/if}
298
+ </div>
299
+
300
+ <div class="sx-textarea-footer">
301
+ <div class="sx-textarea-messages">
302
+ {#if error}
303
+ <p id={errorId} class="sx-textarea-error-msg" role="alert">
304
+ <svg class="sx-textarea-msg-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
305
+ <circle cx="12" cy="12" r="10" /><path d="M15 9l-6 6M9 9l6 6" />
306
+ </svg>
307
+ {error}
308
+ </p>
309
+ {:else if successMessage}
310
+ <p id={successId} class="sx-textarea-success-msg" aria-live="polite">
311
+ <svg class="sx-textarea-msg-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
312
+ <circle cx="12" cy="12" r="10" /><path d="M9 12l2 2 4-4" />
313
+ </svg>
314
+ {successMessage}
315
+ </p>
316
+ {:else if hint}
317
+ <p id={hintId} class="sx-textarea-hint">{hint}</p>
318
+ {/if}
319
+ </div>
320
+
321
+ {#if showCharCount}
322
+ <span
323
+ id={charCountId}
324
+ class={cn(
325
+ 'sx-textarea-charcount',
326
+ charWarning && 'sx-textarea-charcount-warning',
327
+ charExceeded && 'sx-textarea-charcount-exceeded'
328
+ )}
329
+ aria-live="polite"
330
+ >
331
+ {charCount}{maxLength ? `/${maxLength}` : ''}
332
+ </span>
333
+ {/if}
334
+ </div>
335
+ </div>
336
+
337
+ <style>
338
+ .sx-textarea-wrapper {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: var(--sx-space-1-5);
342
+ width: 100%;
343
+ }
344
+
345
+ /* Label */
346
+ .sx-textarea-label {
347
+ font-family: var(--sx-font-body);
348
+ font-size: var(--sx-text-sm);
349
+ font-weight: 600;
350
+ color: var(--sx-color-text);
351
+ line-height: var(--sx-leading-normal);
352
+ }
353
+
354
+ .sx-textarea-required {
355
+ color: var(--sx-color-red);
356
+ margin-left: var(--sx-space-0-5);
357
+ }
358
+
359
+ .sx-sr-only {
360
+ position: absolute;
361
+ width: 1px;
362
+ height: 1px;
363
+ padding: 0;
364
+ margin: -1px;
365
+ overflow: hidden;
366
+ clip: rect(0, 0, 0, 0);
367
+ white-space: nowrap;
368
+ border: 0;
369
+ }
370
+
371
+ /* Field wrapper */
372
+ .sx-textarea-field-wrapper {
373
+ position: relative;
374
+ display: flex;
375
+ flex-direction: column;
376
+ background: var(--sx-color-surface);
377
+ border: 1px solid var(--sx-color-border-strong);
378
+ border-radius: var(--sx-radius-md);
379
+ overflow: hidden;
380
+ transition:
381
+ border-color var(--sx-transition-fast),
382
+ box-shadow var(--sx-transition-fast);
383
+ }
384
+
385
+ .sx-textarea-resizable {
386
+ overflow: hidden;
387
+ }
388
+
389
+ .sx-textarea-field-wrapper:hover:not(.sx-textarea-disabled):not(.sx-textarea-focused):not(.sx-textarea-error-state) {
390
+ border-color: var(--sx-color-border-hover);
391
+ }
392
+
393
+ .sx-textarea-focused {
394
+ border-color: var(--sx-color-cyan);
395
+ box-shadow: 0 0 0 3px var(--sx-color-cyan-ring);
396
+ }
397
+
398
+ .sx-textarea-error-state {
399
+ border-color: var(--sx-color-red);
400
+ box-shadow: 0 0 0 3px var(--sx-color-red-ring);
401
+ }
402
+
403
+ .sx-textarea-success-state {
404
+ border-color: var(--sx-color-green);
405
+ }
406
+
407
+ .sx-textarea-disabled {
408
+ opacity: 0.5;
409
+ cursor: not-allowed;
410
+ background: var(--sx-color-surface-2);
411
+ }
412
+
413
+ .sx-textarea-dragging {
414
+ user-select: none;
415
+ }
416
+
417
+ /* Textarea input — always hide native resize */
418
+ .sx-textarea-input {
419
+ width: 100%;
420
+ flex: 1;
421
+ min-height: 0;
422
+ border: none;
423
+ background: transparent;
424
+ outline: none;
425
+ resize: none;
426
+ font-family: var(--sx-font-body);
427
+ color: var(--sx-color-text);
428
+ line-height: var(--sx-leading-relaxed);
429
+ }
430
+
431
+ .sx-textarea-input::placeholder {
432
+ color: var(--sx-color-text-disabled);
433
+ }
434
+
435
+ .sx-textarea-input:focus-visible {
436
+ box-shadow: none;
437
+ }
438
+
439
+ .sx-textarea-autoresize {
440
+ flex: none;
441
+ overflow-y: hidden;
442
+ }
443
+
444
+ .sx-textarea-input:disabled {
445
+ cursor: not-allowed;
446
+ }
447
+
448
+ /* Sizes */
449
+ .sx-textarea-sm .sx-textarea-input {
450
+ padding: var(--sx-space-2) var(--sx-space-3);
451
+ font-size: var(--sx-text-xs);
452
+ }
453
+
454
+ .sx-textarea-md .sx-textarea-input {
455
+ padding: var(--sx-space-3) var(--sx-space-4);
456
+ font-size: var(--sx-text-sm);
457
+ }
458
+
459
+ .sx-textarea-lg .sx-textarea-input {
460
+ padding: var(--sx-space-4) var(--sx-space-5);
461
+ font-size: var(--sx-text-base);
462
+ }
463
+
464
+ /* ── Custom resize handle ── */
465
+
466
+ .sx-textarea-handle {
467
+ position: absolute;
468
+ display: flex;
469
+ align-items: center;
470
+ justify-content: center;
471
+ color: var(--sx-color-text-disabled);
472
+ touch-action: none;
473
+ z-index: 1;
474
+ transition: color var(--sx-transition-fast), background var(--sx-transition-fast);
475
+ }
476
+
477
+ .sx-textarea-handle:hover,
478
+ .sx-textarea-handle-active {
479
+ color: var(--sx-color-text-secondary);
480
+ }
481
+
482
+ .sx-textarea-handle-active {
483
+ color: var(--sx-color-cyan);
484
+ }
485
+
486
+ /* Vertical: bar along the bottom edge */
487
+ .sx-textarea-handle-vertical {
488
+ bottom: 0;
489
+ left: 0;
490
+ right: 0;
491
+ height: 14px;
492
+ cursor: ns-resize;
493
+ border-radius: 0 0 var(--sx-radius-md) var(--sx-radius-md);
494
+ }
495
+
496
+ .sx-textarea-handle-vertical:hover,
497
+ .sx-textarea-handle-vertical.sx-textarea-handle-active {
498
+ background: var(--sx-color-surface-2);
499
+ }
500
+
501
+ /* Horizontal: bar along the right edge */
502
+ .sx-textarea-handle-horizontal {
503
+ top: 0;
504
+ bottom: 0;
505
+ right: 0;
506
+ width: 14px;
507
+ cursor: ew-resize;
508
+ border-radius: 0 var(--sx-radius-md) var(--sx-radius-md) 0;
509
+ }
510
+
511
+ .sx-textarea-handle-horizontal:hover,
512
+ .sx-textarea-handle-horizontal.sx-textarea-handle-active {
513
+ background: var(--sx-color-surface-2);
514
+ }
515
+
516
+ /* Both: corner grip at bottom-right */
517
+ .sx-textarea-handle-both {
518
+ bottom: 0;
519
+ right: 0;
520
+ width: 18px;
521
+ height: 18px;
522
+ cursor: nwse-resize;
523
+ border-radius: 0 0 calc(var(--sx-radius-md) - 1px) 0;
524
+ }
525
+
526
+ .sx-textarea-handle-both:hover,
527
+ .sx-textarea-handle-both.sx-textarea-handle-active {
528
+ background: var(--sx-color-surface-2);
529
+ }
530
+
531
+ /* Footer */
532
+ .sx-textarea-footer {
533
+ display: flex;
534
+ justify-content: space-between;
535
+ align-items: flex-start;
536
+ gap: var(--sx-space-4);
537
+ min-height: 0;
538
+ }
539
+
540
+ .sx-textarea-messages {
541
+ flex: 1;
542
+ min-width: 0;
543
+ }
544
+
545
+ .sx-textarea-error-msg,
546
+ .sx-textarea-success-msg,
547
+ .sx-textarea-hint {
548
+ margin: 0;
549
+ font-family: var(--sx-font-body);
550
+ font-size: var(--sx-text-xs);
551
+ line-height: var(--sx-leading-relaxed);
552
+ display: flex;
553
+ align-items: center;
554
+ gap: var(--sx-space-1);
555
+ }
556
+
557
+ .sx-textarea-error-msg {
558
+ color: var(--sx-color-red);
559
+ }
560
+
561
+ .sx-textarea-success-msg {
562
+ color: var(--sx-color-green);
563
+ }
564
+
565
+ .sx-textarea-hint {
566
+ color: var(--sx-color-text-secondary);
567
+ }
568
+
569
+ .sx-textarea-msg-icon {
570
+ flex-shrink: 0;
571
+ }
572
+
573
+ /* Character count */
574
+ .sx-textarea-charcount {
575
+ flex-shrink: 0;
576
+ font-family: var(--sx-font-mono);
577
+ font-size: var(--sx-text-xs);
578
+ color: var(--sx-color-text-secondary);
579
+ font-variant-numeric: tabular-nums;
580
+ }
581
+
582
+ .sx-textarea-charcount-warning {
583
+ color: var(--sx-color-gold);
584
+ }
585
+
586
+ .sx-textarea-charcount-exceeded {
587
+ color: var(--sx-color-red);
588
+ font-weight: 600;
589
+ }
590
+
591
+ /* Disabled wrapper */
592
+ .sx-textarea-disabled-wrap .sx-textarea-label {
593
+ opacity: 0.5;
594
+ }
595
+
596
+ @media (prefers-reduced-motion: reduce) {
597
+ .sx-textarea-field-wrapper {
598
+ transition: none;
599
+ }
600
+
601
+ .sx-textarea-handle {
602
+ transition: none;
603
+ }
604
+ }
605
+
606
+ @media (forced-colors: active) {
607
+ .sx-textarea-handle {
608
+ color: ButtonText;
609
+ }
610
+
611
+ .sx-textarea-handle-active {
612
+ color: Highlight;
613
+ }
614
+ }
615
+ </style>
@@ -0,0 +1,47 @@
1
+ type TextareaSize = 'sm' | 'md' | 'lg';
2
+ type ResizeMode = 'none' | 'vertical' | 'horizontal' | 'both';
3
+ interface Props {
4
+ id?: string;
5
+ name?: string;
6
+ label: string;
7
+ value?: string;
8
+ placeholder?: string;
9
+ required?: boolean;
10
+ disabled?: boolean;
11
+ readonly?: boolean;
12
+ error?: string;
13
+ hint?: string;
14
+ successMessage?: string;
15
+ maxLength?: number;
16
+ minLength?: number;
17
+ showCharCount?: boolean;
18
+ rows?: number;
19
+ minRows?: number;
20
+ maxRows?: number;
21
+ autoResize?: boolean;
22
+ /** Manual resize direction. Ignored when autoResize is true. */
23
+ resize?: ResizeMode;
24
+ size?: TextareaSize;
25
+ hideLabel?: boolean;
26
+ class?: string;
27
+ oninput?: (event: Event) => void;
28
+ onblur?: (event: FocusEvent) => void;
29
+ onfocus?: (event: FocusEvent) => void;
30
+ testId?: string;
31
+ }
32
+ /**
33
+ * Textarea
34
+ *
35
+ * Neo-Brutalist Dark — Multi-line text input with custom resize handle,
36
+ * auto-resize, character count, validation states, and full accessibility.
37
+ * Follows TextInput visual patterns.
38
+ *
39
+ * @example
40
+ * <Textarea label="Description" bind:value={desc} />
41
+ * <Textarea label="Bio" autoResize maxRows={8} showCharCount maxLength={500} />
42
+ * <Textarea label="Notes" resize="both" />
43
+ */
44
+ declare const Textarea: import("svelte").Component<Props, {}, "value">;
45
+ type Textarea = ReturnType<typeof Textarea>;
46
+ export default Textarea;
47
+ //# sourceMappingURL=Textarea.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Textarea.svelte.d.ts","sourceRoot":"","sources":["../../../src/forms/Textarea/Textarea.svelte.ts"],"names":[],"mappings":"AAOA,KAAK,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AACvC,KAAK,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,YAAY,GAAG,MAAM,CAAC;AAE9D,UAAU,KAAK;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gEAAgE;IAChE,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAuQD;;;;;;;;;;;GAWG;AACH,QAAA,MAAM,QAAQ,gDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default as Textarea } from './Textarea.svelte';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/forms/Textarea/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1 @@
1
+ export { default as Textarea } from './Textarea.svelte';