@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
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React from "react";
4
- import { Button, Div, Heading, Modal, Pagination, Select, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text } from "../../../ui/index.js";
4
+ import { Button, Div, Heading, Modal, Pagination, Select, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text, TextLink } from "../../../ui/index.js";
5
+ import { ROUTES } from "../../../next/routing/route-map.js";
5
6
  import { useShipmentProjections, useShipmentItems } from "../hooks/useShipments.js";
6
7
  import { ShipmentItemLinkModal } from "./ShipmentItemLinkModal.js";
7
8
  import { formatCurrency } from "../../../utils/number.formatter.js";
@@ -25,7 +26,7 @@ export function AdminShipmentProjectionsView() {
25
26
  const [sorts, setSorts] = React.useState(SORT_OPTIONS[0].value);
26
27
  const { lots, meta, isLoading, refetch } = useShipmentProjections({ page, pageSize, sorts });
27
28
  const [pickerLot, setPickerLot] = React.useState(null);
28
- return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { layout: "flex", align: "center", justify: "between", children: [_jsx(Heading, { level: 2, children: "Projections" }), _jsx(Select, { "aria-label": "Sort by", value: sorts, onValueChange: setSorts, options: SORT_OPTIONS })] }), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : lots.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No lots to project yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Lot" }), _jsx(Th, { children: "Items" }), _jsx(Th, { children: "Landed Cost" }), _jsx(Th, { children: "Projected Revenue" }), _jsx(Th, { children: "Projected Profit" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: lots.map((lot) => (_jsxs(Tr, { children: [_jsx(Td, { children: lot.lotName }), _jsx(Td, { children: lot.itemCount }), _jsx(Td, { children: formatCurrency(lot.totalLandedCost) }), _jsx(Td, { children: formatCurrency(lot.projectedRevenue) }), _jsx(Td, { className: lot.projectedProfit < 0 ? "text-error" : "text-success", children: formatCurrency(lot.projectedProfit) }), _jsx(Td, { children: _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setPickerLot(lot), children: "Create pre-order link" }) })] }, lot.id))) })] })), meta && meta.totalPages > 1 && (_jsx(Pagination, { currentPage: page, totalPages: meta.totalPages, onPageChange: setPage, pageSize: pageSize, onPageSizeChange: (size) => {
29
+ return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { layout: "flex", align: "center", justify: "between", children: [_jsx(Heading, { level: 2, children: "Projections" }), _jsx(Select, { "aria-label": "Sort by", value: sorts, onValueChange: setSorts, options: SORT_OPTIONS })] }), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : lots.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No lots to project yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Lot" }), _jsx(Th, { children: "Items" }), _jsx(Th, { children: "Landed Cost" }), _jsx(Th, { children: "Projected Revenue" }), _jsx(Th, { children: "Projected Profit" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: lots.map((lot) => (_jsxs(Tr, { children: [_jsx(Td, { children: _jsx(TextLink, { href: String(ROUTES.ADMIN.SHIPMENT_LOT_ITEMS(lot.shipmentId, lot.id)), children: lot.lotName }) }), _jsx(Td, { children: lot.itemCount }), _jsx(Td, { children: formatCurrency(lot.totalLandedCost) }), _jsx(Td, { children: formatCurrency(lot.projectedRevenue) }), _jsx(Td, { className: lot.projectedProfit < 0 ? "text-error" : "text-success", children: formatCurrency(lot.projectedProfit) }), _jsx(Td, { children: _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setPickerLot(lot), children: "Create pre-order link" }) })] }, lot.id))) })] })), meta && meta.totalPages > 1 && (_jsx(Pagination, { currentPage: page, totalPages: meta.totalPages, onPageChange: setPage, pageSize: pageSize, onPageSizeChange: (size) => {
29
30
  setPageSize(size);
30
31
  setPage(1);
31
32
  } })), pickerLot && (_jsx(LotItemPickerModal, { lot: pickerLot, onClose: () => setPickerLot(null), onLinked: refetch }))] }));
@@ -37,6 +37,7 @@ import { Alert, Button, Card, Form, Heading, Stack, Text } from "../../../ui/ind
37
37
  import { FieldInput } from "../../../ui/forms/FieldInput.js";
38
38
  import { FieldSelect } from "../../../ui/forms/FieldSelect.js";
39
39
  import { TIER_HELP } from "../constants/help-content.js";
40
+ import { toUserMessage } from "../../../errors/error-display-map.js";
40
41
  /** Every tier, unlike the studio — see the header. */
41
42
  const TIERS = ["draft", "standard", "precise", "max"];
42
43
  export function StickerCommitPanel({ file, dimensions, onCommit, onCommitted, }) {
@@ -80,7 +81,7 @@ export function StickerCommitPanel({ file, dimensions, onCommit, onCommitted, })
80
81
  * buyer, which this is not — and the routes already map their own
81
82
  * failures to user-safe text.
82
83
  */
83
- setError(n.message);
84
+ setError(toUserMessage(n.code, undefined, { fallback: "We couldn't commit that sheet. Please try again." }));
84
85
  }
