@mohasinac/appkit 4.61.0 → 4.62.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.
Files changed (96) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
  3. package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
  4. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
  5. package/dist/_internal/server/jobs/core/paymentReconcile.js +87 -97
  6. package/dist/_internal/shared/actions/action-registry.js +149 -9
  7. package/dist/_internal/shared/media/limits.d.ts +10 -0
  8. package/dist/_internal/shared/media/limits.js +10 -0
  9. package/dist/_internal/shared/types/action-result.js +15 -1
  10. package/dist/client/api/surface-error.js +16 -2
  11. package/dist/client/api/useApiMutation.js +17 -1
  12. package/dist/client.d.ts +1 -1
  13. package/dist/client.js +1 -1
  14. package/dist/constants/api-endpoints.d.ts +3 -0
  15. package/dist/constants/api-endpoints.js +1 -0
  16. package/dist/constants/field-names.d.ts +17 -0
  17. package/dist/constants/field-names.js +23 -0
  18. package/dist/core/server-action.js +19 -3
  19. package/dist/errors/error-mapping.js +26 -1
  20. package/dist/features/account/components/UserOffersPanel.js +5 -3
  21. package/dist/features/admin/components/AdminActionIndexView.js +100 -5
  22. package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
  23. package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
  24. package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
  25. package/dist/features/admin/components/AdminCarouselView.js +96 -4
  26. package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
  27. package/dist/features/admin/components/AdminMediaView.js +36 -3
  28. package/dist/features/admin/components/AdminNavigationView.js +4 -1
  29. package/dist/features/admin/components/AdminOffersView.js +2 -1
  30. package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
  31. package/dist/features/admin/components/AdminProductEditorView.js +2 -2
  32. package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
  33. package/dist/features/admin/components/AdminSectionsView.js +486 -224
  34. package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
  35. package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
  36. package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
  37. package/dist/features/admin/repository/notification.repository.d.ts +15 -0
  38. package/dist/features/admin/repository/notification.repository.js +32 -0
  39. package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
  40. package/dist/features/admin/schemas/action-index-form.js +115 -0
  41. package/dist/features/auth/components/RegisterForm.js +2 -1
  42. package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
  43. package/dist/features/events/components/AdminEventEditorView.js +8 -6
  44. package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
  45. package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
  46. package/dist/features/orders/components/RefundRequestView.js +4 -4
  47. package/dist/features/orders/schemas/index.d.ts +6 -6
  48. package/dist/features/products/components/GroupMemberPicker.js +4 -1
  49. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
  50. package/dist/features/products/components/ProductDetailActions.js +2 -1
  51. package/dist/features/seller/components/FulfillmentView.js +3 -2
  52. package/dist/features/seller/components/SellerAddressesView.js +32 -4
  53. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
  54. package/dist/features/seller/components/SellerBundlesView.js +53 -14
  55. package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
  56. package/dist/features/seller/components/SellerListingTypeView.js +56 -10
  57. package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
  58. package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
  59. package/dist/features/seller/components/SellerProductsView.js +5 -1
  60. package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
  61. package/dist/features/seller/components/SellerShippingView.js +2 -1
  62. package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
  63. package/dist/features/seller/components/SellerSupportView.js +69 -2
  64. package/dist/features/shell/SectionForm.js +20 -16
  65. package/dist/features/shell/build-sections.js +14 -1
  66. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
  67. package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
  68. package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
  69. package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
  70. package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
  71. package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
  72. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
  73. package/dist/index.d.ts +5 -1
  74. package/dist/index.js +3 -1
  75. package/dist/next/routing/route-map.d.ts +4 -0
  76. package/dist/next/routing/route-map.js +9 -0
  77. package/dist/schemas/registry.d.ts +4 -4
  78. package/dist/schemas/sieve.d.ts +3 -3
  79. package/dist/server.d.ts +1 -1
  80. package/dist/server.js +1 -1
  81. package/dist/ui/components/Checkbox.js +10 -2
  82. package/dist/ui/components/DateInput.js +8 -1
  83. package/dist/ui/components/Input.js +22 -2
  84. package/dist/ui/components/PaginatedSelect.d.ts +8 -19
  85. package/dist/ui/components/PaginatedSelect.js +43 -9
  86. package/dist/ui/components/Radio.js +8 -1
  87. package/dist/ui/components/Select.js +10 -2
  88. package/dist/ui/components/TagInput.d.ts +8 -1
  89. package/dist/ui/components/TagInput.js +4 -3
  90. package/dist/ui/components/Textarea.js +10 -2
  91. package/dist/ui/forms/FieldCheckbox.js +1 -1
  92. package/dist/ui/forms/FormErrorList.d.ts +0 -16
  93. package/dist/ui/forms/FormErrorList.js +34 -1
  94. package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
  95. package/dist/ui/forms/apply-zod-issues.js +23 -1
  96. package/package.json +1 -1
