@marianmeres/stuic 3.135.0 → 3.137.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.
@@ -1,65 +1,84 @@
1
1
  <script lang="ts" module>
2
2
  import type { Snippet } from "svelte";
3
- import type { HTMLTextareaAttributes } from "svelte/elements";
4
- import type { ValidateOptions } from "../../actions/validate.svelte.js";
3
+ import type {
4
+ ValidateOptions,
5
+ ValidationResult,
6
+ } from "../../actions/validate.svelte.js";
5
7
  import type { THC } from "../Thc/Thc.svelte";
6
8
  import type { InputWrapClassProps } from "../Input/types.js";
7
- import type { MarkdownRenderer } from "./_internal/render.js";
9
+ import { DEFAULT_MOBILE_TOOLBAR } from "../MarkdownEditor/index.js";
10
+ import type {
11
+ MarkdownEditorMode,
12
+ ToolbarItem,
13
+ PromptFn,
14
+ } from "../MarkdownEditor/index.js";
8
15
 
9
16
  type SnippetWithId = Snippet<[{ id: string }]>;
10
17
 
11
- /** Active tab of the comment box. */
12
- export type CommentInputMode = "write" | "preview";
18
+ /** Active editing surface of the comment box (mirrors `MarkdownEditor`). */
19
+ export type CommentInputMode = MarkdownEditorMode;
20
+
21
+ /**
22
+ * Default toolbar for the comment box — mirrors the reduced mobile toolbar
23
+ * ({@link DEFAULT_MOBILE_TOOLBAR}) so the desktop and touch experiences stay
24
+ * consistent. Pass your own `toolbar` array to customize.
25
+ */
26
+ export const DEFAULT_COMMENT_TOOLBAR: ToolbarItem[] = [
27
+ "bold",
28
+ "italic",
29
+ "|",
30
+ "bulletList",
31
+ "orderedList",
32
+ "|",
33
+ "link",
34
+ ];
13
35
 