85
86
  finally {
86
87
  setPending(false);
@@ -32,6 +32,7 @@ import { useCallback, useEffect, useState } from "react";
32
32
  import { normalizeError } from "../../../errors/normalize.js";
33
33
  import { Alert, Badge, Button, Card, Heading, Row, Span, Stack, Text, } from "../../../ui/index.js";
34
34
  import { FieldSelect } from "../../../ui/forms/FieldSelect.js";
35
+ import { toUserMessage } from "../../../errors/error-display-map.js";
35
36
  const PROFILES = [
36
37
  { value: "print-shop", label: "Print shop (Roland / Graphtec)" },
37
38
  { value: "cricut-cut", label: "Cricut Joy 2 — cut only" },
@@ -83,7 +84,7 @@ export function StickerPrintPanel({ loadReport, onDownloadCutFile, onDownloadMos
83
84
  * Rendering both the same way would have an operator confirming a DPI to
84
85
  * fix a lapsed session.
85
86
  */
86
- setError(n.message);
87
+ setError(toUserMessage(n.code, undefined, { fallback: "We couldn't prepare that print. Please try again." }));
87
88
  setReport(null);
88
89
  }
89
90
  finally {
@@ -23,11 +23,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
23
23
  * belongs to which stage.
24
24
  *
25
25
  * 🛑 Built on `<SectionForm>`, not a hand-rolled `<Form>`. Three controls is
26
- * where a form stops being trivial, and sectionising is what buys the error
27
- * summary, the jump-to-field link and the pinned mobile action bar for free —
26
+ * where a form stops being trivial, and sectionising is what buys the
27
+ * jump-to-field link and the pinned mobile action bar —
28
28
  * audit-form-sectionised is strict-zero on exactly this, and its grandfather
29
29
  * list is empty and closed.
30
30
  *
31
+ * 🛑 The error summary is NOT free, and this comment used to claim it was.
32
+ * `SectionForm` deliberately does not own a `FormShellContext` — it consumes
33
+ * whichever one the owning view mounts, and tolerates none. So a view that
34
+ * only renders `<SectionForm schema={…}>` gets section badges and no readable
35
+ * message at all. The view must mount the context and render
36
+ * `<FormErrorSummary>` itself, which is what the bottom of this file now does.
37
+ *
31
38
  * EXPORTS: StickerRetryPanel
32
39
  *
33
40
  * @tag domain:sticker-trace
@@ -43,11 +50,14 @@ import { normalizeError } from "../../../errors/normalize.js";
43
50
  import { Alert, Card, Stack, Text } from "../../../ui/index.js";
44
51
  import { FieldInput } from "../../../ui/forms/FieldInput.js";
45
52
  import { FieldSelect } from "../../../ui/forms/FieldSelect.js";
53
+ import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary.js";
54
+ import { FormShellContext, useFormShellState } from "../../../ui/forms/FormShell.js";
46
55
  import { SectionForm } from "../../shell/SectionForm.js";
47
56
  import { TRACE_STAGES } from "../../../_internal/shared/features/sticker-trace/types.js";
48
57
  import { resolveResumeStage, explainResume, } from "../../../_internal/shared/features/sticker-trace/resume.js";
49
58
  import { paramsForTier } from "../../../_internal/shared/features/sticker-trace/config.js";
50
59
  import { TIER_HELP } from "../constants/help-content.js";
60
+ import { toUserMessage } from "../../../errors/error-display-map.js";
51
61
  const TIERS = ["draft", "standard", "precise", "max"];
52
62
  /**
53
63
  * 🛑 `.min(1)` on the name, mirroring `requireName` on the server.
@@ -103,7 +113,7 @@ export function StickerRetryPanel({ source, artifactsPruned = false, onSubmit, o
103
113
  * operator looking at a form that did nothing — and the likeliest causes,
104
114
  * a lapsed session or a rejected name, are both named by the message.
105
115
  */
106
- setError(n.message);
116
+ setError(toUserMessage(n.code, undefined, { fallback: "We couldn't retry that run. Please try again." }));
107
117
  }
108
118
  finally {
109
119
  setPending(false);
@@ -124,5 +134,15 @@ export function StickerRetryPanel({ source, artifactsPruned = false, onSubmit, o
124
134
  render: ({ values: v, onChange: set }) => (_jsxs(Stack, { gap: "comfortable", children: [_jsx(FieldSelect, { name: "requested", label: "Resume from", value: v.requested, options: TRACE_STAGES.map((s) => ({ value: s, label: s })), onChange: (next) => set({ requested: next }) }), notice ? _jsx(Alert, { variant: "warning", children: notice }) : null, artifactsPruned ? (_jsx(Alert, { variant: "warning", children: "This run's artifacts have been cleared, so the retry starts from the beginning whatever you pick here." })) : null, !source.params ? (_jsx(Text, { size: "xs", color: "muted", children: "This run predates parameter recording, so the resume point cannot be checked ahead of time \u2014 the server will decide and tell you." })) : null] })),
125
135
  },
126
136
  ], [source.name, source.params, notice, artifactsPruned]);
127
- return (_jsx(Card, { variant: "default", children: _jsxs(Stack, { gap: "comfortable", children: [error ? _jsx(Alert, { variant: "error", children: error }) : null, _jsx(SectionForm, { sections: sections, values: values, onChange: onChange, onSubmit: submit, schema: retrySchema, submitLabel: "Start retry", isLoading: pending, onCancel: onCancel, cancelLabel: "Cancel" })] }) }));
137
+ /*
138
+ * 🛑 `SectionForm` does NOT provide a FormShellContext — it reads whichever
139
+ * one the owning view mounts, and may legitimately get none. The header of
140
+ * this file says sectionising buys "the error summary … for free", and that
141
+ * was wrong: nothing here mounted a context or rendered a summary, so a
142
+ * schema rejection set the section badge and produced no readable message
143
+ * anywhere. The file passed `audit-form-error-summary` only because a bare
144
+ * `<SectionForm schema=>` was not a recognised call site.
145
+ */
146
+ const { shellCtx } = useFormShellState(retrySchema);
147
+ return (_jsx(Card, { variant: "default", children: _jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Stack, { gap: "comfortable", children: [error ? _jsx(Alert, { variant: "error", children: error }) : null, _jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: values, onChange: onChange, onSubmit: submit, schema: retrySchema, submitLabel: "Start retry", isLoading: pending, onCancel: onCancel, cancelLabel: "Cancel" })] }) }) }));
128
148
  }
@@ -30,6 +30,7 @@ import { PARAM_HELP, TIER_HELP } from "../constants/help-content.js";
30
30
  import { useLiveTrace } from "../hooks/useLiveTrace.js";
31
31
  import { canvasCodec } from "../../../_internal/client/features/sticker-trace/canvas-codec.js";
32
32
  import { normalizeError } from "../../../errors/normalize.js";
