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