14
- export interface Props
15
- extends
16
- Omit<HTMLTextareaAttributes, "value" | "class" | "style">,
17
- InputWrapClassProps {
18
- /** Comment text (raw markdown). Bindable. */
36
+ export interface Props extends InputWrapClassProps {
37
+ /** Comment content (markdown). Bindable. */
19
38
  value?: string;
20
- /** Active tab. Bindable. Default `"write"`. */
39
+ /** Active editing surface. Bindable. Default `"source"` (opens on raw markdown). */
21
40
  mode?: CommentInputMode;
22
- /** The underlying `<textarea>` element. Bindable. */
23
- input?: HTMLTextAreaElement;
24
- /** Name attribute of the textarea, for native form submission. */
41
+ /** The editor surface wrapper element. Bindable (read). */
42
+ input?: HTMLDivElement;
43
+ /** Name attribute of the hidden input, for native form submission. */
25
44
  name?: string;
26
45
  //
27
46
  label?: SnippetWithId | THC;
28
47
  description?: SnippetWithId | THC;
29
48
  class?: string;
30
49
  id?: string;
31
- tabindex?: number;
32
50
  renderSize?: "sm" | "md" | "lg" | string;
33
51
  required?: boolean;
34
52
  disabled?: boolean;
35
53
  placeholder?: string;
36
54
  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
55
  //
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
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).
57
+ * Formatting toolbar. `true` shows {@link DEFAULT_COMMENT_TOOLBAR}, `false`
58
+ * hides it, or pass an ordered array of {@link ToolbarItem}s (`"|"` for a
59
+ * separator). Default {@link DEFAULT_COMMENT_TOOLBAR}.
61
60
  */
62
- renderMarkdown?: MarkdownRenderer;
61
+ toolbar?: boolean | ToolbarItem[];
62
+ /** Toolbar used on mobile / touch devices. Same shape as `toolbar`. */
63
+ mobileToolbar?: boolean | ToolbarItem[];
64
+ /** Start in source mode on mobile / touch devices. Default `true`. */
65
+ autoSourceOnMobile?: boolean;
66
+ /** Media query defining "mobile". Default `"(pointer: coarse) and (max-width: 640px)"`. */
67
+ mobileQuery?: string;
68
+ /** URL prompt used by the link/image buttons. Defaults to `window.prompt`. */
69
+ prompt?: PromptFn;
70
+ /** Cap the editing surface height (long content scrolls inside). Default `32rem`. */
71
+ maxHeight?: number | string;
72
+ /** Also cap the surface to the parent's available height. Default `true`. */
73
+ capToParent?: boolean;
74
+ /** Show the WYSIWYG/Source mode toggle. Default `true`. */
75
+ showModeToggle?: boolean;
76
+ /** Toggle label while in WYSIWYG mode (switches to source). Default `"Source"`. */
77
+ sourceLabel?: string;
78
+ /** Toggle label while in source mode (switches to WYSIWYG). Default `"Preview"`. */
79
+ previewLabel?: string;
80
+ /** Wire ⌘/Ctrl+B / I / K to bold / italic / link. Default `true`. */
81
+ useShortcuts?: boolean;
63
82
  //
64
83
  /**
65
84
  * Submit handler. Receives the current value. May be async — the submit
@@ -78,7 +97,7 @@
78
97
  showSubmit?: boolean;
79
98
  /** Show the cancel button. Defaults to `true` when `onCancel` is set. */
80
99
  showCancel?: boolean;
81
- /** Submit on ⌘/Ctrl+Enter while the textarea is focused. Default `true`. */
100
+ /** Submit on ⌘/Ctrl+Enter while the editor is focused. Default `true`. */
82
101
  submitOnModEnter?: boolean;
83
102
  /** Clear the value after a successful submit. Default `true`. */
84
103
  clearOnSubmit?: boolean;
@@ -98,12 +117,10 @@
98
117
  /** Extra content rendered in the footer, left of the buttons (e.g. attach). */
99
118
  footer?: Snippet;
100
119
  //
101
- /** Classes for the `<textarea>` element. */
120
+ /** Classes for the editor surface element (forwarded to MarkdownEditor). */
102
121
  classInput?: string;
103
- /** Classes for the tab bar. */
122
+ /** Classes for the toolbar bar (forwarded to MarkdownEditor's `classToolbar`). */
104
123
  classBar?: string;
105
- /** Classes for the preview panel. */
106
- classPreview?: string;
107
124
  /** Classes for the footer (button row). */
108
125
  classFooter?: string;
109
126
  style?: string;
@@ -111,22 +128,15 @@
111
128
  </script>
112
129
 
113
130
  <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
131
  import { getId } from "../../utils/get-id.js";
121
132
  import { twMerge } from "../../utils/tw-merge.js";
122
133
  import { Button } from "../Button/index.js";
123
134
  import Thc from "../Thc/Thc.svelte";
124
- import InputWrap from "../Input/_internal/InputWrap.svelte";
125
- import { renderMarkdownSafe } from "./_internal/render.js";
135
+ import { MarkdownEditor } from "../MarkdownEditor/index.js";
126
136
 
127
137
  let {
128
138
  value = $bindable(""),
129
- mode = $bindable("write"),
139
+ mode = $bindable("source"),
130
140
  input = $bindable(),
131
141
  name,
132
142
  //
@@ -134,24 +144,23 @@
134
144
  description,
135
145
  class: classProp,
136
146
  id = getId(),
137
- tabindex = 0,
138
147
  renderSize = "md",
139
148
  required = false,
140
149
  disabled = false,
141
150
  placeholder,
142
- // Renamed to avoid collision with the exported `validate()` below.
143
151
  validate: validateProp,
144
- useTrim = true,
145
- useAutogrow = true,
146
152
  //
147
- showTabs = true,
148
- writeLabel = "Write",
153
+ toolbar = DEFAULT_COMMENT_TOOLBAR,
154
+ mobileToolbar,
155
+ autoSourceOnMobile = true,
156
+ mobileQuery,
157
+ prompt,
158
+ maxHeight,
159
+ capToParent = true,
160
+ showModeToggle = true,
161
+ sourceLabel = "Source",
149
162
  previewLabel = "Preview",
150
- showMarkdownHint = true,
151
- markdownHintLabel = "Markdown supported",
152
- previewEmptyLabel = "Nothing to preview",
153
- previewLoadingLabel = "Loading preview…",
154
- renderMarkdown,
163
+ useShortcuts = true,
155
164
  //
156
165
  onSubmit,
157
166
  onCancel,
@@ -176,7 +185,6 @@
176
185
  //
177
186
  classInput,
178
187
  classBar,
179
- classPreview,
180
188
  classFooter,
181
189
  classLabel,
182
190
  classLabelBox,
@@ -188,109 +196,36 @@
188
196
  classBelowBox,
189
197
  classValidationBox,
190
198
  style,
191
- //
192
- ...rest
193
199
  }: Props = $props();
194
200
 
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();
201
+ // The embedded editor instance (exposes validate/focus/command/… imperatively).
202
+ let editor = $state<MarkdownEditor>();
199
203
 
200
204
  // --- Submit state ---------------------------------------------------------
201
205
  let submitting = $state(false);
202
206
  const isEmpty = $derived(!(value ?? "").trim());
203
- // Resolve the show* defaults from the presence of their handlers.
204
207
  const _showSubmit = $derived(showSubmit ?? !!onSubmit);
205
208
  const _showCancel = $derived(showCancel ?? !!onCancel);
206
209
  const _busy = $derived(disabled || busy || submitting);
207
210
  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
- });
211
+ // Toggling `disabled` on MarkdownEditor remounts its backend, so only feed it
212
+ // the externally-controlled disable (not the transient `submitting`); the
213
+ // footer buttons carry the in-flight state instead.
214
+ const editorDisabled = $derived(disabled || busy);
274
215
 