33
+ import { toUserMessage } from "../../../errors/error-display-map.js";
33
34
  /*
34
35
  * Only `draft` is offered. The hook runs on the main thread and draft's
35
36
  * 1200px cap is what keeps one trace near a second; `standard`'s 3000 would
@@ -103,7 +104,7 @@ export function StickerTraceStudio({ renderCommit }) {
103
104
  // the operator is the one who needs to know the commit is blocked.
104
105
  if (live) {
105
106
  setNatural(null);
106
- setProbeError(n.message);
107
+ setProbeError(toUserMessage(n.code, undefined, { fallback: "That probe could not be run." }));
107
108
  }
108
109
  });
109
110
  return () => {
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useQuery, useQueryClient } from "@tanstack/react-query";
4
- import { Button, Card, Details, Div, Row, Stack, Summary, Text } from "../../../ui/index.js";
4
+ import { Button, Card, Details, Div, Row, Stack, Summary, Text, TextLink } from "../../../ui/index.js";
5
+ import { ROUTES } from "../../../next/routing/route-map.js";
5
6
  import { MediaImage } from "../../media/MediaImage.js";
6
7
  import { apiClient } from "../../../http/index.js";
7
8
  import { useApiMutation } from "../../../client/api/useApiMutation.js";
@@ -50,6 +51,6 @@ export function AdminTesterFeedbackIssuesView() {
50
51
  const sortedPhases = Array.from(byPhase.keys()).sort((a, b) => a - b);
51
52
  return (_jsx(Stack, { gap: "md", children: sortedPhases.map((phase) => {
52
53
  const phaseIssues = byPhase.get(phase);
53
- return (_jsxs(Details, { tone: "card", defaultOpen: sortedPhases.length === 1, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phase, " (", phaseIssues.length, " issue", phaseIssues.length === 1 ? "" : "s", ")"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: phaseIssues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", children: issue.label }), _jsxs(Text, { size: "sm", color: "muted", children: [issue.groupLabel, " \u203A ", issue.pageLabel] }), _jsxs(Text, { size: "sm", color: "muted", children: ["Reported by ", issue.testerDisplayName] }), issue.comment && _jsx(Text, { size: "sm", children: issue.comment }), issue.screenshotUrl && (_jsx(MediaImage, { src: issue.screenshotUrl, alt: "Tester screenshot", size: "card" })), issue.bugConfirmed ? (_jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsxs(Text, { size: "sm", weight: "semibold", color: "success", children: ["\uD83D\uDC1B Confirmed \u2014 credited to ", issue.bugHunterName ?? "unknown tester"] }), !issue.supersededByItemId && (_jsx(Button, { size: "sm", action: ACTIONS.ADMIN["reopen-checklist-item"], isLoading: reopenMutation.isPending, onClick: () => reopenMutation.mutate(issue.checklistItemId) }))] })) : (_jsx(Div, { children: _jsx(Button, { size: "sm", action: ACTIONS.ADMIN["confirm-bug"], isLoading: confirmBugMutation.isPending, onClick: () => confirmBugMutation.mutate(issue.id) }) }))] }) }, issue.id))) }) })] }, phase));
54
+ return (_jsxs(Details, { tone: "card", defaultOpen: sortedPhases.length === 1, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phase, " (", phaseIssues.length, " issue", phaseIssues.length === 1 ? "" : "s", ")"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: phaseIssues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsx(TextLink, { href: String(ROUTES.ADMIN.TESTER_CHECKLIST_EDIT(issue.checklistItemId)), weight: "semibold", children: issue.label }), _jsxs(Text, { size: "sm", color: "muted", children: [issue.groupLabel, " \u203A ", issue.pageLabel] }), _jsxs(Text, { size: "sm", color: "muted", children: ["Reported by ", issue.testerDisplayName] }), issue.comment && _jsx(Text, { size: "sm", children: issue.comment }), issue.screenshotUrl && (_jsx(MediaImage, { src: issue.screenshotUrl, alt: "Tester screenshot", size: "card" })), issue.bugConfirmed ? (_jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsxs(Text, { size: "sm", weight: "semibold", color: "success", children: ["\uD83D\uDC1B Confirmed \u2014 credited to ", issue.bugHunterName ?? "unknown tester"] }), !issue.supersededByItemId && (_jsx(Button, { size: "sm", action: ACTIONS.ADMIN["reopen-checklist-item"], isLoading: reopenMutation.isPending, onClick: () => reopenMutation.mutate(issue.checklistItemId) }))] })) : (_jsx(Div, { children: _jsx(Button, { size: "sm", action: ACTIONS.ADMIN["confirm-bug"], isLoading: confirmBugMutation.isPending, onClick: () => confirmBugMutation.mutate(issue.id) }) }))] }) }, issue.id))) }) })] }, phase));
54
55
  }) }));
55
56
  }
package/dist/index.d.ts CHANGED
@@ -52,6 +52,10 @@ export type { FieldSelectProps } from "./ui/index";
52
52
  export { FieldSelect } from "./ui/index";
53
53
  export type { FieldCheckboxProps } from "./ui/index";
54
54
  export { FieldCheckbox } from "./ui/index";
55
+ export type { FieldTextareaProps } from "./ui/index";
56
+ export { FieldTextarea } from "./ui/index";
57
+ export type { FormErrorSummaryProps } from "./ui/index";
58
+ export { FormErrorSummary } from "./ui/index";
55
59
  export { GRID_MAP } from "./ui/index";
56
60
  export { ImageGallery } from "./ui/index";
57
61
  export { ImageLightbox } from "./ui/index";
@@ -2940,7 +2944,7 @@ export type { OwnerQuickActionsProps, OwnerQuickActionsResource } from "./featur
2940
2944
  export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
2941
2945
  export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules";
2942
2946
  export type { ListingCheckoutRule, CategoryCheckoutRule, CartItemProductPair, OrderItemInput, RefundPolicy, } from "./_internal/shared/checkout/rules";
2943
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
2947
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, REVIEW_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
2944
2948
  export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
2945
2949
  export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, AD_TRACKED_FIELDS, BLOG_FIELDS, BRAND_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, COUPON_USAGE_FIELDS, CAROUSEL_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
2946
2950
  export { SORT_DIR, sortBy } from "./constants/sort";
package/dist/index.js CHANGED
@@ -138,6 +138,8 @@ export { FormGroup } from "./ui/index.js";
138
138
  export { FieldInput } from "./ui/index.js";
139
139
  export { FieldSelect } from "./ui/index.js";
140
140
  export { FieldCheckbox } from "./ui/index.js";
141
+ export { FieldTextarea } from "./ui/index.js";
142
+ export { FormErrorSummary } from "./ui/index.js";
141
143
  // [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
142
144
  // GRID_MAP - Constant used across modules.
143
145
  export { GRID_MAP } from "./ui/index.js";
@@ -5249,7 +5251,7 @@ export { RecordOwnerQuickActions } from "./features/admin/components/RecordOwner
5249
5251
  // Checkout rule registry â€" pure TS, safe in both client and server bundles.
5250
5252
  export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules/index.js";
5251
5253
  // Media upload limits â€" pure constants, safe in both client and server bundles.
5252
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits.js";
5254
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, REVIEW_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits.js";
5253
5255
  // ── Field name constants — canonical source of truth for all Firestore field names ──
5254
5256
  // NOTE: STORE_FIELDS, CATEGORY_FIELDS, EVENT_FIELDS, EVENT_ENTRY_FIELDS, PAYOUT_FIELDS,
5255
5257
  // COUPON_FIELDS, CART_FIELDS, USER_FIELDS, ADDRESS_FIELDS, NOTIFICATION_FIELDS,
@@ -182,6 +182,8 @@ export declare const DEFAULT_ROUTE_MAP: {
182
182
  readonly CLAIMED_COUPONS: "/user/coupons";
183
183
  /** Points balance + ledger. 1 point = ₹1, spendable at checkout. */
184
184
  readonly POINTS: "/user/points";
185
+ readonly SESSIONS: "/user/sessions";
186
+ readonly PAYMENT_METHODS: "/user/payment-methods";
185
187
  readonly BECOME_SELLER: "/user/become-seller";
