@mieweb/ui 0.11.0-dev.28 → 0.11.0-dev.29

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 (63) hide show
  1. package/MIGRATION.md +49 -0
  2. package/dist/chunk-2VISTX7O.js +802 -0
  3. package/dist/chunk-2VISTX7O.js.map +1 -0
  4. package/dist/{chunk-3XVJYGEE.js → chunk-43QBEF6N.js} +51 -18
  5. package/dist/chunk-43QBEF6N.js.map +1 -0
  6. package/dist/{chunk-J4IIHRMP.cjs → chunk-6GKIISNZ.cjs} +51 -18
  7. package/dist/chunk-6GKIISNZ.cjs.map +1 -0
  8. package/dist/{chunk-IEZXJWHJ.cjs → chunk-7XKM6V2K.cjs} +3 -3
  9. package/dist/{chunk-IEZXJWHJ.cjs.map → chunk-7XKM6V2K.cjs.map} +1 -1
  10. package/dist/{chunk-24MB5436.js → chunk-AK4LHJPC.js} +5 -2
  11. package/dist/chunk-AK4LHJPC.js.map +1 -0
  12. package/dist/chunk-AQUHEKXL.cjs +829 -0
  13. package/dist/chunk-AQUHEKXL.cjs.map +1 -0
  14. package/dist/{chunk-E63B5DB7.js → chunk-MBDT2MGR.js} +4 -4
  15. package/dist/{chunk-E63B5DB7.js.map → chunk-MBDT2MGR.js.map} +1 -1
  16. package/dist/{chunk-DXFP53KF.js → chunk-NIO3XVUV.js} +3 -3
  17. package/dist/{chunk-DXFP53KF.js.map → chunk-NIO3XVUV.js.map} +1 -1
  18. package/dist/{chunk-CXJWYWST.cjs → chunk-PRA2V5BP.cjs} +7 -7
  19. package/dist/{chunk-CXJWYWST.cjs.map → chunk-PRA2V5BP.cjs.map} +1 -1
  20. package/dist/{chunk-3E7C5N2G.js → chunk-SFSBYC46.js} +3 -3
  21. package/dist/{chunk-3E7C5N2G.js.map → chunk-SFSBYC46.js.map} +1 -1
  22. package/dist/{chunk-ZWKC4JML.cjs → chunk-ZIJMLRI3.cjs} +9 -9
  23. package/dist/{chunk-ZWKC4JML.cjs.map → chunk-ZIJMLRI3.cjs.map} +1 -1
  24. package/dist/{chunk-XL4XN4I6.cjs → chunk-ZLBNYCYT.cjs} +5 -2
  25. package/dist/chunk-ZLBNYCYT.cjs.map +1 -0
  26. package/dist/components/EmailEditor/index.cjs +16 -16
  27. package/dist/components/EmailEditor/index.js +7 -7
  28. package/dist/components/RecordHeader/index.cjs +4 -4
  29. package/dist/components/RecordHeader/index.js +2 -2
  30. package/dist/components/RichTextEditor/index.cjs +5 -5
  31. package/dist/components/RichTextEditor/index.d.cts +20 -0
  32. package/dist/components/RichTextEditor/index.d.ts +20 -0
  33. package/dist/components/RichTextEditor/index.js +2 -2
  34. package/dist/components/SuperChat/index.cjs +2 -2
  35. package/dist/components/SuperChat/index.js +2 -2
  36. package/dist/components/Textarea/index.cjs +22 -4
  37. package/dist/components/Textarea/index.d.cts +40 -14
  38. package/dist/components/Textarea/index.d.ts +40 -14
  39. package/dist/components/Textarea/index.js +15 -1
  40. package/dist/esheet.cjs +10 -5
  41. package/dist/esheet.cjs.map +1 -1
  42. package/dist/esheet.js +7 -2
  43. package/dist/esheet.js.map +1 -1
  44. package/dist/index.cjs +147 -151
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +113 -52
  47. package/dist/index.d.ts +113 -52
  48. package/dist/index.js +14 -37
  49. package/dist/index.js.map +1 -1
  50. package/dist/styles.css +1 -1
  51. package/dist/tailwind-preset.cjs +4 -4
  52. package/dist/tailwind-preset.js +1 -1
  53. package/dist/utils/index.cjs +9 -9
  54. package/dist/utils/index.js +1 -1
  55. package/package.json +1 -1
  56. package/dist/chunk-24MB5436.js.map +0 -1
  57. package/dist/chunk-3XVJYGEE.js.map +0 -1
  58. package/dist/chunk-BUN6RUJ5.cjs +0 -393
  59. package/dist/chunk-BUN6RUJ5.cjs.map +0 -1
  60. package/dist/chunk-CPG7SDON.js +0 -370
  61. package/dist/chunk-CPG7SDON.js.map +0 -1
  62. package/dist/chunk-J4IIHRMP.cjs.map +0 -1
  63. package/dist/chunk-XL4XN4I6.cjs.map +0 -1