275
216
  // --- Handlers -------------------------------------------------------------
276
217
  async function handleSubmit() {
277
218
  if (!canSubmit) return;
278
219
  // Validate before invoking the handler — the submit button bypasses any
279
220
  // 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
- }
221
+ const res = editor?.validate();
222
+ if (res && !res.valid) return;
285
223
  try {
286
224
  submitting = true;
287
225
  await onSubmit?.(value ?? "");
288
226
  if (clearOnSubmit) {
289
227
  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?.("");
228
+ editor?.clearValidation();
294
229
  }
295
230
  } finally {
296
231
  submitting = false;
@@ -301,60 +236,52 @@
301
236
  onCancel?.();
302
237
  }
303
238
 
239
+ // Capture keydown on the wrapper so we intercept the shortcut BEFORE the
240
+ // editor's own keymap (stopPropagation prevents a double-handle / native bold).
304
241
  function handleKeydown(e: KeyboardEvent) {
305
- if (submitOnModEnter && (e.metaKey || e.ctrlKey) && e.key === "Enter") {
242
+ if (!(e.metaKey || e.ctrlKey)) return;
243
+ if (submitOnModEnter && e.key === "Enter") {
306
244
  e.preventDefault();
245
+ e.stopPropagation();
307
246
  handleSubmit();
247
+ return;
248
+ }
249
+ if (!useShortcuts) return;
250
+ const cmd =
251
+ e.key === "b" || e.key === "B"
252
+ ? "bold"
253
+ : e.key === "i" || e.key === "I"
254
+ ? "italic"
255
+ : e.key === "k" || e.key === "K"
256
+ ? "link"
257
+ : undefined;
258
+ if (cmd) {
259
+ e.preventDefault();
260
+ e.stopPropagation();
261
+ editor?.command(cmd);
308
262
  }
309
263
  }
310
264
 
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) -------------------------------
265
+ // --- Imperative API (delegates to the embedded MarkdownEditor) ------------
336
266
  /** Trigger validation now. Renders the inline message if invalid. */
337
267
  export function validate(): ValidationResult | undefined {
338
- _doValidate?.();
339
- return validation;
268
+ return editor?.validate();
340
269
  }
341
- /** Clear the inline validation message and reset `setCustomValidity`. */
270
+ /** Clear the inline validation message. */
342
271
  export function clearValidation(): void {
343
- validation = undefined;
344
- input?.setCustomValidity?.("");
272
+ editor?.clearValidation();
345
273
  }