186
188
  readonly CART: "/cart";
187
189
  readonly CHECKOUT: "/checkout";
@@ -634,6 +636,8 @@ export declare const ROUTES: {
634
636
  readonly CLAIMED_COUPONS: "/user/coupons";
635
637
  /** Points balance + ledger. 1 point = ₹1, spendable at checkout. */
636
638
  readonly POINTS: "/user/points";
639
+ readonly SESSIONS: "/user/sessions";
640
+ readonly PAYMENT_METHODS: "/user/payment-methods";
637
641
  readonly BECOME_SELLER: "/user/become-seller";
638
642
  readonly CART: "/cart";
639
643
  readonly CHECKOUT: "/checkout";
@@ -302,6 +302,15 @@ export const DEFAULT_ROUTE_MAP = {
302
302
  CLAIMED_COUPONS: "/user/coupons",
303
303
  /** Points balance + ledger. 1 point = ₹1, spendable at checkout. */
304
304
  POINTS: "/user/points",
305
+ /*
306
+ * 🛑 Both of these had a COMPLETE API and no page. `GET /api/user/sessions`
307
+ * + `DELETE [id]`, and `GET/POST /api/user/payment-methods` + `DELETE
308
+ * [id]`, were all live with zero consumers — and `BuyerAccountGuideView`
309
+ * already told buyers to "view all devices signed in at My Account →
310
+ * Sessions", pointing at a security control that did not exist.
311
+ */
312
+ SESSIONS: "/user/sessions",
313
+ PAYMENT_METHODS: "/user/payment-methods",
305
314
  BECOME_SELLER: "/user/become-seller",
306
315
  CART: "/cart",
307
316
  CHECKOUT: "/checkout",
@@ -2801,8 +2801,8 @@ export declare const SCHEMAS: {
2801
2801
  }, import("zod").ZodTypeAny, "passthrough">>]>>;
2802
2802
  }, "strip", import("zod").ZodTypeAny, {
2803
2803
  status: "paid" | "pending" | "overdue";
2804
- amount: number;
2805
2804
  index: number;
2805
+ amount: number;
2806
2806
  dueDate: string | Date | import("zod").objectOutputType<{
2807
2807
  toDate: import("zod").ZodFunction<import("zod").ZodTuple<[], import("zod").ZodUnknown>, import("zod").ZodDate>;
2808
2808
  }, import("zod").ZodTypeAny, "passthrough">;
@@ -2816,8 +2816,8 @@ export declare const SCHEMAS: {
2816
2816
  }, import("zod").ZodTypeAny, "passthrough"> | undefined;
2817
2817
  }, {
2818
2818
  status: "paid" | "pending" | "overdue";
2819
- amount: number;
2820
2819
  index: number;
2820
+ amount: number;
2821
2821
  dueDate: string | Date | import("zod").objectInputType<{
2822
2822
  toDate: import("zod").ZodFunction<import("zod").ZodTuple<[], import("zod").ZodUnknown>, import("zod").ZodDate>;
2823
2823
  }, import("zod").ZodTypeAny, "passthrough">;
@@ -3080,8 +3080,8 @@ export declare const SCHEMAS: {
3080
3080
  emiSurchargeAmount?: number | undefined;
3081
3081
  emiInstallments?: {
3082
3082
  status: "paid" | "pending" | "overdue";
3083
- amount: number;
3084
3083
  index: number;
3084
+ amount: number;
3085
3085
  dueDate: string | Date | import("zod").objectOutputType<{
3086
3086
  toDate: import("zod").ZodFunction<import("zod").ZodTuple<[], import("zod").ZodUnknown>, import("zod").ZodDate>;
3087
3087
  }, import("zod").ZodTypeAny, "passthrough">;
@@ -3234,8 +3234,8 @@ export declare const SCHEMAS: {
3234
3234
  emiSurchargeAmount?: number | undefined;
3235
3235
  emiInstallments?: {
3236
3236
  status: "paid" | "pending" | "overdue";
3237
- amount: number;
3238
3237
  index: number;
3238
+ amount: number;
3239
3239
  dueDate: string | Date | import("zod").objectInputType<{
3240
3240
  toDate: import("zod").ZodFunction<import("zod").ZodTuple<[], import("zod").ZodUnknown>, import("zod").ZodDate>;
3241
3241
  }, import("zod").ZodTypeAny, "passthrough">;
@@ -23,12 +23,12 @@ export declare const sieveFilterClauseSchema: z.ZodObject<{
23
23
  value: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>, "many">]>;
24
24
  }, "strip", z.ZodTypeAny, {
25
25
  value: string | number | boolean | (string | number | boolean)[] | null;
26
- op: "==" | "!=" | "<" | "<=" | ">" | ">=" | "array-contains" | "in" | "not-in" | "array-contains-any";
27
26
  field: string;
27
+ op: "==" | "!=" | "<" | "<=" | ">" | ">=" | "array-contains" | "in" | "not-in" | "array-contains-any";
28
28
  }, {
29
29
  value: string | number | boolean | (string | number | boolean)[] | null;
30
- op: "==" | "!=" | "<" | "<=" | ">" | ">=" | "array-contains" | "in" | "not-in" | "array-contains-any";
31
30
  field: string;
31
+ op: "==" | "!=" | "<" | "<=" | ">" | ">=" | "array-contains" | "in" | "not-in" | "array-contains-any";
32
32
  }>;
33
33
  export type SieveFilterClause = z.infer<typeof sieveFilterClauseSchema>;
34
34
  export type SieveWhereOp = z.infer<typeof sieveWhereOpSchema>;
@@ -36,8 +36,8 @@ export declare const sieveSortClauseSchema: z.ZodObject<{
36
36
  field: z.ZodString;
37
37
  direction: z.ZodDefault<z.ZodEnum<["asc", "desc"]>>;
38
38
  }, "strip", z.ZodTypeAny, {
39
- direction: "desc" | "asc";
40
39
  field: string;
40
+ direction: "desc" | "asc";
41
41
  }, {
42
42
  field: string;
43
43
  direction?: "desc" | "asc" | undefined;
package/dist/server.d.ts CHANGED
@@ -502,7 +502,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
502
502
  export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
503
503
  export { applyMediaContextGuards, CONTEXT_LIMITS, } from "./_internal/server/features/media/contextGuards";
504
504
  export type { GuardResult, GuardError, GuardSuccess } from "./_internal/server/features/media/contextGuards";
505
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
505
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, REVIEW_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
506
506
  export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
507
507
  export { getServerPermissions, checkPermission, checkAnyPermission, makeAdminSectionLayout, } from "./_internal/server/features/auth/permissions";
508
508
  export type { ResolvedPermissions, AdminSectionLayoutOpts, } from "./_internal/server/features/auth/permissions";
package/dist/server.js CHANGED
@@ -1435,7 +1435,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
1435
1435
  // Media context guards — shared by /api/media/sign + legacy /api/media/upload.
1436
1436
  export { applyMediaContextGuards, CONTEXT_LIMITS, } from "./_internal/server/features/media/contextGuards.js";
1437
1437
  // Media upload limits — shared by /api/media/sign + /api/media/finalize + /api/media/upload.
1438
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits.js";
1438
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, IMAGE_ACCEPT_ATTR, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, REVIEW_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits.js";
1439
1439
  // ── RBAC server helpers ───────────────────────────────────────────────────────
1440
1440
  // getServerPermissions, checkPermission, makeAdminSectionLayout
1441
1441
  export { getServerPermissions, checkPermission, checkAnyPermission, makeAdminSectionLayout, } from "./_internal/server/features/auth/permissions.js";
@@ -19,13 +19,21 @@ export function Checkbox({ label, suffix, error, indeterminate = false, type = "
19
19
  const generatedId = React.useId();
20
20
  const inputId = id ?? generatedId;
21
21
  const inputRef = React.useRef(null);
22
+ /*
23
+ * A caller's own `aria-invalid` wins. `{...props}` is spread first, so
24
+ * re-specifying the attribute afterwards used to overwrite an incoming
25
+ * `"true"` with `undefined` — and `FieldCheckbox` sets it itself while
26
+ * withholding `error` (to avoid a double message). See the full writeup in
27
+ * `Input.tsx`; the same bug existed here.
28
+ */
29
+ const ariaInvalid = props["aria-invalid"] ?? (error ? "true" : undefined);
22
30
  React.useEffect(() => {
23
31
  if (inputRef.current) {
24
32
  inputRef.current.indeterminate = indeterminate;
25
33
  }
26
34
  }, [indeterminate]);
27
35
  if (bare) {
28
- return (_jsx("input", { ...props, ref: inputRef, id: inputId, type: type, checked: checked, disabled: disabled, "aria-invalid": error ? "true" : undefined, className: className }));
36
+ return (_jsx("input", { ...props, ref: inputRef, id: inputId, type: type, checked: checked, disabled: disabled, "aria-invalid": ariaInvalid, className: className }));
29
37
  }
30
38
  // A checkbox with no label/suffix has no text to lay out beside the box, so
31
39
  // the default `width: 100%` on the root serves no purpose and actively harms
@@ -43,7 +51,7 @@ export function Checkbox({ label, suffix, error, indeterminate = false, type = "
43
51
  disabled ? UI_CHECKBOX.labelDisabled : "",
44
52
  ]
45
53
  .filter(Boolean)
46
- .join(" "), children: [_jsxs("span", { className: UI_CHECKBOX.boxWrap, children: [_jsx("input", { ...props, ref: inputRef, id: inputId, type: type, checked: checked, disabled: disabled, "aria-invalid": error ? "true" : undefined, className: [
54
+ .join(" "), children: [_jsxs("span", { className: UI_CHECKBOX.boxWrap, children: [_jsx("input", { ...props, ref: inputRef, id: inputId, type: type, checked: checked, disabled: disabled, "aria-invalid": ariaInvalid, className: [
47
55
  UI_CHECKBOX.input,
48
56
  error ? UI_CHECKBOX.inputError : "",
49
57
  className,
@@ -5,7 +5,14 @@ import { Label, Span, Text } from "./Typography.js";
5
5
  export const DateInput = React.forwardRef(function DateInput({ label, error, helperText, value, onChange, disabled, required, id, className = "", ...props }, ref) {
6
6
  const generatedId = React.useId();
7
7
  const inputId = id ?? generatedId;
8
- return (_jsxs("div", { className: "w-full", children: [label && (_jsx(Label, { htmlFor: inputId, required: required, className: "appkit-form-field__label mb-1.5", children: label })), _jsx("input", { ...props, ref: ref, id: inputId, type: "date", value: value, disabled: disabled, required: required, "aria-invalid": error ? "true" : undefined, onChange: (e) => onChange?.(e.target.value), className: ["appkit-input", error ? "appkit-input--error" : "", className].filter(Boolean).join(" ") }), error ? (_jsx(Text, { size: "sm", variant: "error", role: "alert", className: "mt-1.5", children: error })) : helperText ? (_jsx(Text, { size: "sm", variant: "secondary", className: "mt-1.5", children: helperText })) : null] }));
8
+ /*
9
+ * A caller's own `aria-invalid` wins. `{...props}` is spread first, so
10
+ * re-specifying the attribute afterwards overwrites an incoming "true"
11
+ * with `undefined` — the defect that left every Field*-built form in the
12
+ * app with no aria-invalid at all. See the full writeup in `Input.tsx`.
13
+ */
14
+ const ariaInvalid = props["aria-invalid"] ?? (error ? "true" : undefined);
15
+ return (_jsxs("div", { className: "w-full", children: [label && (_jsx(Label, { htmlFor: inputId, required: required, className: "appkit-form-field__label mb-1.5", children: label })), _jsx("input", { ...props, ref: ref, id: inputId, type: "date", value: value, disabled: disabled, required: required, "aria-invalid": ariaInvalid, onChange: (e) => onChange?.(e.target.value), className: ["appkit-input", error ? "appkit-input--error" : "", className].filter(Boolean).join(" ") }), error ? (_jsx(Text, { size: "sm", variant: "error", role: "alert", className: "mt-1.5", children: error })) : helperText ? (_jsx(Text, { size: "sm", variant: "secondary", className: "mt-1.5", children: helperText })) : null] }));
9
16
  });
10
17
  export function DateRangeInput({ label, startLabel = "From", endLabel = "To", startValue, endValue, onStartChange, onEndChange, error, helperText, disabled, required, startMin, startMax, endMin, endMax, }) {
11
18
  return (_jsxs("div", { className: "w-full", children: [label && (_jsx(Text, { size: "sm", className: "appkit-form-field__label mb-1.5 block font-medium", children: label })), _jsxs("div", { className: "flex items-end gap-2", children: [_jsx(DateInput, { label: startLabel, value: startValue, onChange: onStartChange, disabled: disabled, required: required, min: startMin, max: startMax ?? endValue }), _jsx(Span, { size: "sm", className: "mb-2.5 shrink-0 text-[var(--appkit-color-text-muted)]", children: "\u2013" }), _jsx(DateInput, { label: endLabel, value: endValue, onChange: onEndChange, disabled: disabled, min: endMin ?? startValue, max: endMax })] }), error ? (_jsx(Text, { size: "sm", variant: "error", role: "alert", className: "mt-1.5", children: error })) : helperText ? (_jsx(Text, { size: "sm", variant: "secondary", className: "mt-1.5", children: helperText })) : null] }));
@@ -13,6 +13,26 @@ export const Input = React.forwardRef(function Input({ label, error, helperText,
13
13
  : variant === "ghost"
14
14
  ? "border-transparent bg-transparent shadow-none"
15
15
  : "";
16
+ /*
17
+ * 🛑 A CALLER'S OWN `aria-invalid` MUST WIN — it did not, and that broke
18
+ * every form in the app.
19
+ *
20
+ * `{...props}` is spread FIRST on both `<input>` elements below, so
21
+ * re-specifying `aria-invalid={error ? "true" : undefined}` afterwards
22
+ * OVERWROTE an incoming `"true"` with `undefined` (JSX: later wins).
23
+ *
24
+ * That is not hypothetical. `FieldInput` sets `aria-invalid` itself and
25
+ * deliberately WITHHOLDS `error` from this component, so the message is not
26
+ * rendered twice — which means `error` here is `undefined` exactly when the
27
+ * field is invalid. Net effect: no `Field*`-built form ever emitted
28
+ * `aria-invalid`, and `SectionForm`'s jump-to-error
29
+ * (`querySelector("[aria-invalid='true']")?.focus()`) silently focused
30
+ * nothing, every time.
31
+ *
32
+ * Same precedence rule as `bareAriaLabel` below: fill the gap, never
33
+ * override an explicit choice.
34
+ */
35
+ const ariaInvalid = props["aria-invalid"] ?? (error ? "true" : undefined);
16
36
  const inputClassName = [
17
37
  "appkit-input",
18
38
  icon ? "pl-10" : "",
@@ -45,10 +65,10 @@ export const Input = React.forwardRef(function Input({ label, error, helperText,
45
65
  * overrides an explicit choice.
46
66
  */
47
67
  const bareAriaLabel = props["aria-label"] ?? (typeof label === "string" ? label : undefined);
48
- return (_jsx("input", { ...props, ref: ref, id: inputId, disabled: disabled, "aria-label": bareAriaLabel, "aria-invalid": error ? "true" : undefined, className: inputClassName }));
68
+ return (_jsx("input", { ...props, ref: ref, id: inputId, disabled: disabled, "aria-label": bareAriaLabel, "aria-invalid": ariaInvalid, className: inputClassName }));
49
69
  }
50
70
  const adornment = rightIcon ??
51
71
  (success && !error ? (_jsx("svg", { className: CLS_INPUT_SUCCESS_ICON, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M16.704 5.29a1 1 0 010 1.42l-7.2 7.2a1 1 0 01-1.415 0l-3.2-3.2a1 1 0 111.414-1.42l2.493 2.494 6.493-6.494a1 1 0 011.415 0z", clipRule: "evenodd" }) })) : null);
52
- return (_jsxs("div", { className: "w-full", "data-section": "input-div-527", children: [label && (_jsx(Label, { htmlFor: inputId, required: required, children: label })), _jsxs("div", { className: "relative", "data-section": "input-div-528", children: [icon && (_jsx(Div, { className: "pointer-events-none absolute inset-y-0 left-3 flex items-center text-[var(--appkit-color-text-faint)]", "data-section": "input-div-529", children: icon })), _jsx("input", { ...props, ref: ref, id: inputId, disabled: disabled, "aria-invalid": error ? "true" : undefined, className: inputClassName }), adornment && (_jsx(Div, { className: "pointer-events-none absolute inset-y-0 right-3 flex items-center text-[var(--appkit-color-text-faint)]", "data-section": "input-div-530", children: adornment }))] }), error ? (_jsx(Text, { size: "sm", variant: "error", className: "mt-1.5", role: "alert", children: error })) : helperText ? (_jsx(Text, { size: "sm", variant: "secondary", className: "mt-1.5", children: helperText })) : null] }));
72
+ return (_jsxs("div", { className: "w-full", "data-section": "input-div-527", children: [label && (_jsx(Label, { htmlFor: inputId, required: required, children: label })), _jsxs("div", { className: "relative", "data-section": "input-div-528", children: [icon && (_jsx(Div, { className: "pointer-events-none absolute inset-y-0 left-3 flex items-center text-[var(--appkit-color-text-faint)]", "data-section": "input-div-529", children: icon })), _jsx("input", { ...props, ref: ref, id: inputId, disabled: disabled, "aria-invalid": ariaInvalid, className: inputClassName }), adornment && (_jsx(Div, { className: "pointer-events-none absolute inset-y-0 right-3 flex items-center text-[var(--appkit-color-text-faint)]", "data-section": "input-div-530", children: adornment }))] }), error ? (_jsx(Text, { size: "sm", variant: "error", className: "mt-1.5", role: "alert", children: error })) : helperText ? (_jsx(Text, { size: "sm", variant: "secondary", className: "mt-1.5", children: helperText })) : null] }));
53
73
  });
54
74
  Input.displayName = "Input";
@@ -20,6 +20,14 @@ interface BaseProps<V> {
20
20
  disabled?: boolean;
21
21
  className?: string;
22
22
  ariaLabel?: string;
23
+ /** Field name — resolves this control's error from `FormShellContext`, as `Field*` do. */
24
+ name?: string;
25
+ /**
26
+ * Validation message. Explicit prop wins; else read from context by `name`.
27
+ * Without these two this control had no error affordance at all: a required
28
+ * taxonomy pick could fail and the control showed nothing.
29
+ */
30
+ error?: React.ReactNode;
23
31
  /** "+ Create new …" button. Provide `renderCreateForm` OR (`createFields` + `onCreateSubmit`). */
24
32
  createLabel?: string;
25
33
  drawerTitle?: string;
@@ -59,24 +67,5 @@ interface MultiProps<V> extends BaseProps<V> {
59
67
  maxChipsVisible?: number;
60
68
  }
61
69
  export type PaginatedSelectProps<V = string> = SingleProps<V> | MultiProps<V>;
62
- /**
63
- * PaginatedSelect — searchable, async-paginated select with optional multi-mode
64
- * and optional inline create-new drawer.
65
- *
66
- * @example single-select
67
- * <PaginatedSelect value={storeId} onChange={setStoreId} loadOptions={loadStores} />
68
- *
69
- * @example single-select with inline create
70
- * <PaginatedSelect
71
- * value={categoryId}
72
- * onChange={(v) => setCategoryId(v ?? "")}
73
- * loadOptions={loadCategories}
74
- * createLabel="Category"
75
- * renderCreateForm={({ onCreated, onCancel }) => <CategoryForm ... />}
76
- * />
77
- *
78
- * @example multi-select
79
- * <PaginatedSelect multiple value={tagIds} onChange={setTagIds} loadOptions={loadTags} />
80
- */
81
70
  export declare function PaginatedSelect<V = string>(props: PaginatedSelectProps<V>): React.JSX.Element;
82
71
  export {};
@@ -1,7 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useRef, useState } from "react";
3
+ import React, { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { Input } from "./Input.js";
5
+ import { toUserMessage } from "../../errors/error-display-map.js";
6
+ import { Text } from "./Typography.js";
7
+ import { FormShellContext } from "../forms/FormShell.js";
5
8
  import { SideDrawer } from "./SideDrawer.js";
6
9
  import { Button } from "./Button.js";
7
10
  import { QuickFormDrawer } from "../../features/shell/QuickFormDrawer.js";
@@ -87,9 +90,43 @@ function isValueSelected(value, props, isMulti) {
87
90
  * @example multi-select
88
91
  * <PaginatedSelect multiple value={tagIds} onChange={setTagIds} loadOptions={loadTags} />
89
92
  */
93
+ /**
94
+ * Resolve this control's validation message and the id that labels it.
95
+ *
96
+ * A hook rather than four lines in the component body: `PaginatedSelect` is
97
+ * already at `audit-code-quality`'s size limit, and the error affordance is
98
+ * self-contained — explicit prop wins, else `FormShellContext` by `name`,
99
+ * exactly as the `Field*` primitives resolve theirs.
100
+ */
101
+ function usePaginatedSelectError(name, errorProp) {
102
+ const ctx = React.useContext(FormShellContext);
103
+ const error = errorProp ?? (name ? ctx?.errors[name] : undefined);
104
+ const errorId = `${React.useId()}-error`;
105
+ return { error, errorId };
106
+ }
107
+ /** The message, under the trigger it describes. */
108
+ function PaginatedSelectError({ id, error }) {
109
+ if (!error)
110
+ return null;
111
+ return (_jsx(Text, { id: id, size: "sm", variant: "error", className: "appkit-form-field__error", role: "alert", children: error }));
112
+ }
113
+ /**
114
+ * The selected-values row, multi-select only.
115
+ *
116
+ * Its own component because `PaginatedSelect` sits against
117
+ * `audit-code-quality`'s size limit, and this block needs none of the
118
+ * control's twelve state variables — only what it is handed.
119
+ */
120
+ function PaginatedSelectChips({ chips, labelFor, overflowCount, disabled, onRemove, }) {
121
+ return (_jsxs("div", { className: C.chips, children: [chips.map((v) => (_jsxs("span", { className: C.chip, children: [_jsx("span", { className: C.chipLabel, children: labelFor(v) }), !disabled && (_jsx("button", { type: "button", "aria-label": "Remove", className: C.chipRemove, onClick: (e) => {
122
+ e.stopPropagation();
123
+ onRemove(v);
124
+ }, children: "\u2715" }))] }, String(v)))), overflowCount > 0 && (_jsxs("span", { className: C.overflowChip, children: ["+", overflowCount, " more"] }))] }));
125
+ }
90
126
  export function PaginatedSelect(props) {
91
- const { options, loadOptions, placeholder = "Select...", searchPlaceholder = "Search...", noResultsText = "No results", disabled, className = "", ariaLabel, createLabel, drawerTitle, renderCreateForm, createFields, createSchema, onCreateSubmit, createSubmitLabel, editLabel, editDrawerTitle, renderEditForm, editFields, editSchema, onEditLoad, onEditSubmit, editSubmitLabel, } = props;
127
+ const { options, loadOptions, placeholder = "Select...", searchPlaceholder = "Search...", noResultsText = "No results", disabled, className = "", ariaLabel, name, error: errorProp, createLabel, drawerTitle, renderCreateForm, createFields, createSchema, onCreateSubmit, createSubmitLabel, editLabel, editDrawerTitle, renderEditForm, editFields, editSchema, onEditLoad, onEditSubmit, editSubmitLabel, } = props;
92
128
  const isMulti = props.multiple === true;
129
+ const { error, errorId } = usePaginatedSelectError(name, errorProp);
93
130
  const maxChipsVisible = isMulti ? props.maxChipsVisible ?? 3 : 0;
94
131
  const [query, setQuery] = useState("");
95
132
  const [open, setOpen] = useState(false);
@@ -222,7 +259,7 @@ export function PaginatedSelect(props) {
222
259
  // Same staleness rule as the success path: a superseded request must
223
260
  // not paint its failure over a newer request's results.
224
261
  if (requestId === requestIdRef.current) {
225
- setLoadError(normalized.message || "Could not load options.");
262
+ setLoadError(toUserMessage(normalized.code, undefined, { fallback: "Could not load options." }));
226
263
  if (reset)
227
264
  setAsyncOptions([]);
228
265
  }
@@ -370,12 +407,9 @@ export function PaginatedSelect(props) {
370
407
  const multiValue = isMulti ? props.value : [];
371
408
  const visibleChips = multiValue.slice(0, maxChipsVisible);
372
409
  const overflowCount = multiValue.length - maxChipsVisible;
373
- return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: containerRef, className: [C.root, className].filter(Boolean).join(" "), "aria-label": ariaLabel, children: [isMulti && multiValue.length > 0 && (_jsxs("div", { className: C.chips, children: [visibleChips.map((v) => (_jsxs("span", { className: C.chip, children: [_jsx("span", { className: C.chipLabel, children: labelMap.current.get(v) ?? String(v) }), !disabled && (_jsx("button", { type: "button", "aria-label": "Remove", className: C.chipRemove, onClick: (e) => {
374
- e.stopPropagation();
375
- removeChip(v);
376
- }, children: "\u2715" }))] }, String(v)))), overflowCount > 0 && (_jsxs("span", { className: C.overflowChip, children: ["+", overflowCount, " more"] }))] })), _jsxs("button", { type: "button", disabled: disabled, onClick: () => setOpen((prev) => !prev), className: [C.trigger, disabled ? C.triggerDisabled : ""]
410
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: containerRef, className: [C.root, className].filter(Boolean).join(" "), children: [isMulti && multiValue.length > 0 && (_jsx(PaginatedSelectChips, { chips: visibleChips, labelFor: (v) => labelMap.current.get(v) ?? String(v), overflowCount: overflowCount, disabled: disabled, onRemove: removeChip })), _jsxs("button", { type: "button", disabled: disabled, onClick: () => setOpen((prev) => !prev), className: [C.trigger, disabled ? C.triggerDisabled : ""]
377
411
  .filter(Boolean)
378
- .join(" "), "aria-expanded": open, "aria-haspopup": "listbox", children: [_jsx("span", { className: C.triggerLeft, children: _jsx("span", { className: triggerEmpty ? C.placeholder : "", children: triggerLabel }) }), _jsx("span", { "aria-hidden": "true", children: "\u25BE" })] }), hasEdit && !disabled ? (_jsx("button", { type: "button", className: C.editBtn, "aria-label": `Edit ${editLabel}`, title: `Edit ${editLabel}`, onClick: () => {
412
+ .join(" "), "aria-label": ariaLabel, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? "true" : undefined, "aria-describedby": error ? errorId : undefined, children: [_jsx("span", { className: C.triggerLeft, children: _jsx("span", { className: triggerEmpty ? C.placeholder : "", children: triggerLabel }) }), _jsx("span", { "aria-hidden": "true", children: "\u25BE" })] }), hasEdit && !disabled ? (_jsx("button", { type: "button", className: C.editBtn, "aria-label": `Edit ${editLabel}`, title: `Edit ${editLabel}`, onClick: () => {
379
413
  setOpen(false);
380
414
  setEditOpen(true);
381
415
  }, children: "\u270E" })) : null, open && (_jsxs("div", { className: [C.dropdown, opensUp ? C.dropdownUp : ""].filter(Boolean).join(" "), role: "listbox", "aria-multiselectable": isMulti || undefined, children: [_jsx(Input, { autoFocus: true, value: query, onChange: (e) => setQuery(e.target.value), placeholder: searchPlaceholder, className: C.search }), _jsxs("ul", { className: C.list, children: [filtered.map((opt, idx) => {
@@ -386,7 +420,7 @@ export function PaginatedSelect(props) {
386
420
  }), loadError && !loading && (_jsxs("li", { className: C.empty, role: "alert", children: [loadError, " ", _jsx(Button, { variant: "ghost", size: "sm", onClick: () => void load(query, 1, true), children: "Try again" })] })), !loadError && filtered.length === 0 && !loading && (_jsx("li", { className: C.empty, children: noResultsText })), loading && _jsx("li", { className: C.loading, children: "Loading\u2026" }), hasMore && !loading && (_jsx("li", { children: _jsx("button", { type: "button", className: C.loadMore, onClick: () => void load(query, page + 1), children: "Load more" }) })), hasCreate && (_jsx("li", { children: _jsxs("button", { type: "button", className: C.createBtn, onClick: () => {
387
421
  setOpen(false);
388
422
  setDrawerOpen(true);
389
- }, children: ["+ Create new ", createLabel] }) }, CREATE_SENTINEL))] })] }))] }), hasCreate && renderCreateForm ? (_jsx(SideDrawer, { isOpen: drawerOpen, onClose: () => setDrawerOpen(false), title: drawerTitle ?? `Create ${createLabel}`, mode: "create", footer: _jsx(Button, { variant: "ghost", onClick: () => setDrawerOpen(false), children: "Cancel" }), children: renderCreateForm({
423
+ }, children: ["+ Create new ", createLabel] }) }, CREATE_SENTINEL))] })] })), _jsx(PaginatedSelectError, { id: errorId, error: error })] }), hasCreate && renderCreateForm ? (_jsx(SideDrawer, { isOpen: drawerOpen, onClose: () => setDrawerOpen(false), title: drawerTitle ?? `Create ${createLabel}`, mode: "create", footer: _jsx(Button, { variant: "ghost", onClick: () => setDrawerOpen(false), children: "Cancel" }), children: renderCreateForm({
390
424
  onCreated: handleCreated,
391
425
  onCancel: () => setDrawerOpen(false),
392
426
  }) })) : hasCreate && createFields && onCreateSubmit ? (_jsx(QuickFormDrawer, { isOpen: drawerOpen, onClose: () => setDrawerOpen(false), title: drawerTitle ?? `Create ${createLabel}`, fields: createFields, schema: createSchema, submitLabel: createSubmitLabel ?? `Create ${createLabel}`, onSubmit: async (values) => {
@@ -66,5 +66,12 @@ export function RadioGroup({ name, options, value, onChange, label, error, orien
66
66
  export function RadioItem({ label, suffix, error, className = "", checked, disabled, id, ...props }) {
67
67
  const generatedId = React.useId();
68
68
  const inputId = id ?? generatedId;
69
- return (_jsxs("div", { className: "appkit-radio-item", "data-section": "radio-item-div", children: [_jsxs(Label, { htmlFor: inputId, className: ["appkit-radio-item__label", disabled ? "appkit-radio-item__label--disabled" : ""].filter(Boolean).join(" "), children: [_jsx("span", { className: "appkit-radio-item__box-wrap", children: _jsx("input", { ...props, id: inputId, type: "radio", checked: checked, disabled: disabled, "aria-invalid": error ? "true" : undefined, className: ["appkit-radio-item__input", error ? "appkit-radio-item__input--error" : "", className].filter(Boolean).join(" ") }) }), (label || suffix) && (_jsxs("span", { className: "appkit-radio-item__content", children: [label ? _jsx(Span, { className: "appkit-radio-item__text", children: label }) : _jsx("span", {}), suffix] }))] }), error && (_jsx(Text, { size: "sm", variant: "error", className: "appkit-radio-item__error", role: "alert", children: error }))] }));
69
+ /*
70
+ * A caller's own `aria-invalid` wins. `{...props}` is spread first, so
71
+ * re-specifying the attribute afterwards overwrites an incoming "true"
72
+ * with `undefined` — the defect that left every Field*-built form in the
73
+ * app with no aria-invalid at all. See the full writeup in `Input.tsx`.
74
+ */
75
+ const ariaInvalid = props["aria-invalid"] ?? (error ? "true" : undefined);
76
+ return (_jsxs("div", { className: "appkit-radio-item", "data-section": "radio-item-div", children: [_jsxs(Label, { htmlFor: inputId, className: ["appkit-radio-item__label", disabled ? "appkit-radio-item__label--disabled" : ""].filter(Boolean).join(" "), children: [_jsx("span", { className: "appkit-radio-item__box-wrap", children: _jsx("input", { ...props, id: inputId, type: "radio", checked: checked, disabled: disabled, "aria-invalid": ariaInvalid, className: ["appkit-radio-item__input", error ? "appkit-radio-item__input--error" : "", className].filter(Boolean).join(" ") }) }), (label || suffix) && (_jsxs("span", { className: "appkit-radio-item__content", children: [label ? _jsx(Span, { className: "appkit-radio-item__text", children: label }) : _jsx("span", {}), suffix] }))] }), error && (_jsx(Text, { size: "sm", variant: "error", className: "appkit-radio-item__error", role: "alert", children: error }))] }));
70
77
  }