@@ -0,0 +1,802 @@
1
+ import { RichTextEditor } from './chunk-43QBEF6N.js';
2
+ import { RequiredMark } from './chunk-3IMM2PM4.js';
3
+ import { cn } from './chunk-TQONQSJO.js';
4
+ import * as React2 from 'react';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+ import { cva } from 'class-variance-authority';
7
+
8
+ var TextAreaRich = React2.forwardRef(
9
+ ({ className, markdownShortcuts = true, defaultToolbar = "hidden", ...rest }, ref) => /* @__PURE__ */ jsx(
10
+ RichTextEditor,
11
+ {
12
+ ref,
13
+ chromeless: true,
14
+ blockMenu: true,
15
+ markdownShortcuts,
16
+ defaultToolbar,
17
+ className: cn(
18
+ // Textarea's chrome: border, radius, focus ring (on the wrapper via
19
+ // focus-within since the editable sits inside it).
20
+ "border-input rounded-lg border",
21
+ "transition-colors duration-200",
22
+ "focus-within:ring-ring focus-within:border-transparent focus-within:ring-2",
23
+ className
24
+ ),
25
+ ...rest
26
+ }
27
+ )
28
+ );
29
+ TextAreaRich.displayName = "TextAreaRich";
30
+ var textareaVariants = cva(
31
+ [
32
+ "w-full px-3 py-2",
33
+ "border border-input rounded-lg",
34
+ "bg-background text-foreground",
35
+ "placeholder:text-muted-foreground",
36
+ "transition-colors duration-200",
37
+ "focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent",
38
+ "disabled:cursor-not-allowed disabled:opacity-50",
39
+ "resize-y"
40
+ ],
41
+ {
42
+ variants: {
43
+ size: {
44
+ sm: "text-sm min-h-[60px]",
45
+ md: "text-base min-h-[80px]",
46
+ lg: "text-lg min-h-[100px]"
47
+ },
48
+ hasError: {
49
+ true: "border-destructive focus:ring-destructive",
50
+ false: ""
51
+ },
52
+ resize: {
53
+ none: "resize-none",
54
+ vertical: "resize-y",
55
+ horizontal: "resize-x",
56
+ both: "resize"
57
+ },
58
+ labelVariant: {
59
+ stacked: "",
60
+ floating: "peer placeholder:text-transparent"
61
+ },
62
+ autoResize: {
63
+ true: "",
64
+ false: ""
65
+ }
66
+ },
67
+ compoundVariants: [
68
+ // Floating labels reserve top padding so entered text always starts
69
+ // below the floated label, plus a little extra min-height.
70
+ {
71
+ labelVariant: "floating",
72
+ size: "sm",
73
+ className: "pt-5 min-h-[76px]"
74
+ },
75
+ {
76
+ labelVariant: "floating",
77
+ size: "md",
78
+ className: "pt-6 min-h-[96px]"
79
+ },
80
+ {
81
+ labelVariant: "floating",
82
+ size: "lg",
83
+ className: "pt-7 min-h-[120px]"
84
+ },
85
+ // Auto-growing textareas start at the same visual height as a
86
+ // single-line Input (h-8/h-10/h-12): min-height matches the Input
87
+ // size token and the vertical padding is tuned so one line of text
88
+ // plus padding plus borders lands exactly on that height.
89
+ {
90
+ autoResize: true,
91
+ labelVariant: "stacked",
92
+ size: "sm",
93
+ className: "min-h-8 py-[5px]"
94
+ },
95
+ {
96
+ autoResize: true,
97
+ labelVariant: "stacked",
98
+ size: "md",
99
+ className: "min-h-10 py-[7px]"
100
+ },
101
+ {
102
+ autoResize: true,
103
+ labelVariant: "stacked",
104
+ size: "lg",
105
+ className: "min-h-12 py-[9px]"
106
+ },
107
+ // Floating label + auto-grow: single line below the floated label.
108
+ {
109
+ autoResize: true,
110
+ labelVariant: "floating",
111
+ size: "sm",
112
+ className: "min-h-[50px]"
113
+ },
114
+ {
115
+ autoResize: true,
116
+ labelVariant: "floating",
117
+ size: "md",
118
+ className: "min-h-[58px]"
119
+ },
120
+ {
121
+ autoResize: true,
122
+ labelVariant: "floating",
123
+ size: "lg",
124
+ className: "min-h-[66px]"
125
+ }
126
+ ],
127
+ defaultVariants: {
128
+ size: "md",
129
+ hasError: false,
130
+ resize: "vertical",
131
+ labelVariant: "stacked",
132
+ autoResize: true
133
+ }
134
+ }
135
+ );
136
+ var floatingLabelVariants = cva(
137
+ [
138
+ "absolute start-3 pointer-events-none select-none",
139
+ "transition-all duration-200 ease-out"
140
+ ],
141
+ {
142
+ variants: {
143
+ size: {
144
+ sm: [
145
+ "text-sm top-3",
146
+ "peer-focus:top-1 peer-[:not(:placeholder-shown)]:top-1 peer-autofill:top-1",
147
+ "peer-focus:text-[0.65rem] peer-[:not(:placeholder-shown)]:text-[0.65rem] peer-autofill:text-[0.65rem]"
148
+ ],
149
+ md: [
150
+ "text-base top-4",
151
+ "peer-focus:top-1.5 peer-[:not(:placeholder-shown)]:top-1.5 peer-autofill:top-1.5",
152
+ "peer-focus:text-xs peer-[:not(:placeholder-shown)]:text-xs peer-autofill:text-xs"
153
+ ],
154
+ lg: [
155
+ "text-lg top-5",
156
+ "peer-focus:top-2 peer-[:not(:placeholder-shown)]:top-2 peer-autofill:top-2",
157
+ "peer-focus:text-sm peer-[:not(:placeholder-shown)]:text-sm peer-autofill:text-sm"
158
+ ]
159
+ },
160
+ hasError: {
161
+ true: "text-destructive",
162
+ false: "text-muted-foreground"
163
+ }
164
+ },
165
+ defaultVariants: {
166
+ size: "md",
167
+ hasError: false
168
+ }
169
+ }
170
+ );
171
+ var TextAreaPlain = React2.forwardRef(
172
+ ({
173
+ className,
174
+ size,
175
+ hasError,
176
+ resize,
177
+ label,
178
+ labelVariant = "stacked",
179
+ hideLabel,
180
+ error,
181
+ helperText,
182
+ maxLength,
183
+ showCount = false,
184
+ autoResize = true,
185
+ required = false,
186
+ requiredVariant,
187
+ id,
188
+ rows,
189
+ value,
190
+ defaultValue,
191
+ onChange,
192
+ placeholder,
193
+ "aria-describedby": ariaDescribedBy,
194
+ ...props
195
+ }, ref) => {
196
+ const internalRef = React2.useRef(null);
197
+ const [internalValue, setInternalValue] = React2.useState(
198
+ defaultValue || ""
199
+ );
200
+ const generatedId = React2.useId();
201
+ const textareaId = id || generatedId;
202
+ const errorId = `${textareaId}-error`;
203
+ const helperId = `${textareaId}-helper`;
204
+ const countId = `${textareaId}-count`;
205
+ React2.useImperativeHandle(ref, () => internalRef.current);
206
+ const currentValue = value !== void 0 ? String(value) : internalValue;
207
+ const characterCount = currentValue.length;
208
+ const userFloorRef = React2.useRef(null);
209
+ const expectedHeightRef = React2.useRef(null);
210
+ const initialFloorCapturedRef = React2.useRef(false);
211
+ const adjustHeight = React2.useCallback(() => {
212
+ const textarea = internalRef.current;
213
+ if (textarea && autoResize) {
214
+ textarea.style.height = "auto";
215
+ const borders = textarea.offsetHeight - textarea.clientHeight;
216
+ const next = Math.max(
217
+ textarea.scrollHeight + borders,
218
+ userFloorRef.current ?? 0
219
+ );
220
+ textarea.style.height = `${next}px`;
221
+ expectedHeightRef.current = textarea.offsetHeight || next;
222
+ }
223
+ }, [autoResize]);
224
+ React2.useEffect(() => {
225
+ const textarea = internalRef.current;
226
+ if (textarea && autoResize && !initialFloorCapturedRef.current) {
227
+ initialFloorCapturedRef.current = true;
228
+ const configured = textarea.offsetHeight;
229
+ adjustHeight();
230
+ if (configured - textarea.offsetHeight > 1) {
231
+ userFloorRef.current = configured;
232
+ textarea.style.height = `${configured}px`;
233
+ expectedHeightRef.current = textarea.offsetHeight || configured;
234
+ }
235
+ return;
236
+ }
237
+ adjustHeight();
238
+ }, [currentValue, adjustHeight, autoResize]);
239
+ React2.useEffect(() => {
240
+ const textarea = internalRef.current;
241
+ if (!autoResize || !textarea || typeof ResizeObserver === "undefined") {
242
+ return;
243
+ }
244
+ let lastWidth = textarea.offsetWidth;
245
+ const observer = new ResizeObserver(() => {
246
+ const expected = expectedHeightRef.current;
247
+ const height = textarea.offsetHeight;
248
+ if (expected !== null && Math.abs(height - expected) > 1) {
249
+ userFloorRef.current = height;
250
+ expectedHeightRef.current = height;
251
+ }
252
+ if (textarea.offsetWidth !== lastWidth) {
253
+ lastWidth = textarea.offsetWidth;
254
+ adjustHeight();
255
+ }
256
+ });
257
+ observer.observe(textarea);
258
+ return () => observer.disconnect();
259
+ }, [autoResize, adjustHeight]);
260
+ const handleChange = React2.useCallback(
261
+ (e) => {
262
+ if (value === void 0) {
263
+ setInternalValue(e.target.value);
264
+ }
265
+ onChange?.(e);
266
+ adjustHeight();
267
+ },
268
+ [value, onChange, adjustHeight]
269
+ );
270
+ const describedByIds = [
271
+ error ? errorId : null,
272
+ helperText && !error ? helperId : null,
273
+ showCount ? countId : null,
274
+ ariaDescribedBy
275
+ ].filter(Boolean).join(" ");
276
+ const isFloating = labelVariant === "floating" && !!label && !hideLabel;
277
+ const requiredMark = required && /* @__PURE__ */ jsx(RequiredMark, { variant: requiredVariant });
278
+ const textareaElement = /* @__PURE__ */ jsx(
279
+ "textarea",
280
+ {
281
+ "data-slot": "textarea",
282
+ "data-auto-resize": autoResize ? "true" : void 0,
283
+ ref: internalRef,
284
+ id: textareaId,
285
+ value,
286
+ defaultValue: value === void 0 ? defaultValue : void 0,
287
+ onChange: handleChange,
288
+ maxLength,
289
+ "aria-invalid": hasError || !!error,
290
+ "aria-describedby": describedByIds || void 0,
291
+ required,
292
+ rows: autoResize ? rows ?? 1 : rows,
293
+ placeholder: isFloating ? " " : placeholder,
294
+ className: cn(
295
+ textareaVariants({
296
+ size,
297
+ hasError: hasError || !!error,
298
+ resize,
299
+ labelVariant: isFloating ? "floating" : "stacked",
300
+ autoResize
301
+ }),
302
+ className
303
+ ),
304
+ ...props
305
+ }
306
+ );
307
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "textarea-wrapper", className: "flex flex-col gap-1.5", children: [
308
+ label && !isFloating && /* @__PURE__ */ jsxs(
309
+ "label",
310
+ {
311
+ "data-slot": "textarea-label",
312
+ htmlFor: textareaId,
313
+ className: cn(
314
+ "text-foreground text-sm font-medium",
315
+ hideLabel && "sr-only"
316
+ ),
317
+ children: [
318
+ label,
319
+ requiredMark
320
+ ]
321
+ }
322
+ ),
323
+ isFloating ? /* @__PURE__ */ jsxs("div", { className: "relative", children: [
324
+ textareaElement,
325
+ /* @__PURE__ */ jsxs(
326
+ "label",
327
+ {
328
+ "data-slot": "textarea-label",
329
+ htmlFor: textareaId,
330
+ className: floatingLabelVariants({
331
+ size,
332
+ hasError: hasError || !!error
333
+ }),
334
+ children: [
335
+ label,
336
+ requiredMark
337
+ ]
338
+ }
339
+ )
340
+ ] }) : textareaElement,
341
+ /* @__PURE__ */ jsxs(
342
+ "div",
343
+ {
344
+ "data-slot": "textarea-footer",
345
+ className: "flex items-center justify-between gap-2",
346
+ children: [
347
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
348
+ error && /* @__PURE__ */ jsx(
349
+ "p",
350
+ {
351
+ id: errorId,
352
+ "data-slot": "textarea-error",
353
+ className: "text-destructive-700 dark:text-destructive-400 text-sm",
354
+ role: "alert",
355
+ children: error
356
+ }
357
+ ),
358
+ helperText && !error && /* @__PURE__ */ jsx(
359
+ "p",
360
+ {
361
+ id: helperId,
362
+ "data-slot": "textarea-helper",
363
+ className: "text-muted-foreground text-sm",
364
+ children: helperText
365
+ }
366
+ )
367
+ ] }),
368
+ showCount && /* @__PURE__ */ jsxs(
369
+ "p",
370
+ {
371
+ id: countId,
372
+ "data-slot": "textarea-count",
373
+ className: cn(
374
+ "text-muted-foreground shrink-0 text-xs",
375
+ maxLength && characterCount >= maxLength && "text-destructive"
376
+ ),
377
+ children: [
378
+ characterCount,
379
+ maxLength && `/${maxLength}`
380
+ ]
381
+ }
382
+ )
383
+ ]
384
+ }
385
+ )
386
+ ] });
387
+ }
388
+ );
389
+ TextAreaPlain.displayName = "TextAreaPlain";
390
+ function escapeHtml(text) {
391
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
392
+ }
393
+ function normalizeNewlines(text) {
394
+ return text.replace(/\r\n?/g, "\n");
395
+ }
396
+ function plainToHtml(text) {
397
+ if (!text) return "";
398
+ return normalizeNewlines(text).split("\n").map((line) => line ? `<p>${escapeHtml(line)}</p>` : "<p><br></p>").join("");
399
+ }
400
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
401
+ "P",
402
+ "DIV",
403
+ "H1",
404
+ "H2",
405
+ "H3",
406
+ "H4",
407
+ "H5",
408
+ "H6",
409
+ "LI",
410
+ "UL",
411
+ "OL",
412
+ "BLOCKQUOTE",
413
+ "PRE",
414
+ "TABLE",
415
+ "CAPTION",
416
+ "THEAD",
417
+ "TBODY",
418
+ "TFOOT",
419
+ "TR"
420
+ ]);
421
+ var CELL_TAGS = /* @__PURE__ */ new Set(["TD", "TH"]);
422
+ var TABLE_STRUCTURE_TAGS = /* @__PURE__ */ new Set([
423
+ "TABLE",
424
+ "THEAD",
425
+ "TBODY",
426
+ "TFOOT",
427
+ "TR"
428
+ ]);
429
+ var IGNORED_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT"]);
430
+ function nodeListToText(nodes) {
431
+ let out = "";
432
+ let afterCell = false;
433
+ for (const node of nodes) {
434
+ if (node.nodeType === Node.TEXT_NODE) {
435
+ const text = node.textContent ?? "";
436
+ const parentTag = node.parentElement?.tagName;
437
+ if (!text.trim() && parentTag && TABLE_STRUCTURE_TAGS.has(parentTag)) {
438
+ continue;
439
+ }
440
+ out += text;
441
+ if (text.trim()) afterCell = false;
442
+ continue;
443
+ }
444
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
445
+ const el = node;
446
+ if (IGNORED_TAGS.has(el.tagName)) continue;
447
+ if (el.tagName === "BR") {
448
+ afterCell = false;
449
+ out += "\n";
450
+ continue;
451
+ }
452
+ if (CELL_TAGS.has(el.tagName)) {
453
+ if (afterCell) out += " ";
454
+ out += nodeListToText(el.childNodes);
455
+ afterCell = true;
456
+ continue;
457
+ }
458
+ afterCell = false;
459
+ if (BLOCK_TAGS.has(el.tagName)) {
460
+ if (out && !out.endsWith("\n")) out += "\n";
461
+ let inner = nodeListToText(el.childNodes);
462
+ if (inner === "\n") inner = "";
463
+ out += inner;
464
+ if (inner === "" || !inner.endsWith("\n")) out += "\n";
465
+ continue;
466
+ }
467
+ out += nodeListToText(el.childNodes);
468
+ }
469
+ return out;
470
+ }
471
+ function htmlToPlain(html) {
472
+ if (!html) return "";
473
+ const root = document.createElement("template");
474
+ root.innerHTML = html;
475
+ const text = nodeListToText(root.content.childNodes);
476
+ return text.endsWith("\n") ? text.slice(0, -1) : text;
477
+ }
478
+ function sanitizePastedHtml(html) {
479
+ return plainToHtml(htmlToPlain(html));
480
+ }
481
+ var warned = /* @__PURE__ */ new Set();
482
+ function warnOnce(key, message) {
483
+ if (process.env.NODE_ENV !== "production" && !warned.has(key)) {
484
+ warned.add(key);
485
+ console.warn(`[mieweb/ui Textarea] ${message}`);
486
+ }
487
+ }
488
+ var RichTextarea = React2.forwardRef(
489
+ ({
490
+ label,
491
+ labelVariant = "stacked",
492
+ hideLabel,
493
+ error,
494
+ helperText,
495
+ maxLength,
496
+ showCount,
497
+ autoResize = true,
498
+ requiredVariant,
499
+ size = "md",
500
+ hasError,
501
+ resize,
502
+ className,
503
+ value,
504
+ defaultValue,
505
+ onChange,
506
+ onBlur,
507
+ onFocus,
508
+ onKeyDown,
509
+ onKeyUp,
510
+ name,
511
+ form,
512
+ id,
513
+ rows,
514
+ required,
515
+ readOnly,
516
+ disabled,
517
+ placeholder,
518
+ "aria-describedby": ariaDescribedBy,
519
+ "aria-label": ariaLabel,
520
+ "aria-labelledby": ariaLabelledBy
521
+ }, ref) => {
522
+ warnOnce(
523
+ "rich-upgrade",
524
+ 'Since 0.12.0 Textarea renders a rich text surface (plain-text value contract unchanged). Watch for: CSS "textarea" selectors, E2E fill()/type(), and maxLength no longer blocking typing. Opt out per field with richText={false} or use TextAreaPlain. See MIGRATION.md.'
525
+ );
526
+ if (labelVariant === "floating") {
527
+ warnOnce(
528
+ "floating",
529
+ 'labelVariant="floating" is not supported by the rich surface; falling back to a stacked label. Use TextAreaPlain for floating labels.'
530
+ );
531
+ }
532
+ if (autoResize === false) {
533
+ warnOnce(
534
+ "autoResize",
535
+ "autoResize={false} is ignored; the rich surface always grows with content. Use TextAreaPlain for a fixed-height textarea."
536
+ );
537
+ }
538
+ if (resize !== void 0) {
539
+ warnOnce(
540
+ "resize",
541
+ "resize is ignored; the rich surface has no native resize handle. Use TextAreaPlain for drag-resizing."
542
+ );
543
+ }
544
+ if (maxLength !== void 0) {
545
+ warnOnce(
546
+ "maxLength",
547
+ "maxLength is not enforced while typing; the character count still reflects it. Enforce limits in onChange or use TextAreaPlain."
548
+ );
549
+ }
550
+ const reactId = React2.useId();
551
+ const errorId = `${reactId}-error`;
552
+ const helperId = `${reactId}-helper`;
553
+ const countId = `${reactId}-count`;
554
+ const controlledValue = value !== void 0 ? normalizeNewlines(String(value)) : void 0;
555
+ const [state, setState] = React2.useState(() => {
556
+ const plain = controlledValue ?? (defaultValue !== void 0 ? normalizeNewlines(String(defaultValue)) : "");
557
+ return { html: plainToHtml(plain), plain };
558
+ });
559
+ if (controlledValue !== void 0 && controlledValue !== state.plain) {
560
+ setState({ html: plainToHtml(controlledValue), plain: controlledValue });
561
+ }
562
+ const hiddenRef = React2.useRef(null);
563
+ const editorWrapperRef = React2.useRef(null);
564
+ React2.useEffect(() => {
565
+ const editable = editorWrapperRef.current?.querySelector(
566
+ '[role="textbox"]'
567
+ );
568
+ if (editable && htmlToPlain(editable.innerHTML) !== state.plain) {
569
+ editable.innerHTML = state.html;
570
+ }
571
+ });
572
+ const focusEditor = React2.useCallback(() => {
573
+ editorWrapperRef.current?.querySelector('[role="textbox"]')?.focus();
574
+ }, []);
575
+ React2.useImperativeHandle(ref, () => {
576
+ const node = hiddenRef.current;
577
+ node.focus = focusEditor;
578
+ node.blur = () => {
579
+ const editable = editorWrapperRef.current?.querySelector(
580
+ '[role="textbox"]'
581
+ );
582
+ if (document.activeElement === editable) editable?.blur();
583
+ };
584
+ return node;
585
+ }, [focusEditor]);
586
+ const [externalLabelId, setExternalLabelId] = React2.useState();
587
+ React2.useEffect(() => {
588
+ if (!id) return;
589
+ const externalLabel = document.querySelector(
590
+ `label[for="${globalThis.CSS.escape(id)}"]`
591
+ );
592
+ if (!externalLabel) return;
593
+ if (!externalLabel.id) externalLabel.id = `${reactId}-external-label`;
594
+ setExternalLabelId(externalLabel.id);
595
+ const handleClick = () => focusEditor();
596
+ externalLabel.addEventListener("click", handleClick);
597
+ return () => externalLabel.removeEventListener("click", handleClick);
598
+ }, [id, reactId, focusEditor]);
599
+ const retargetEvent = React2.useCallback(
600
+ (event) => {
601
+ const node = hiddenRef.current;
602
+ if (!node) return event;
603
+ const retargeted = Object.create(event);
604
+ Object.defineProperties(retargeted, {
605
+ target: { value: node, enumerable: true },
606
+ currentTarget: { value: node, enumerable: true },
607
+ stopPropagation: { value: () => event.stopPropagation() },
608
+ preventDefault: { value: () => event.preventDefault() }
609
+ });
610
+ return retargeted;
611
+ },
612
+ []
613
+ );
614
+ const focusStaysInside = (event) => {
615
+ const related = event.relatedTarget;
616
+ if (!related) return false;
617
+ if (event.currentTarget.contains(related)) return true;
618
+ const menuId = related.closest('[data-slot="dropdown-menu"]')?.querySelector('[role="menu"]')?.id;
619
+ return !!menuId && !!event.currentTarget.querySelector(
620
+ `[aria-controls="${globalThis.CSS.escape(menuId)}"]`
621
+ );
622
+ };
623
+ const handleHtmlChange = (html) => {
624
+ const plain = htmlToPlain(html);
625
+ setState({ html, plain });
626
+ const node = hiddenRef.current;
627
+ if (onChange && node) {
628
+ node.value = plain;
629
+ const nativeEvent = new Event("change");
630
+ onChange({
631
+ target: node,
632
+ currentTarget: node,
633
+ type: "change",
634
+ bubbles: false,
635
+ cancelable: false,
636
+ defaultPrevented: false,
637
+ eventPhase: 0,
638
+ isTrusted: false,
639
+ timeStamp: Date.now(),
640
+ nativeEvent,
641
+ preventDefault: () => {
642
+ },
643
+ stopPropagation: () => {
644
+ },
645
+ isDefaultPrevented: () => false,
646
+ isPropagationStopped: () => false,
647
+ persist: () => {
648
+ }
649
+ });
650
+ }
651
+ };
652
+ const showError = !!error;
653
+ const invalid = hasError || showError;
654
+ const describedByIds = [
655
+ showError ? errorId : null,
656
+ helperText && !showError ? helperId : null,
657
+ showCount ? countId : null,
658
+ ariaDescribedBy
659
+ ].filter(Boolean).join(" ") || void 0;
660
+ const characterCount = state.plain.length;
661
+ const lineHeightPx = size === "sm" ? 20 : size === "lg" ? 28 : 24;
662
+ const editorStyle = rows ? (
663
+ // 7px block padding each side, matching the editable's py-[7px].
664
+ { minHeight: `${rows * lineHeightPx + 14}px` }
665
+ ) : void 0;
666
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "textarea-wrapper", className: "flex flex-col gap-1.5", children: [
667
+ label && /* @__PURE__ */ jsxs(
668
+ "label",
669
+ {
670
+ "data-slot": "textarea-label",
671
+ className: cn(
672
+ "text-foreground text-sm font-medium",
673
+ hideLabel && "sr-only"
674
+ ),
675
+ children: [
676
+ label,
677
+ required && /* @__PURE__ */ jsx(RequiredMark, { variant: requiredVariant })
678
+ ]
679
+ }
680
+ ),
681
+ /* @__PURE__ */ jsx(
682
+ "div",
683
+ {
684
+ ref: editorWrapperRef,
685
+ className: "w-full",
686
+ onBlur: onBlur && ((event) => {
687
+ if (focusStaysInside(event)) return;
688
+ onBlur(retargetEvent(event));
689
+ }),
690
+ onFocus: onFocus && ((event) => {
691
+ if (focusStaysInside(event)) return;
692
+ onFocus(retargetEvent(event));
693
+ }),
694
+ onKeyDown: onKeyDown && ((event) => onKeyDown(retargetEvent(event))),
695
+ onKeyUp: onKeyUp && ((event) => onKeyUp(retargetEvent(event))),
696
+ children: /* @__PURE__ */ jsx(
697
+ TextAreaRich,
698
+ {
699
+ value: state.html,
700
+ onChange: handleHtmlChange,
701
+ placeholder,
702
+ disabled,
703
+ readOnly,
704
+ id,
705
+ sanitizeHtml: sanitizePastedHtml,
706
+ "aria-label": ariaLabel ?? label,
707
+ "aria-labelledby": ariaLabelledBy ?? externalLabelId,
708
+ "aria-describedby": describedByIds,
709
+ "aria-invalid": invalid || void 0,
710
+ "aria-required": required || void 0,
711
+ editorClassName: cn(
712
+ // A native textarea renders repeated spaces and tabs
713
+ // faithfully; normal HTML layout would collapse them.
714
+ "whitespace-pre-wrap",
715
+ size === "sm" && "text-sm",
716
+ size === "lg" && "text-lg"
717
+ ),
718
+ editorStyle,
719
+ className: cn(
720
+ size === "sm" && "text-sm",
721
+ size === "lg" && "text-lg",
722
+ invalid && "border-destructive focus-within:ring-destructive focus-within:border-destructive",
723
+ disabled && "cursor-not-allowed opacity-50",
724
+ className
725
+ )
726
+ }
727
+ )
728
+ }
729
+ ),
730
+ /* @__PURE__ */ jsx(
731
+ "textarea",
732
+ {
733
+ "data-slot": "textarea-mirror",
734
+ ref: hiddenRef,
735
+ hidden: true,
736
+ readOnly: true,
737
+ tabIndex: -1,
738
+ "aria-hidden": "true",
739
+ name,
740
+ form,
741
+ required,
742
+ disabled,
743
+ value: state.plain
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsxs(
747
+ "div",
748
+ {
749
+ "data-slot": "textarea-footer",
750
+ className: "flex items-center justify-between gap-2",
751
+ children: [
752
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
753
+ showError && /* @__PURE__ */ jsx(
754
+ "p",
755
+ {
756
+ id: errorId,
757
+ "data-slot": "textarea-error",
758
+ className: "text-destructive-700 dark:text-destructive-400 text-sm",
759
+ role: "alert",
760
+ children: error
761
+ }
762
+ ),
763
+ helperText && !showError && /* @__PURE__ */ jsx(
764
+ "p",
765
+ {
766
+ id: helperId,
767
+ "data-slot": "textarea-helper",
768
+ className: "text-muted-foreground text-sm",
769
+ children: helperText
770
+ }
771
+ )
772
+ ] }),
773
+ showCount && /* @__PURE__ */ jsxs(
774
+ "p",
775
+ {
776
+ id: countId,
777
+ "data-slot": "textarea-count",
778
+ className: cn(
779
+ "text-muted-foreground shrink-0 text-xs",
780
+ maxLength && characterCount >= maxLength && "text-destructive"
781
+ ),
782
+ children: [
783
+ characterCount,
784
+ maxLength && `/${maxLength}`
785
+ ]
786
+ }
787
+ )
788
+ ]
789
+ }
790
+ )
791
+ ] });
792
+ }
793
+ );
794
+ RichTextarea.displayName = "RichTextarea";
795
+ var Textarea = React2.forwardRef(
796
+ ({ richText = true, ...props }, ref) => richText ? /* @__PURE__ */ jsx(RichTextarea, { ref, ...props }) : /* @__PURE__ */ jsx(TextAreaPlain, { ref, ...props })
797
+ );
798
+ Textarea.displayName = "Textarea";
799
+
800
+ export { TextAreaPlain, TextAreaRich, Textarea, htmlToPlain, plainToHtml, textareaVariants };
801
+ //# sourceMappingURL=chunk-2VISTX7O.js.map
802
+ //# sourceMappingURL=chunk-2VISTX7O.js.map