346
274
  /** Current validation state, or undefined if the validator has never run. */
347
275
  export function getValidation(): ValidationResult | undefined {
348
- return validation;
276
+ return editor?.getValidation();
349
277
  }
350
- /** Focus the underlying `<textarea>` (switches to the Write tab first). */
278
+ /** Focus the editor surface. */
351
279
  export function focus(): void {
352
- mode = "write";
353
- input?.focus?.();
280
+ editor?.focus();
354
281
  }
355
282
  /** Scroll the field into view. Defaults to smooth + center. */
356
283
  export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
357
- input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
284
+ editor?.scrollIntoView(opts);
358
285
  }
359
286
  /** Programmatically run the submit flow (respects the empty/busy guards). */
360
287
  export function submit(): void {
@@ -370,178 +297,94 @@
370
297
  {/if}
371
298
  {/snippet}
372
299
 
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}
300
+ <div
301
+ class={twMerge("stuic-comment-input", classProp)}
302
+ data-size={renderSize}
303
+ class:disabled
396
304
  {style}
305
+ onkeydowncapture={handleKeydown}
397
306
  >
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}
307
+ {#if avatar}
308
+ <div class="stuic-comment-input-avatar">
309
+ {@render thcOrSnippet(avatar)}
310
+ </div>
311
+ {/if}
404
312
 
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"
313
+ <div class="stuic-comment-input-main">
314
+ <MarkdownEditor
315
+ bind:this={editor}
316
+ bind:value
317
+ bind:mode
318
+ bind:input
319
+ {id}
320
+ {name}
321
+ {label}
322
+ {description}
323
+ {labelAfter}
324
+ {below}
325
+ {required}
326
+ disabled={editorDisabled}
327
+ {placeholder}
328
+ {renderSize}
329
+ validate={validateProp}
330
+ {toolbar}
331
+ {mobileToolbar}
332
+ {autoSourceOnMobile}
333
+ {mobileQuery}
334
+ {prompt}
335
+ {maxHeight}
336
+ {capToParent}
337
+ {showModeToggle}
338
+ {sourceLabel}
339
+ {previewLabel}
340
+ {labelLeft}
341
+ {labelLeftWidth}
342
+ {labelLeftBreakpoint}
343
+ classInput={twMerge("stuic-comment-input-editor", classInput)}
344
+ classToolbar={classBar}
345
+ {classLabel}
346
+ {classLabelBox}
347
+ {classInputBox}
348
+ {classInputBoxWrap}
349
+ {classInputBoxWrapInvalid}
350
+ {classDescBox}
351
+ {classDescBoxToggle}
352
+ {classBelowBox}
353
+ {classValidationBox}
354
+ onChange={(v) => onChange?.(v)}
355
+ />
356
+
357
+ {#if _showSubmit || _showCancel || footer}
358
+ <div class={twMerge("stuic-comment-input-footer", classFooter)}>
359
+ <div class="stuic-comment-input-footer-start">
360
+ {@render footer?.()}
361
+ </div>
362
+ <div class="stuic-comment-input-footer-end">
363
+ {#if _showCancel}
364
+ <Button
365
+ type="button"
366
+ variant="ghost"
367
+ size={renderSize}
368
+ disabled={submitting}
369
+ onclick={handleCancel}
413
370
  >
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>
371
+ {cancelLabel}
372
+ </Button>
443
373
  {/if}
444
- {#if showMarkdownHint && mode === "write"}
445
- <span class="stuic-comment-input-hint">{markdownHintLabel}</span>
374
+ {#if _showSubmit}
375
+ <Button
376
+ type="button"
377
+ intent="primary"
378
+ size={renderSize}
379
+ disabled={!canSubmit}
380
+ spinner={submitting}
381
+ onclick={handleSubmit}
382
+ >
383
+ {submitLabel}
384
+ </Button>
446
385
  {/if}
447
386
  </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
387
  </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>
388
+ {/if}
546
389
  </div>
547
- </InputWrap>
390
+ </div>