@@ -6,6 +6,14 @@ import { Label, Text } from "./Typography.js";
6
6
  export function Select({ options, value, onChange, onValueChange, placeholder, label, error, helperText, disabled = false, required, className = "", id, variant = "default", bare = false, wrapperClassName, ...props }) {
7
7
  const generatedId = React.useId();
8
8
  const inputId = id ?? generatedId;
9
+ /*
10
+ * A caller's own `aria-invalid` wins. `{...props}` is spread first, so
11
+ * re-specifying the attribute afterwards used to overwrite an incoming
12
+ * `"true"` with `undefined` — and `FieldSelect` sets it itself while
13
+ * withholding `error` (to avoid a double message). See the full writeup in
14
+ * `Input.tsx`; the same bug existed here.
15
+ */
16
+ const ariaInvalid = props["aria-invalid"] ?? (error ? "true" : undefined);
9
17
  const handleChange = (event) => {
10
18
  onChange?.(event);
11
19
  onValueChange?.(event.target.value);
@@ -28,7 +36,7 @@ export function Select({ options, value, onChange, onValueChange, placeholder, l
28
36
  .join(" ");
29
37
  const optionsMarkup = (_jsxs(_Fragment, { children: [placeholder ? (_jsx("option", { value: "", disabled: true, children: placeholder })) : null, options.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, String(option.value))))] }));
30
38
  if (bare) {
31
- return (_jsx("select", { ...props, id: inputId, value: value, onChange: handleChange, disabled: disabled, "aria-invalid": error ? "true" : undefined, className: selectClassName, children: optionsMarkup }));
39
+ return (_jsx("select", { ...props, id: inputId, value: value, onChange: handleChange, disabled: disabled, "aria-invalid": ariaInvalid, className: selectClassName, children: optionsMarkup }));
32
40
  }
33
- return (_jsxs("div", { className: ["appkit-select", wrapperClassName].filter(Boolean).join(" "), "data-section": "select-div-592", children: [label && (_jsx(Label, { htmlFor: inputId, className: "appkit-select__label", required: required, children: label })), _jsxs("div", { className: "relative group", "data-section": "select-div-593", children: [_jsx("select", { ...props, id: inputId, value: value, onChange: handleChange, disabled: disabled, "aria-invalid": error ? "true" : undefined, className: selectClassName, children: optionsMarkup }), _jsx(Div, { className: "pointer-events-none absolute inset-y-0 right-3 flex items-center text-[var(--appkit-color-text-faint)]", "data-section": "select-div-594", children: _jsx("svg", { className: "h-4 w-4", viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", clipRule: "evenodd" }) }) })] }), error ? (_jsx(Text, { size: "sm", variant: "error", className: "appkit-select__error", role: "alert", children: error })) : helperText ? (_jsx(Text, { size: "sm", variant: "secondary", className: "appkit-select__error", children: helperText })) : null] }));
41
+ return (_jsxs("div", { className: ["appkit-select", wrapperClassName].filter(Boolean).join(" "), "data-section": "select-div-592", children: [label && (_jsx(Label, { htmlFor: inputId, className: "appkit-select__label", required: required, children: label })), _jsxs("div", { className: "relative group", "data-section": "select-div-593", children: [_jsx("select", { ...props, id: inputId, value: value, onChange: handleChange, disabled: disabled, "aria-invalid": ariaInvalid, className: selectClassName, children: optionsMarkup }), _jsx(Div, { className: "pointer-events-none absolute inset-y-0 right-3 flex items-center text-[var(--appkit-color-text-faint)]", "data-section": "select-div-594", children: _jsx("svg", { className: "h-4 w-4", viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", clipRule: "evenodd" }) }) })] }), error ? (_jsx(Text, { size: "sm", variant: "error", className: "appkit-select__error", role: "alert", children: error })) : helperText ? (_jsx(Text, { size: "sm", variant: "secondary", className: "appkit-select__error", children: helperText })) : null] }));
34
42
  }
@@ -7,5 +7,12 @@ export interface TagInputProps {
7
7
  placeholder?: string;
8
8
  className?: string;
9
9
  helperText?: string;
10
+ /**
11
+ * Validation message. This control had no error slot at all, so a schema
12
+ * failure on a tags field could only ever appear in the summary — the field
13
+ * itself showed nothing and was not announced as invalid. Mirrors the
14
+ * contract Input/Select/Textarea/Checkbox already have.
15
+ */
16
+ error?: React.ReactNode;
10
17
  }
11
- export declare function TagInput({ value, onChange, disabled, label, placeholder, className, helperText, }: TagInputProps): React.JSX.Element;
18
+ export declare function TagInput({ value, onChange, disabled, label, placeholder, className, helperText, error, }: TagInputProps): React.JSX.Element;
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useRef, useState } from "react";
3
+ import React, { useRef, useState } from "react";
4
4
  import { Label, Span } from "./Typography.js";
5
- export function TagInput({ value, onChange, disabled = false, label, placeholder = "Add a tag...", className = "", helperText = "Press Enter or comma to add a tag", }) {
5
+ export function TagInput({ value, onChange, disabled = false, label, placeholder = "Add a tag...", className = "", helperText = "Press Enter or comma to add a tag", error, }) {
6
6
  const [draft, setDraft] = useState("");
7
7
  const inputRef = useRef(null);
8
+ const errorId = React.useId();
8
9
  const normalizeTag = (raw) => raw.trim().replace(/,+$/, "").trim();
9
10
  const addTag = (raw) => {
10
11
  const tag = normalizeTag(raw);
@@ -69,5 +70,5 @@ export function TagInput({ value, onChange, disabled = false, label, placeholder
69
70
  disabled ? "appkit-tag-input__text--disabled" : "",
70
71
  ]
71
72
  .filter(Boolean)
72
- .join(" "), "aria-label": label ?? "Tag input" })] }), helperText && (_jsx(Span, { size: "xs", className: "appkit-tag-input__helper", children: helperText }))] }));
73
+ .join(" "), "aria-label": label ?? "Tag input", "aria-invalid": error ? "true" : undefined, "aria-describedby": error ? errorId : undefined })] }), error ? (_jsx(Span, { id: errorId, size: "xs", className: "appkit-tag-input__helper appkit-form-field__error", role: "alert", children: error })) : helperText ? (_jsx(Span, { size: "xs", className: "appkit-tag-input__helper", children: helperText })) : null] }));
73
74
  }
@@ -15,12 +15,20 @@ export const Textarea = React.forwardRef(function Textarea({ label, error, helpe
15
15
  : variant === "ghost"
16
16
  ? "border-transparent bg-transparent shadow-none"
17
17
  : "";
18
+ /*
19
+ * A caller's own `aria-invalid` wins. `{...props}` is spread first, so
20
+ * re-specifying the attribute afterwards used to overwrite an incoming
21
+ * `"true"` with `undefined` — and `FieldTextarea` sets it itself while
22
+ * withholding `error` (to avoid a double message). See the full writeup in
23
+ * `Input.tsx`; the same bug existed here.
24
+ */
25
+ const ariaInvalid = props["aria-invalid"] ?? (error ? "true" : undefined);
18
26
  const textareaClassName = ["appkit-textarea", variantClass, className]
19
27
  .filter(Boolean)
20
28
  .join(" ");
21
29
  if (bare) {
22
- return (_jsx("textarea", { ...props, ref: ref, id: inputId, value: value, defaultValue: defaultValue, maxLength: maxLength, "aria-invalid": error ? "true" : undefined, className: textareaClassName }));
30
+ return (_jsx("textarea", { ...props, ref: ref, id: inputId, value: value, defaultValue: defaultValue, maxLength: maxLength, "aria-invalid": ariaInvalid, className: textareaClassName }));
23
31
  }
24
- return (_jsxs("div", { className: "w-full", "data-section": "textarea-div-620", children: [label && (_jsx(Label, { htmlFor: inputId, required: required, children: label })), _jsx("textarea", { ...props, ref: ref, id: inputId, value: value, defaultValue: defaultValue, maxLength: maxLength, "aria-invalid": error ? "true" : undefined, className: textareaClassName }), (error || helperText || (showCharCount && maxLength)) && (_jsxs("div", { className: "mt-1.5 flex items-start justify-between gap-3", "data-section": "textarea-div-621", children: [_jsx("div", { className: "min-w-0 flex-1", "data-section": "textarea-div-622", children: error ? (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: error })) : helperText ? (_jsx(Text, { size: "sm", variant: "secondary", children: helperText })) : null }), showCharCount && maxLength ? (_jsxs(Text, { size: "xs", variant: charCount >= maxLength ? "error" : "muted", className: "shrink-0 tabular-nums", children: [charCount, "/", maxLength] })) : null] }))] }));
32
+ return (_jsxs("div", { className: "w-full", "data-section": "textarea-div-620", children: [label && (_jsx(Label, { htmlFor: inputId, required: required, children: label })), _jsx("textarea", { ...props, ref: ref, id: inputId, value: value, defaultValue: defaultValue, maxLength: maxLength, "aria-invalid": ariaInvalid, className: textareaClassName }), (error || helperText || (showCharCount && maxLength)) && (_jsxs("div", { className: "mt-1.5 flex items-start justify-between gap-3", "data-section": "textarea-div-621", children: [_jsx("div", { className: "min-w-0 flex-1", "data-section": "textarea-div-622", children: error ? (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: error })) : helperText ? (_jsx(Text, { size: "sm", variant: "secondary", children: helperText })) : null }), showCharCount && maxLength ? (_jsxs(Text, { size: "xs", variant: charCount >= maxLength ? "error" : "muted", className: "shrink-0 tabular-nums", children: [charCount, "/", maxLength] })) : null] }))] }));
25
33
  });
26
34
  Textarea.displayName = "Textarea";
@@ -34,5 +34,5 @@ export function FieldCheckbox({ name, label, hint, checked, onChange, onBlur, di
34
34
  ctx?.setFieldTouched(name);
35
35
  onBlur?.();
36
36
  }
37
- return (_jsxs("div", { className: ["appkit-form-field", className].filter(Boolean).join(" "), children: [_jsx(Checkbox, { id: inputId, name: name, checked: checked, onChange: handleChange, onBlur: handleBlur, label: label, disabled: disabled, "aria-describedby": showError ? errorId : undefined }), !showError && hint && (_jsx(Text, { size: "sm", variant: "secondary", className: "appkit-form-field__hint", children: hint })), showError && (_jsx(Text, { id: errorId, size: "sm", variant: "error", className: "appkit-form-field__error", role: "alert", children: error }))] }));
37
+ return (_jsxs("div", { className: ["appkit-form-field", className].filter(Boolean).join(" "), children: [_jsx(Checkbox, { id: inputId, name: name, checked: checked, onChange: handleChange, onBlur: handleBlur, label: label, disabled: disabled, "aria-invalid": showError || undefined, "aria-describedby": showError ? errorId : undefined }), !showError && hint && (_jsx(Text, { size: "sm", variant: "secondary", className: "appkit-form-field__hint", children: hint })), showError && (_jsx(Text, { id: errorId, size: "sm", variant: "error", className: "appkit-form-field__error", role: "alert", children: error }))] }));
38
38
  }
@@ -9,21 +9,5 @@ export interface FormErrorListProps {
9
9
  ctx?: FormShellContextValue;
10
10
  className?: string;
11
11
  }
12
- /**
13
- * The `<ul>` of current validation errors, with a jump link per entry when
14
- * the form declares sections.
15
- *
16
- * WHY it is its own file: two surfaces render this exact list — the inline
17
- * `<FormErrorSummary>` beside the submit button on desktop, and the mobile
18
- * "Fix N issues" sheet in the bottom-chrome tier. Writing it twice would mean
19
- * a jump-link fix, an empty-state fix or a wording fix landing in one and not
20
- * the other, which is the bug-fix multiplier the Duplication Framework calls
21
- * out. There are exactly two consumers and they are the same list, so this is
22
- * a consolidation, not speculative abstraction.
23
- *
24
- * Renders nothing when there are no errors. It does NOT gate on
25
- * `submitAttempted` — that is the *summary's* decision, not the list's, and
26
- * the sheet needs to keep rendering while open.
27
- */
28
12
  export declare function FormErrorList({ ctx: ctxProp, className }: FormErrorListProps): import("react").JSX.Element | null;
29
13
  export default FormErrorList;
@@ -19,6 +19,39 @@ import { Span } from "../components/Typography.js";
19
19
  * `submitAttempted` — that is the *summary's* decision, not the list's, and
20
20
  * the sheet needs to keep rendering while open.
21
21
  */
22
+ /*
23
+ * 🛑 The two sides of this lookup use DIFFERENT key shapes, so an exact match
24
+ * silently loses every nested error.
25
+ *
26
+ * `errors` is keyed by the full dotted Zod path — `applyZodIssues` does
27
+ * `issue.path.map(String).join(".")`, giving `"address.line1"`. But
28
+ * `fieldToStepIndex` is built by `buildFieldToSectionIndex` from
29
+ * `SectionDef.fields`, which lists BARE field names — `"address"`. So
30
+ * `fieldToStepIndex["address.line1"]` was always `undefined`, the entry fell
31
+ * through to the plain `<Span>`, and the error that a section demonstrably
32
+ * owned rendered with no jump link.
33
+ *
34
+ * `SectionForm` already solved this for its own badge counts
35
+ * (`erroringSectionIds`, `errorCountFor`) with a prefix match. This is the same
36
+ * rule, so the list and the badges can no longer disagree about which section
37
+ * an error belongs to.
38
+ *
39
+ * Exact match is tried first: a field genuinely named `"address.line1"` must
40
+ * beat a prefix match on `"address"`.
41
+ */
42
+ function resolveStepIndex(ctx, key) {
43
+ const map = ctx.fieldToStepIndex;
44
+ if (!map)
45
+ return undefined;
46
+ const exact = map[key];
47
+ if (exact != null)
48
+ return exact;
49
+ for (const [field, index] of Object.entries(map)) {
50
+ if (key.startsWith(`${field}.`))
51
+ return index;
52
+ }
53
+ return undefined;
54
+ }
22
55
  export function FormErrorList({ ctx: ctxProp, className }) {
23
56
  const ambient = useContext(FormShellContext);
24
57
  const ctx = ctxProp ?? ambient;
@@ -28,7 +61,7 @@ export function FormErrorList({ ctx: ctxProp, className }) {
28
61
  if (entries.length === 0)
29
62
  return null;
30
63
  return (_jsx("ul", { className: ["appkit-formshell__error-summary__list", className].filter(Boolean).join(" "), children: entries.map(([key, message]) => {
31
- const stepIndex = ctx.fieldToStepIndex?.[key];
64
+ const stepIndex = resolveStepIndex(ctx, key);
32
65
  const stepLabel = stepIndex != null ? ctx.steps[stepIndex]?.label : undefined;
33
66
  return (_jsx("li", { children: stepLabel ? (_jsxs("button", { type: "button", className: "appkit-formshell__error-summary__step-link", onClick: () => ctx.goToStep(stepIndex), children: [_jsxs(Span, { weight: "semibold", children: [stepLabel, ":"] }), " ", message] })) : (_jsx(Span, { children: message })) }, key));
34
67
  }) }));
@@ -22,7 +22,16 @@
22
22
  *
23
23
  * Root-level `.refine()` issues (empty `path: []`, no owning field) are still
24
24
  * skipped — there is no field to attach them to, so the caller must surface
25
- * those some other way (they fall through to a toast in `surfaceError`).
25
+ * those some other way.
26
+ *
27
+ * 🛑 That last sentence used to end "(they fall through to a toast in
28
+ * `surfaceError`)", which was only true when EVERY issue was root-level.
29
+ * In the mixed case — one field error plus one cross-field `.refine()`, which
30
+ * is the common shape for "passwords must match" or "end date must be after
31
+ * start date" — `hasAttachableIssue` returned true, this function skipped the
32
+ * root issue, and `surfaceError` returned without a toast. The cross-field
33
+ * rule was enforced, the save was refused, and the user was told nothing
34
+ * about why. `rootLevelMessages` below is how a caller surfaces them.
26
35
  */
27
36
  export declare function applyZodIssues(issues: ReadonlyArray<{
28
37
  path?: ReadonlyArray<PropertyKey>;
@@ -32,3 +41,12 @@ export declare function applyZodIssues(issues: ReadonlyArray<{
32
41
  export declare function hasAttachableIssue(issues: ReadonlyArray<{
33
42
  path?: ReadonlyArray<PropertyKey>;
34
43
  }> | undefined): boolean;
44
+ /**
45
+ * The messages `applyZodIssues` cannot attach — the root-level `.refine()`
46
+ * ones with no owning field. A caller that has just run `applyZodIssues`
47
+ * should surface these separately, or they are silently discarded.
48
+ */
49
+ export declare function rootLevelMessages(issues: ReadonlyArray<{
50
+ path?: ReadonlyArray<PropertyKey>;
51
+ message: string;
52
+ }> | undefined): string[];
@@ -22,7 +22,16 @@
22
22
  *
23
23
  * Root-level `.refine()` issues (empty `path: []`, no owning field) are still
24
24
  * skipped — there is no field to attach them to, so the caller must surface
25
- * those some other way (they fall through to a toast in `surfaceError`).
25
+ * those some other way.
26
+ *
27
+ * 🛑 That last sentence used to end "(they fall through to a toast in
28
+ * `surfaceError`)", which was only true when EVERY issue was root-level.
29
+ * In the mixed case — one field error plus one cross-field `.refine()`, which
30
+ * is the common shape for "passwords must match" or "end date must be after
31
+ * start date" — `hasAttachableIssue` returned true, this function skipped the
32
+ * root issue, and `surfaceError` returned without a toast. The cross-field
33
+ * rule was enforced, the save was refused, and the user was told nothing
34
+ * about why. `rootLevelMessages` below is how a caller surfaces them.
26
35
  */
27
36
  export function applyZodIssues(issues, setFieldError) {
28
37
  for (const issue of issues) {
@@ -36,3 +45,16 @@ export function applyZodIssues(issues, setFieldError) {
36
45
  export function hasAttachableIssue(issues) {
37
46
  return Boolean(issues?.some((i) => i.path && i.path.length > 0));
38
47
  }
48
+ /**
49
+ * The messages `applyZodIssues` cannot attach — the root-level `.refine()`
50
+ * ones with no owning field. A caller that has just run `applyZodIssues`
51
+ * should surface these separately, or they are silently discarded.
52
+ */
53
+ export function rootLevelMessages(issues) {
54
+ if (!issues)
55
+ return [];
56
+ return issues
57
+ .filter((i) => !i.path || i.path.length === 0)
58
+ .map((i) => i.message)
59
+ .filter((m) => typeof m === "string" && m.length > 0);
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.61.0",
3
+ "version": "4.62.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"