@marianmeres/stuic 3.131.0 → 3.133.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.
@@ -0,0 +1,547 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLTextareaAttributes } from "svelte/elements";
4
+ import type { ValidateOptions } from "../../actions/validate.svelte.js";
5
+ import type { THC } from "../Thc/Thc.svelte";
6
+ import type { InputWrapClassProps } from "../Input/types.js";
7
+ import type { MarkdownRenderer } from "./_internal/render.js";
8
+
9
+ type SnippetWithId = Snippet<[{ id: string }]>;
10
+
11
+ /** Active tab of the comment box. */
12
+ export type CommentInputMode = "write" | "preview";
13
+
14
+ export interface Props
15
+ extends
16
+ Omit<HTMLTextareaAttributes, "value" | "class" | "style">,
17
+ InputWrapClassProps {
18
+ /** Comment text (raw markdown). Bindable. */
19
+ value?: string;
20
+ /** Active tab. Bindable. Default `"write"`. */
21
+ mode?: CommentInputMode;
22
+ /** The underlying `<textarea>` element. Bindable. */
23
+ input?: HTMLTextAreaElement;
24
+ /** Name attribute of the textarea, for native form submission. */
25
+ name?: string;
26
+ //
27
+ label?: SnippetWithId | THC;
28
+ description?: SnippetWithId | THC;
29
+ class?: string;
30
+ id?: string;
31
+ tabindex?: number;
32
+ renderSize?: "sm" | "md" | "lg" | string;
33
+ required?: boolean;
34
+ disabled?: boolean;
35
+ placeholder?: string;
36
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
37
+ /** Trim surrounding whitespace on blur. Default `true`. */
38
+ useTrim?: boolean;
39
+ /** Grow the textarea with its content (up to `max`). Default `true`. */
40
+ useAutogrow?: boolean | { enabled?: boolean; max?: number };
41
+ //
42
+ /** Show the Write/Preview tabs. Default `true`. Set `false` for a plain markdown textarea. */
43
+ showTabs?: boolean;
44
+ /** Label for the "write" tab. Default `"Write"`. */
45
+ writeLabel?: string;
46
+ /** Label for the "preview" tab. Default `"Preview"`. */
47
+ previewLabel?: string;
48
+ /** Show the subtle "Markdown supported" hint in the tab bar. Default `true`. */
49
+ showMarkdownHint?: boolean;
50
+ /** Text of the markdown hint. Default `"Markdown supported"`. */
51
+ markdownHintLabel?: string;
52
+ /** Placeholder shown in the Preview tab when there is nothing to render. */
53
+ previewEmptyLabel?: string;
54
+ /** Text shown in the Preview tab while the renderer is (lazy-)loading. */
55
+ previewLoadingLabel?: string;
56
+ /**
57
+ * Override the Preview renderer. Receives the raw markdown, returns (or
58
+ * resolves to) an HTML string that is rendered as-is — **you are responsible
59
+ * for sanitizing it**. When omitted, the bundled marked + DOMPurify pipeline
60
+ * is used (lazy-loaded; requires the optional `marked` / `dompurify` peers).
61
+ */
62
+ renderMarkdown?: MarkdownRenderer;
63
+ //
64
+ /**
65
+ * Submit handler. Receives the current value. May be async — the submit
66
+ * button shows a spinner and the box is disabled while it resolves.
67
+ */
68
+ onSubmit?: (value: string) => void | Promise<void>;
69
+ /** Cancel handler. When set, a Cancel button is shown by default. */
70
+ onCancel?: () => void;
71
+ /** Fired on every edit with the new value. */
72
+ onChange?: (value: string) => void;
73
+ /** Submit button label. Default `"Comment"`. */
74
+ submitLabel?: string;
75
+ /** Cancel button label. Default `"Cancel"`. */
76
+ cancelLabel?: string;
77
+ /** Show the submit button. Defaults to `true` when `onSubmit` is set. */
78
+ showSubmit?: boolean;
79
+ /** Show the cancel button. Defaults to `true` when `onCancel` is set. */
80
+ showCancel?: boolean;
81
+ /** Submit on ⌘/Ctrl+Enter while the textarea is focused. Default `true`. */
82
+ submitOnModEnter?: boolean;
83
+ /** Clear the value after a successful submit. Default `true`. */
84
+ clearOnSubmit?: boolean;
85
+ /** Disable the submit button when the value is empty/whitespace. Default `true`. */
86
+ submitDisabledWhenEmpty?: boolean;
87
+ /** Externally-controlled busy state (disables the box + submit). */
88
+ busy?: boolean;
89
+ //
90
+ labelAfter?: SnippetWithId | THC;
91
+ below?: SnippetWithId | THC;
92
+ labelLeft?: boolean;
93
+ labelLeftWidth?: "normal" | "wide";
94
+ labelLeftBreakpoint?: number;
95
+ //
96
+ /** Optional avatar/gutter rendered to the left of the box. */
97
+ avatar?: SnippetWithId | THC;
98
+ /** Extra content rendered in the footer, left of the buttons (e.g. attach). */
99
+ footer?: Snippet;
100
+ //
101
+ /** Classes for the `<textarea>` element. */
102
+ classInput?: string;
103
+ /** Classes for the tab bar. */
104
+ classBar?: string;
105
+ /** Classes for the preview panel. */
106
+ classPreview?: string;
107
+ /** Classes for the footer (button row). */
108
+ classFooter?: string;
109
+ style?: string;
110
+ }
111
+ </script>
112
+
113
+ <script lang="ts">
114
+ import { autogrow } from "../../actions/autogrow.svelte.js";
115
+ import { trim } from "../../actions/trim.svelte.js";
116
+ import {
117
+ validate as validateAction,
118
+ type ValidationResult,
119
+ } from "../../actions/validate.svelte.js";
120
+ import { getId } from "../../utils/get-id.js";
121
+ import { twMerge } from "../../utils/tw-merge.js";
122
+ import { Button } from "../Button/index.js";
123
+ import Thc from "../Thc/Thc.svelte";
124
+ import InputWrap from "../Input/_internal/InputWrap.svelte";
125
+ import { renderMarkdownSafe } from "./_internal/render.js";
126
+
127
+ let {
128
+ value = $bindable(""),
129
+ mode = $bindable("write"),
130
+ input = $bindable(),
131
+ name,
132
+ //
133
+ label = "",
134
+ description,
135
+ class: classProp,
136
+ id = getId(),
137
+ tabindex = 0,
138
+ renderSize = "md",
139
+ required = false,
140
+ disabled = false,
141
+ placeholder,
142
+ // Renamed to avoid collision with the exported `validate()` below.
143
+ validate: validateProp,
144
+ useTrim = true,
145
+ useAutogrow = true,
146
+ //
147
+ showTabs = true,
148
+ writeLabel = "Write",
149
+ previewLabel = "Preview",
150
+ showMarkdownHint = true,
151
+ markdownHintLabel = "Markdown supported",
152
+ previewEmptyLabel = "Nothing to preview",
153
+ previewLoadingLabel = "Loading preview…",
154
+ renderMarkdown,
155
+ //
156
+ onSubmit,
157
+ onCancel,
158
+ onChange,
159
+ submitLabel = "Comment",
160
+ cancelLabel = "Cancel",
161
+ showSubmit,
162
+ showCancel,
163
+ submitOnModEnter = true,
164
+ clearOnSubmit = true,
165
+ submitDisabledWhenEmpty = true,
166
+ busy = false,
167
+ //
168
+ labelAfter,
169
+ below,
170
+ labelLeft = false,
171
+ labelLeftWidth = "normal",
172
+ labelLeftBreakpoint = 480,
173
+ //
174
+ avatar,
175
+ footer,
176
+ //
177
+ classInput,
178
+ classBar,
179
+ classPreview,
180
+ classFooter,
181
+ classLabel,
182
+ classLabelBox,
183
+ classInputBox,
184
+ classInputBoxWrap,
185
+ classInputBoxWrapInvalid,
186
+ classDescBox,
187
+ classDescBoxToggle,
188
+ classBelowBox,
189
+ classValidationBox,
190
+ style,
191
+ //
192
+ ...rest
193
+ }: Props = $props();
194
+
195
+ // --- Validation (mirrors FieldTextarea: action lives on the real textarea) ---
196
+ let validation: ValidationResult | undefined = $state();
197
+ const setValidationResult = (res: ValidationResult) => (validation = res);
198
+ let _doValidate: (() => void) | undefined = $state();
199
+
200
+ // --- Submit state ---------------------------------------------------------
201
+ let submitting = $state(false);
202
+ const isEmpty = $derived(!(value ?? "").trim());
203
+ // Resolve the show* defaults from the presence of their handlers.
204
+ const _showSubmit = $derived(showSubmit ?? !!onSubmit);
205
+ const _showCancel = $derived(showCancel ?? !!onCancel);
206
+ const _busy = $derived(disabled || busy || submitting);
207
+ const canSubmit = $derived(!_busy && !(submitDisabledWhenEmpty && isEmpty));
208
+
209
+ // --- Preview rendering ----------------------------------------------------
210
+ let previewHtml = $state("");
211
+ let previewError = $state(false);
212
+ let previewLoading = $state(false);
213
+
214
+ $effect(() => {
215
+ if (mode !== "preview" || !showTabs) return;
216
+ const md = value ?? "";
217
+ if (!md.trim()) {
218
+ previewHtml = "";
219
+ previewError = false;
220
+ previewLoading = false;
221
+ return;
222
+ }
223
+ const render = renderMarkdown ?? renderMarkdownSafe;
224
+ let cancelled = false;
225
+ previewLoading = true;
226
+ // Drop the previous value's HTML so the loading placeholder shows instead of
227
+ // briefly rendering content that no longer matches `value`.
228
+ previewHtml = "";
229
+ previewError = false;
230
+ (async () => {
231
+ try {
232
+ const html = await render(md);
233
+ if (cancelled) return;
234
+ previewHtml = html;
235
+ previewError = false;
236
+ } catch (err) {
237
+ if (cancelled) return;
238
+ previewHtml = "";
239
+ previewError = true;
240
+ // eslint-disable-next-line no-console
241
+ console.warn(
242
+ "[CommentInput] preview renderer failed. When using the default " +
243
+ "renderer, make sure the optional peers `marked` and `dompurify` " +
244
+ "are installed, or pass a `renderMarkdown` function.",
245
+ err
246
+ );
247
+ } finally {
248
+ if (!cancelled) previewLoading = false;
249
+ }
250
+ })();
251
+ return () => {
252
+ cancelled = true;
253
+ };
254
+ });
255
+
256
+ // Returning to the Write tab: re-measure the textarea. autogrow can't size a
257
+ // `display:none` element, so an external value change made while previewing
258
+ // would otherwise leave the textarea at a stale height.
259
+ $effect(() => {
260
+ if (mode !== "write" || !input || !useAutogrow) return;
261
+ const el = input;
262
+ const max =
263
+ typeof useAutogrow === "object" && useAutogrow.max ? useAutogrow.max : 250;
264
+ requestAnimationFrame(() => {
265
+ el.style.height = "auto";
266
+ const cs = getComputedStyle(el);
267
+ const borderY =
268
+ cs.boxSizing === "border-box"
269
+ ? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth)
270
+ : 0;
271
+ el.style.height = Math.min(el.scrollHeight + borderY, max) + "px";
272
+ });
273
+ });
274
+
275
+ // --- Handlers -------------------------------------------------------------
276
+ async function handleSubmit() {
277
+ if (!canSubmit) return;
278
+ // Validate before invoking the handler — the submit button bypasses any
279
+ // form-level submit check, so enforce it here (surfaces the inline message).
280
+ const res = validate();
281
+ if (res && !res.valid) {
282
+ mode = "write";
283
+ return;
284
+ }
285
+ try {
286
+ submitting = true;
287
+ await onSubmit?.(value ?? "");
288
+ if (clearOnSubmit) {
289
+ value = "";
290
+ // Back to Write so the (now empty) box is editable again.
291
+ if (mode === "preview") mode = "write";
292
+ validation = undefined;
293
+ input?.setCustomValidity?.("");
294
+ }
295
+ } finally {
296
+ submitting = false;
297
+ }
298
+ }
299
+
300
+ function handleCancel() {
301
+ onCancel?.();
302
+ }
303
+
304
+ function handleKeydown(e: KeyboardEvent) {
305
+ if (submitOnModEnter && (e.metaKey || e.ctrlKey) && e.key === "Enter") {
306
+ e.preventDefault();
307
+ handleSubmit();
308
+ }
309
+ }
310
+
311
+ function setMode(next: CommentInputMode) {
312
+ if (disabled) return;
313
+ mode = next;
314
+ }
315
+
316
+ // Roving-tabindex keyboard nav for the Write/Preview tablist (automatic
317
+ // activation — Arrow/Home/End move selection and focus together).
318
+ function handleTabKeydown(e: KeyboardEvent) {
319
+ const keys = ["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp", "Home", "End"];
320
+ if (!keys.includes(e.key)) return;
321
+ e.preventDefault();
322
+ const next: CommentInputMode =
323
+ e.key === "Home"
324
+ ? "write"
325
+ : e.key === "End"
326
+ ? "preview"
327
+ : mode === "write"
328
+ ? "preview"
329
+ : "write";
330
+ setMode(next);
331
+ if (disabled) return;
332
+ document.getElementById(`${id}-tab-${next}`)?.focus();
333
+ }
334
+
335
+ // --- Imperative API (mirrors FieldTextarea) -------------------------------
336
+ /** Trigger validation now. Renders the inline message if invalid. */
337
+ export function validate(): ValidationResult | undefined {
338
+ _doValidate?.();
339
+ return validation;
340
+ }
341
+ /** Clear the inline validation message and reset `setCustomValidity`. */
342
+ export function clearValidation(): void {
343
+ validation = undefined;
344
+ input?.setCustomValidity?.("");
345
+ }
346
+ /** Current validation state, or undefined if the validator has never run. */
347
+ export function getValidation(): ValidationResult | undefined {
348
+ return validation;
349
+ }
350
+ /** Focus the underlying `<textarea>` (switches to the Write tab first). */
351
+ export function focus(): void {
352
+ mode = "write";
353
+ input?.focus?.();
354
+ }
355
+ /** Scroll the field into view. Defaults to smooth + center. */
356
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
357
+ input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
358
+ }
359
+ /** Programmatically run the submit flow (respects the empty/busy guards). */
360
+ export function submit(): void {
361
+ handleSubmit();
362
+ }
363
+ </script>
364
+
365
+ {#snippet thcOrSnippet(value?: SnippetWithId | THC)}
366
+ {#if typeof value === "function"}
367
+ {@render value({ id })}
368
+ {:else if value}
369
+ <Thc thc={value} forceAsHtml />
370
+ {/if}
371
+ {/snippet}
372
+
373
+ <InputWrap
374
+ {description}
375
+ class={classProp}
376
+ size={renderSize}
377
+ {id}
378
+ {label}
379
+ {labelAfter}
380
+ {below}
381
+ {required}
382
+ {disabled}
383
+ {labelLeft}
384
+ {labelLeftWidth}
385
+ {labelLeftBreakpoint}
386
+ {classLabel}
387
+ {classLabelBox}
388
+ {classInputBox}
389
+ {classInputBoxWrap}
390
+ {classInputBoxWrapInvalid}
391
+ {classDescBox}
392
+ {classDescBoxToggle}
393
+ {classBelowBox}
394
+ {classValidationBox}
395
+ {validation}
396
+ {style}
397
+ >
398
+ <div class="stuic-comment-input" data-size={renderSize} class:disabled>
399
+ {#if avatar}
400
+ <div class="stuic-comment-input-avatar">
401
+ {@render thcOrSnippet(avatar)}
402
+ </div>
403
+ {/if}
404
+
405
+ <div class="stuic-comment-input-editor" data-mode={mode}>
406
+ {#if showTabs || showMarkdownHint}
407
+ <div class={twMerge("stuic-comment-input-bar", classBar)}>
408
+ {#if showTabs}
409
+ <div
410
+ class="stuic-comment-input-tabs"
411
+ role="tablist"
412
+ aria-label="Comment editor"
413
+ >
414
+ <button
415
+ id={`${id}-tab-write`}
416
+ type="button"
417
+ role="tab"
418
+ aria-selected={mode === "write"}
419
+ aria-controls={`${id}-panel-write`}
420
+ tabindex={mode === "write" ? 0 : -1}
421
+ data-active={mode === "write" ? "true" : undefined}
422
+ {disabled}
423
+ onclick={() => setMode("write")}
424
+ onkeydown={handleTabKeydown}
425
+ >
426
+ {writeLabel}
427
+ </button>
428
+ <button
429
+ id={`${id}-tab-preview`}
430
+ type="button"
431
+ role="tab"
432
+ aria-selected={mode === "preview"}
433
+ aria-controls={`${id}-panel-preview`}
434
+ tabindex={mode === "preview" ? 0 : -1}
435
+ data-active={mode === "preview" ? "true" : undefined}
436
+ {disabled}
437
+ onclick={() => setMode("preview")}
438
+ onkeydown={handleTabKeydown}
439
+ >
440
+ {previewLabel}
441
+ </button>
442
+ </div>
443
+ {/if}
444
+ {#if showMarkdownHint && mode === "write"}
445
+ <span class="stuic-comment-input-hint">{markdownHintLabel}</span>
446
+ {/if}
447
+ </div>
448
+ {/if}
449
+
450
+ <div class="stuic-comment-input-surface">
451
+ <div
452
+ class="stuic-comment-input-panel"
453
+ id={`${id}-panel-write`}
454
+ role={showTabs ? "tabpanel" : undefined}
455
+ aria-labelledby={showTabs ? `${id}-tab-write` : undefined}
456
+ hidden={showTabs && mode === "preview"}
457
+ >
458
+ <textarea
459
+ bind:value
460
+ bind:this={input}
461
+ {...rest}
462
+ {id}
463
+ {name}
464
+ {placeholder}
465
+ {tabindex}
466
+ {required}
467
+ disabled={_busy}
468
+ class={twMerge("stuic-comment-input-textarea", classInput)}
469
+ oninput={(e) => onChange?.(e.currentTarget.value)}
470
+ onkeydown={handleKeydown}
471
+ use:trim={() => ({
472
+ enabled: useTrim,
473
+ setValue: (v: string) => (value = v),
474
+ })}
475
+ use:validateAction={() => ({
476
+ enabled: validateProp !== false,
477
+ ...(typeof validateProp === "boolean" ? {} : validateProp),
478
+ setValidationResult,
479
+ setDoValidate: (fn) => (_doValidate = fn),
480
+ })}
481
+ use:autogrow={() => ({
482
+ enabled: !!useAutogrow,
483
+ ...(typeof useAutogrow === "boolean" ? {} : useAutogrow),
484
+ value,
485
+ })}
486
+ ></textarea>
487
+ </div>
488
+
489
+ {#if showTabs && mode === "preview"}
490
+ <div
491
+ class={twMerge(
492
+ "stuic-comment-input-panel stuic-comment-input-preview",
493
+ classPreview
494
+ )}
495
+ id={`${id}-panel-preview`}
496
+ role="tabpanel"
497
+ aria-labelledby={`${id}-tab-preview`}
498
+ >
499
+ {#if previewError}
500
+ <p class="stuic-comment-input-preview-empty">Preview unavailable.</p>
501
+ {:else if !(value ?? "").trim()}
502
+ <p class="stuic-comment-input-preview-empty">{previewEmptyLabel}</p>
503
+ {:else if previewLoading && !previewHtml}
504
+ <p class="stuic-comment-input-preview-empty">{previewLoadingLabel}</p>
505
+ {:else}
506
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
507
+ {@html previewHtml}
508
+ {/if}
509
+ </div>
510
+ {/if}
511
+ </div>
512
+
513
+ {#if _showSubmit || _showCancel || footer}
514
+ <div class={twMerge("stuic-comment-input-footer", classFooter)}>
515
+ <div class="stuic-comment-input-footer-start">
516
+ {@render footer?.()}
517
+ </div>
518
+ <div class="stuic-comment-input-footer-end">
519
+ {#if _showCancel}
520
+ <Button
521
+ type="button"
522
+ variant="ghost"
523
+ size={renderSize}
524
+ disabled={submitting}
525
+ onclick={handleCancel}
526
+ >
527
+ {cancelLabel}
528
+ </Button>
529
+ {/if}
530
+ {#if _showSubmit}
531
+ <Button
532
+ type="button"
533
+ intent="primary"
534
+ size={renderSize}
535
+ disabled={!canSubmit}
536
+ spinner={submitting}
537
+ onclick={handleSubmit}
538
+ >
539
+ {submitLabel}
540
+ </Button>
541
+ {/if}
542
+ </div>
543
+ </div>
544
+ {/if}
545
+ </div>
546
+ </div>
547
+ </InputWrap>
@@ -0,0 +1,113 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLTextareaAttributes } from "svelte/elements";
3
+ import type { ValidateOptions } from "../../actions/validate.svelte.js";
4
+ import type { THC } from "../Thc/Thc.svelte";
5
+ import type { InputWrapClassProps } from "../Input/types.js";
6
+ import type { MarkdownRenderer } from "./_internal/render.js";
7
+ type SnippetWithId = Snippet<[{
8
+ id: string;
9
+ }]>;
10
+ /** Active tab of the comment box. */
11
+ export type CommentInputMode = "write" | "preview";
12
+ export interface Props extends Omit<HTMLTextareaAttributes, "value" | "class" | "style">, InputWrapClassProps {
13
+ /** Comment text (raw markdown). Bindable. */
14
+ value?: string;
15
+ /** Active tab. Bindable. Default `"write"`. */
16
+ mode?: CommentInputMode;
17
+ /** The underlying `<textarea>` element. Bindable. */
18
+ input?: HTMLTextAreaElement;
19
+ /** Name attribute of the textarea, for native form submission. */
20
+ name?: string;
21
+ label?: SnippetWithId | THC;
22
+ description?: SnippetWithId | THC;
23
+ class?: string;
24
+ id?: string;
25
+ tabindex?: number;
26
+ renderSize?: "sm" | "md" | "lg" | string;
27
+ required?: boolean;
28
+ disabled?: boolean;
29
+ placeholder?: string;
30
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
31
+ /** Trim surrounding whitespace on blur. Default `true`. */
32
+ useTrim?: boolean;
33
+ /** Grow the textarea with its content (up to `max`). Default `true`. */
34
+ useAutogrow?: boolean | {
35
+ enabled?: boolean;
36
+ max?: number;
37
+ };
38
+ /** Show the Write/Preview tabs. Default `true`. Set `false` for a plain markdown textarea. */
39
+ showTabs?: boolean;
40
+ /** Label for the "write" tab. Default `"Write"`. */
41
+ writeLabel?: string;
42
+ /** Label for the "preview" tab. Default `"Preview"`. */
43
+ previewLabel?: string;
44
+ /** Show the subtle "Markdown supported" hint in the tab bar. Default `true`. */
45
+ showMarkdownHint?: boolean;
46
+ /** Text of the markdown hint. Default `"Markdown supported"`. */
47
+ markdownHintLabel?: string;
48
+ /** Placeholder shown in the Preview tab when there is nothing to render. */
49
+ previewEmptyLabel?: string;
50
+ /** Text shown in the Preview tab while the renderer is (lazy-)loading. */
51
+ previewLoadingLabel?: string;
52
+ /**
53
+ * Override the Preview renderer. Receives the raw markdown, returns (or
54
+ * resolves to) an HTML string that is rendered as-is — **you are responsible
55
+ * for sanitizing it**. When omitted, the bundled marked + DOMPurify pipeline
56
+ * is used (lazy-loaded; requires the optional `marked` / `dompurify` peers).
57
+ */
58
+ renderMarkdown?: MarkdownRenderer;
59
+ /**
60
+ * Submit handler. Receives the current value. May be async — the submit
61
+ * button shows a spinner and the box is disabled while it resolves.
62
+ */
63
+ onSubmit?: (value: string) => void | Promise<void>;
64
+ /** Cancel handler. When set, a Cancel button is shown by default. */
65
+ onCancel?: () => void;
66
+ /** Fired on every edit with the new value. */
67
+ onChange?: (value: string) => void;
68
+ /** Submit button label. Default `"Comment"`. */
69
+ submitLabel?: string;
70
+ /** Cancel button label. Default `"Cancel"`. */
71
+ cancelLabel?: string;
72
+ /** Show the submit button. Defaults to `true` when `onSubmit` is set. */
73
+ showSubmit?: boolean;
74
+ /** Show the cancel button. Defaults to `true` when `onCancel` is set. */
75
+ showCancel?: boolean;
76
+ /** Submit on ⌘/Ctrl+Enter while the textarea is focused. Default `true`. */
77
+ submitOnModEnter?: boolean;
78
+ /** Clear the value after a successful submit. Default `true`. */
79
+ clearOnSubmit?: boolean;
80
+ /** Disable the submit button when the value is empty/whitespace. Default `true`. */
81
+ submitDisabledWhenEmpty?: boolean;
82
+ /** Externally-controlled busy state (disables the box + submit). */
83
+ busy?: boolean;
84
+ labelAfter?: SnippetWithId | THC;
85
+ below?: SnippetWithId | THC;
86
+ labelLeft?: boolean;
87
+ labelLeftWidth?: "normal" | "wide";
88
+ labelLeftBreakpoint?: number;
89
+ /** Optional avatar/gutter rendered to the left of the box. */
90
+ avatar?: SnippetWithId | THC;
91
+ /** Extra content rendered in the footer, left of the buttons (e.g. attach). */
92
+ footer?: Snippet;
93
+ /** Classes for the `<textarea>` element. */
94
+ classInput?: string;
95
+ /** Classes for the tab bar. */
96
+ classBar?: string;
97
+ /** Classes for the preview panel. */
98
+ classPreview?: string;
99
+ /** Classes for the footer (button row). */
100
+ classFooter?: string;
101
+ style?: string;
102
+ }
103
+ import { type ValidationResult } from "../../actions/validate.svelte.js";
104
+ declare const CommentInput: import("svelte").Component<Props, {
105
+ validate: () => ValidationResult | undefined;
106
+ clearValidation: () => void;
107
+ getValidation: () => ValidationResult | undefined;
108
+ focus: () => void;
109
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
110
+ submit: () => void;
111
+ }, "value" | "input" | "mode">;
112
+ type CommentInput = ReturnType<typeof CommentInput>;
113
+ export default CommentInput;