@marianmeres/stuic 3.136.0 → 3.138.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.
@@ -6,6 +6,7 @@
6
6
  } from "../../actions/validate.svelte.js";
7
7
  import type { THC } from "../Thc/Thc.svelte";
8
8
  import type { InputWrapClassProps } from "../Input/types.js";
9
+ import { DEFAULT_MOBILE_TOOLBAR } from "../MarkdownEditor/index.js";
9
10
  import type {
10
11
  MarkdownEditorMode,
11
12
  ToolbarItem,
@@ -18,14 +19,18 @@
18
19
  export type CommentInputMode = MarkdownEditorMode;
19
20
 
20
21
  /**
21
- * Minimal default toolbar for the comment box — kept intentionally light
22
- * (GitHub-composer feel). Pass your own `toolbar` array to customize.
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.
23
25
  */
24
26
  export const DEFAULT_COMMENT_TOOLBAR: ToolbarItem[] = [
25
27
  "bold",
26
28
  "italic",
29
+ "|",
30
+ "bulletList",
31
+ "orderedList",
32
+ "|",
27
33
  "link",
28
- "codeBlock",
29
34
  ];
30
35
 
31
36
  export interface Props extends InputWrapClassProps {
@@ -96,8 +101,18 @@
96
101
  submitOnModEnter?: boolean;
97
102
  /** Clear the value after a successful submit. Default `true`. */
98
103
  clearOnSubmit?: boolean;
99
- /** Disable the submit button when the value is empty/whitespace. Default `true`. */
100
- submitDisabledWhenEmpty?: boolean;
104
+ /**
105
+ * Block submitting an empty/whitespace comment. Default `true`. When blocked,
106
+ * the native inline validation error is surfaced (the submit button stays
107
+ * enabled) instead of disabling the button — a disabled submit makes the whole
108
+ * box feel dead. Set `false` to let empty submits through to `onSubmit`.
109
+ */
110
+ blockEmptySubmit?: boolean;
111
+ /**
112
+ * Message rendered by the inline error when an empty submit is blocked (see
113
+ * {@link blockEmptySubmit}). Default `"Please write something before submitting."`.
114
+ */
115
+ emptyMessage?: string;
101
116
  /** Externally-controlled busy state (disables the box + submit). */
102
117
  busy?: boolean;
103
118
  //
@@ -166,7 +181,8 @@
166
181
  showCancel,
167
182
  submitOnModEnter = true,
168
183
  clearOnSubmit = true,
169
- submitDisabledWhenEmpty = true,
184
+ blockEmptySubmit = true,
185
+ emptyMessage = "Please write something before submitting.",
170
186
  busy = false,
171
187
  //
172
188
  labelAfter,
@@ -202,15 +218,61 @@
202
218
  const _showSubmit = $derived(showSubmit ?? !!onSubmit);
203
219
  const _showCancel = $derived(showCancel ?? !!onCancel);
204
220
  const _busy = $derived(disabled || busy || submitting);
205
- const canSubmit = $derived(!_busy && !(submitDisabledWhenEmpty && isEmpty));
206
221
  // Toggling `disabled` on MarkdownEditor remounts its backend, so only feed it
207
222
  // the externally-controlled disable (not the transient `submitting`); the
208
223
  // footer buttons carry the in-flight state instead.
209
224
  const editorDisabled = $derived(disabled || busy);
210
225
 
226
+ // Empty submits are blocked by surfacing the native inline error (NOT by
227
+ // disabling the button — a disabled submit makes the whole box feel dead).
228
+ // We arm this only for the duration of an explicit submit attempt, so a casual
229
+ // focus/blur of an empty box doesn't nag with an error. Plain (non-reactive)
230
+ // `let`: the validator closure reads it live, and we never want toggling it to
231
+ // recompute `_validate`.
232
+ let emptyCheckArmed = false;
233
+
234
+ // Validation options forwarded to MarkdownEditor. When `blockEmptySubmit` is on
235
+ // we splice an empty-check into the consumer's validator — preserving any custom
236
+ // rule plus the editor's own `required` handling — that fires only while armed.
237
+ const _validate = $derived.by(() => {
238
+ // Consumer disabled validation entirely: pass through untouched. There is no
239
+ // inline error to surface then; `handleSubmit` still refuses empty submits.
240
+ if (validateProp === false) return false;
241
+ if (!blockEmptySubmit) return validateProp;
242
+ const base: Omit<ValidateOptions, "setValidationResult"> =
243
+ typeof validateProp === "object" && validateProp ? validateProp : {};
244
+ const inner = base.customValidator;
245
+ return {
246
+ ...base,
247
+ customValidator(
248
+ val: unknown,
249
+ ctx: Record<string, unknown> | undefined,
250
+ el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
251
+ ) {
252
+ if (emptyCheckArmed && (val == null || String(val).trim() === "")) {
253
+ return emptyMessage;
254
+ }
255
+ return inner ? inner(val, ctx, el) || "" : "";
256
+ },
257
+ };
258
+ });
259
+
211
260
  // --- Handlers -------------------------------------------------------------
212
261
  async function handleSubmit() {
213
- if (!canSubmit) return;
262
+ if (_busy) return;
263
+ // Block an empty submit by surfacing the native inline error instead of
264
+ // disabling the button. Arm the empty-check just for this validation pass,
265
+ // then focus the box so the user can start typing.
266
+ if (blockEmptySubmit && isEmpty) {
267
+ emptyCheckArmed = true;
268
+ try {
269
+ editor?.validate();
270
+ } finally {
271
+ emptyCheckArmed = false;
272
+ }
273
+ editor?.focus();
274
+ return;
275
+ }
214
276
  // Validate before invoking the handler — the submit button bypasses any
215
277
  // form-level submit check, so enforce it here (surfaces the inline message).
216
278
  const res = editor?.validate();
@@ -321,7 +383,7 @@
321
383
  disabled={editorDisabled}
322
384
  {placeholder}
323
385
  {renderSize}
324
- validate={validateProp}
386
+ validate={_validate}
325
387
  {toolbar}
326
388
  {mobileToolbar}
327
389
  {autoSourceOnMobile}
@@ -371,7 +433,7 @@
371
433
  type="button"
372
434
  intent="primary"
373
435
  size={renderSize}
374
- disabled={!canSubmit}
436
+ disabled={_busy}
375
437
  spinner={submitting}
376
438
  onclick={handleSubmit}
377
439
  >
@@ -9,8 +9,9 @@ type SnippetWithId = Snippet<[{
9
9
  /** Active editing surface of the comment box (mirrors `MarkdownEditor`). */
10
10
  export type CommentInputMode = MarkdownEditorMode;
11
11
  /**
12
- * Minimal default toolbar for the comment box — kept intentionally light
13
- * (GitHub-composer feel). Pass your own `toolbar` array to customize.
12
+ * Default toolbar for the comment box — mirrors the reduced mobile toolbar
13
+ * ({@link DEFAULT_MOBILE_TOOLBAR}) so the desktop and touch experiences stay
14
+ * consistent. Pass your own `toolbar` array to customize.
14
15
  */
15
16
  export declare const DEFAULT_COMMENT_TOOLBAR: ToolbarItem[];
16
17
  export interface Props extends InputWrapClassProps {
@@ -78,8 +79,18 @@ export interface Props extends InputWrapClassProps {
78
79
  submitOnModEnter?: boolean;
79
80
  /** Clear the value after a successful submit. Default `true`. */
80
81
  clearOnSubmit?: boolean;
81
- /** Disable the submit button when the value is empty/whitespace. Default `true`. */
82
- submitDisabledWhenEmpty?: boolean;
82
+ /**
83
+ * Block submitting an empty/whitespace comment. Default `true`. When blocked,
84
+ * the native inline validation error is surfaced (the submit button stays
85
+ * enabled) instead of disabling the button — a disabled submit makes the whole
86
+ * box feel dead. Set `false` to let empty submits through to `onSubmit`.
87
+ */
88
+ blockEmptySubmit?: boolean;
89
+ /**
90
+ * Message rendered by the inline error when an empty submit is blocked (see
91
+ * {@link blockEmptySubmit}). Default `"Please write something before submitting."`.
92
+ */
93
+ emptyMessage?: string;
83
94
  /** Externally-controlled busy state (disables the box + submit). */
84
95
  busy?: boolean;
85
96
  labelAfter?: SnippetWithId | THC;
@@ -51,11 +51,11 @@ console error explains why.
51
51
 
52
52
  ## Toolbar
53
53
 
54
- `toolbar` accepts `true` (the minimal default), `false` (hidden), or an ordered
54
+ `toolbar` accepts `true` (the default), `false` (hidden), or an ordered
55
55
  array of items (`"|"` for a separator):
56
56
 
57
57
  ```svelte
58
- <!-- Minimal default: ["bold", "italic", "link", "codeBlock"] -->
58
+ <!-- Default: ["bold", "italic", "|", "bulletList", "orderedList", "|", "link"] -->
59
59
  <CommentInput bind:value onSubmit={post} />
60
60
 
61
61
  <!-- Custom set -->
@@ -82,43 +82,44 @@ Available items: `bold`, `italic`, `heading1`–`heading6`, `link`, `image`,
82
82
 
83
83
  ## Props
84
84
 
85
- | Prop | Type | Default | Description |
86
- | ------------------------- | ------------------------------------------ | ------------------------- | --------------------------------------------------------- |
87
- | `value` | `string` (bindable) | `""` | Comment content (markdown). |
88
- | `mode` | `"wysiwyg" \| "source"` (bindable) | `"source"` | Active editing surface (opens on raw markdown). |
89
- | `input` | `HTMLDivElement` (bindable) | — | The editor surface wrapper element. |
90
- | `name` | `string` | — | Hidden input `name`, for native form submission. |
91
- | `label` | `THC \| Snippet` | `""` | Field label (via InputWrap). |
92
- | `description` | `THC \| Snippet` | — | Collapsible hint below the box. |
93
- | `placeholder` | `string` | — | Editor placeholder. |
94
- | `renderSize` | `"sm" \| "md" \| "lg" \| string` | `"md"` | Size variant. |
95
- | `required` | `boolean` | `false` | Mark required + enforce on validation. |
96
- | `disabled` | `boolean` | `false` | Disable the whole box. |
97
- | `validate` | `boolean \| ValidateOptions` | — | Enable validation (same contract as `MarkdownEditor`). |
98
- | `toolbar` | `boolean \| ToolbarItem[]` | `DEFAULT_COMMENT_TOOLBAR` | Formatting toolbar config. |
99
- | `mobileToolbar` | `boolean \| ToolbarItem[]` | MarkdownEditor default | Toolbar on touch devices. |
100
- | `autoSourceOnMobile` | `boolean` | `true` | Start in source mode on mobile. |
101
- | `mobileQuery` | `string` | `(pointer: coarse) …` | Media query defining "mobile". |
102
- | `prompt` | `PromptFn` | `window.prompt` | URL prompt used by the link/image buttons. |
103
- | `maxHeight` | `number \| string` | `32rem` | Cap the editing surface height (scrolls inside). |
104
- | `capToParent` | `boolean` | `true` | Also cap the surface to the parent's available height. |
105
- | `showModeToggle` | `boolean` | `true` | Show the WYSIWYG/Source toggle. |
106
- | `sourceLabel` | `string` | `"Source"` | Toggle label while in WYSIWYG mode. |
107
- | `previewLabel` | `string` | `"Preview"` | Toggle label while in source mode. |
108
- | `useShortcuts` | `boolean` | `true` | Wire ⌘/Ctrl+B / I / K to bold / italic / link. |
109
- | `onSubmit` | `(value: string) => void \| Promise<void>` | — | Submit handler. Async → spinner + disabled while pending. |
110
- | `onCancel` | `() => void` | — | Cancel handler. |
111
- | `onChange` | `(value: string) => void` | — | Fired on every edit. |
112
- | `submitLabel` | `string` | `"Comment"` | Submit button label. |
113
- | `cancelLabel` | `string` | `"Cancel"` | Cancel button label. |
114
- | `showSubmit` | `boolean` | `!!onSubmit` | Show the submit button. |
115
- | `showCancel` | `boolean` | `!!onCancel` | Show the cancel button. |
116
- | `submitOnModEnter` | `boolean` | `true` | Submit on ⌘/Ctrl+Enter. |
117
- | `clearOnSubmit` | `boolean` | `true` | Clear value after a successful submit. |
118
- | `submitDisabledWhenEmpty` | `boolean` | `true` | Disable submit when empty/whitespace. |
119
- | `busy` | `boolean` | `false` | External busy state (disables box + submit). |
120
- | `avatar` | `THC \| Snippet` | | Optional gutter to the left of the box. |
121
- | `footer` | `Snippet` | — | Extra footer content, left of the buttons. |
85
+ | Prop | Type | Default | Description |
86
+ | -------------------- | ------------------------------------------ | --------------------------------------------- | --------------------------------------------------------------------------- |
87
+ | `value` | `string` (bindable) | `""` | Comment content (markdown). |
88
+ | `mode` | `"wysiwyg" \| "source"` (bindable) | `"source"` | Active editing surface (opens on raw markdown). |
89
+ | `input` | `HTMLDivElement` (bindable) | — | The editor surface wrapper element. |
90
+ | `name` | `string` | — | Hidden input `name`, for native form submission. |
91
+ | `label` | `THC \| Snippet` | `""` | Field label (via InputWrap). |
92
+ | `description` | `THC \| Snippet` | — | Collapsible hint below the box. |
93
+ | `placeholder` | `string` | — | Editor placeholder. |
94
+ | `renderSize` | `"sm" \| "md" \| "lg" \| string` | `"md"` | Size variant. |
95
+ | `required` | `boolean` | `false` | Mark required + enforce on validation. |
96
+ | `disabled` | `boolean` | `false` | Disable the whole box. |
97
+ | `validate` | `boolean \| ValidateOptions` | — | Enable validation (same contract as `MarkdownEditor`). |
98
+ | `toolbar` | `boolean \| ToolbarItem[]` | `DEFAULT_COMMENT_TOOLBAR` | Formatting toolbar config. |
99
+ | `mobileToolbar` | `boolean \| ToolbarItem[]` | MarkdownEditor default | Toolbar on touch devices. |
100
+ | `autoSourceOnMobile` | `boolean` | `true` | Start in source mode on mobile. |
101
+ | `mobileQuery` | `string` | `(pointer: coarse) …` | Media query defining "mobile". |
102
+ | `prompt` | `PromptFn` | `window.prompt` | URL prompt used by the link/image buttons. |
103
+ | `maxHeight` | `number \| string` | `32rem` | Cap the editing surface height (scrolls inside). |
104
+ | `capToParent` | `boolean` | `true` | Also cap the surface to the parent's available height. |
105
+ | `showModeToggle` | `boolean` | `true` | Show the WYSIWYG/Source toggle. |
106
+ | `sourceLabel` | `string` | `"Source"` | Toggle label while in WYSIWYG mode. |
107
+ | `previewLabel` | `string` | `"Preview"` | Toggle label while in source mode. |
108
+ | `useShortcuts` | `boolean` | `true` | Wire ⌘/Ctrl+B / I / K to bold / italic / link. |
109
+ | `onSubmit` | `(value: string) => void \| Promise<void>` | — | Submit handler. Async → spinner + disabled while pending. |
110
+ | `onCancel` | `() => void` | — | Cancel handler. |
111
+ | `onChange` | `(value: string) => void` | — | Fired on every edit. |
112
+ | `submitLabel` | `string` | `"Comment"` | Submit button label. |
113
+ | `cancelLabel` | `string` | `"Cancel"` | Cancel button label. |
114
+ | `showSubmit` | `boolean` | `!!onSubmit` | Show the submit button. |
115
+ | `showCancel` | `boolean` | `!!onCancel` | Show the cancel button. |
116
+ | `submitOnModEnter` | `boolean` | `true` | Submit on ⌘/Ctrl+Enter. |
117
+ | `clearOnSubmit` | `boolean` | `true` | Clear value after a successful submit. |
118
+ | `blockEmptySubmit` | `boolean` | `true` | Block empty/whitespace submits via the inline error (submit stays enabled). |
119
+ | `emptyMessage` | `string` | `"Please write something before submitting."` | Inline error shown when an empty submit is blocked. |
120
+ | `busy` | `boolean` | `false` | External busy state (disables box + submit). |
121
+ | `avatar` | `THC \| Snippet` | — | Optional gutter to the left of the box. |
122
+ | `footer` | `Snippet` | — | Extra footer content, left of the buttons. |
122
123
 
123
124
  Plus the standard `InputWrap` layout props (`labelAfter`, `below`, `labelLeft`,
124
125
  `labelLeftWidth`, `labelLeftBreakpoint`) and `class*` props (`classInput` →
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.136.0",
3
+ "version": "3.138.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",