@marianmeres/stuic 3.137.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.
@@ -101,8 +101,18 @@
101
101
  submitOnModEnter?: boolean;
102
102
  /** Clear the value after a successful submit. Default `true`. */
103
103
  clearOnSubmit?: boolean;
104
- /** Disable the submit button when the value is empty/whitespace. Default `true`. */
105
- 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;
106
116
  /** Externally-controlled busy state (disables the box + submit). */
107
117
  busy?: boolean;
108
118
  //
@@ -171,7 +181,8 @@
171
181
  showCancel,
172
182
  submitOnModEnter = true,
173
183
  clearOnSubmit = true,
174
- submitDisabledWhenEmpty = true,
184
+ blockEmptySubmit = true,
185
+ emptyMessage = "Please write something before submitting.",
175
186
  busy = false,
176
187
  //
177
188
  labelAfter,
@@ -207,15 +218,61 @@
207
218
  const _showSubmit = $derived(showSubmit ?? !!onSubmit);
208
219
  const _showCancel = $derived(showCancel ?? !!onCancel);
209
220
  const _busy = $derived(disabled || busy || submitting);
210
- const canSubmit = $derived(!_busy && !(submitDisabledWhenEmpty && isEmpty));
211
221
  // Toggling `disabled` on MarkdownEditor remounts its backend, so only feed it
212
222
  // the externally-controlled disable (not the transient `submitting`); the
213
223
  // footer buttons carry the in-flight state instead.
214
224
  const editorDisabled = $derived(disabled || busy);
215
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
+
216
260
  // --- Handlers -------------------------------------------------------------
217
261
  async function handleSubmit() {
218
- 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
+ }
219
276
  // Validate before invoking the handler — the submit button bypasses any
220
277
  // form-level submit check, so enforce it here (surfaces the inline message).
221
278
  const res = editor?.validate();
@@ -326,7 +383,7 @@
326
383
  disabled={editorDisabled}
327
384
  {placeholder}
328
385
  {renderSize}
329
- validate={validateProp}
386
+ validate={_validate}
330
387
  {toolbar}
331
388
  {mobileToolbar}
332
389
  {autoSourceOnMobile}
@@ -376,7 +433,7 @@
376
433
  type="button"
377
434
  intent="primary"
378
435
  size={renderSize}
379
- disabled={!canSubmit}
436
+ disabled={_busy}
380
437
  spinner={submitting}
381
438
  onclick={handleSubmit}
382
439
  >
@@ -79,8 +79,18 @@ export interface Props extends InputWrapClassProps {
79
79
  submitOnModEnter?: boolean;
80
80
  /** Clear the value after a successful submit. Default `true`. */
81
81
  clearOnSubmit?: boolean;
82
- /** Disable the submit button when the value is empty/whitespace. Default `true`. */
83
- 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;
84
94
  /** Externally-controlled busy state (disables the box + submit). */
85
95
  busy?: boolean;
86
96
  labelAfter?: SnippetWithId | THC;
@@ -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.137.0",
3
+ "version": "3.138.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",