@mohasinac/appkit 4.33.1 → 4.34.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 (88) hide show
  1. package/dist/_internal/server/features/checkout/actions.js +9 -1
  2. package/dist/_internal/server/features/checkout/expand-order-items.js +9 -0
  3. package/dist/_internal/server/features/media/contextGuards.d.ts +3 -3
  4. package/dist/_internal/server/features/media/contextGuards.js +19 -5
  5. package/dist/_internal/server/features/orders/actions.d.ts +9 -0
  6. package/dist/_internal/server/features/orders/actions.js +47 -4
  7. package/dist/_internal/server/features/orders/adapters.js +10 -0
  8. package/dist/_internal/server/features/refunds/actions.d.ts +9 -1
  9. package/dist/_internal/server/features/refunds/actions.js +36 -3
  10. package/dist/_internal/server/features/site-settings/adapters.js +1 -1
  11. package/dist/_internal/shared/features/orders/refund-copy.d.ts +12 -0
  12. package/dist/_internal/shared/features/orders/refund-copy.js +12 -0
  13. package/dist/_internal/shared/features/orders/return-reasons.d.ts +59 -0
  14. package/dist/_internal/shared/features/orders/return-reasons.js +96 -0
  15. package/dist/_internal/shared/features/orders/schema.d.ts +30 -0
  16. package/dist/_internal/shared/features/orders/schema.js +20 -0
  17. package/dist/_internal/shared/features/products/schema.d.ts +12 -0
  18. package/dist/_internal/shared/features/products/schema.js +4 -1
  19. package/dist/_internal/shared/media/limits.d.ts +14 -0
  20. package/dist/_internal/shared/media/limits.js +29 -0
  21. package/dist/client.d.ts +9 -1
  22. package/dist/client.js +19 -1
  23. package/dist/features/admin/components/AdminProductEditorView.js +6 -1
  24. package/dist/features/admin/components/AdminSiteSettingsView.js +2 -6
  25. package/dist/features/admin/components/index.d.ts +0 -2
  26. package/dist/features/admin/components/index.js +0 -1
  27. package/dist/features/admin/schemas/admin-editor-forms.js +1 -1
  28. package/dist/features/admin/schemas/firestore.d.ts +0 -2
  29. package/dist/features/auctions/components/AuctionDetailPageView.js +4 -3
  30. package/dist/features/contact/components/ContactForm.js +46 -31
  31. package/dist/features/media/upload/MediaUploadList.d.ts +16 -1
  32. package/dist/features/media/upload/MediaUploadList.js +58 -8
  33. package/dist/features/orders/components/RefundRequestView.d.ts +26 -10
  34. package/dist/features/orders/components/RefundRequestView.js +108 -24
  35. package/dist/features/orders/schemas/firestore.d.ts +42 -0
  36. package/dist/features/orders/types/index.d.ts +24 -0
  37. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +5 -3
  38. package/dist/features/products/components/ProductDetailPageView.js +14 -2
  39. package/dist/features/products/components/ProductFeatureBadges.d.ts +9 -1
  40. package/dist/features/products/components/ProductFeatureBadges.js +17 -1
  41. package/dist/features/products/components/ProductForm.js +46 -10
  42. package/dist/features/products/components/ProductGrid.js +10 -1
  43. package/dist/features/products/constants/final-sale.d.ts +40 -0
  44. package/dist/features/products/constants/final-sale.js +40 -0
  45. package/dist/features/products/index.d.ts +1 -0
  46. package/dist/features/products/index.js +1 -0
  47. package/dist/features/products/schemas/firestore.d.ts +19 -2
  48. package/dist/features/products/schemas/firestore.js +4 -0
  49. package/dist/features/products/types/index.d.ts +2 -0
  50. package/dist/features/seller/components/SellerProductShell.d.ts +7 -0
  51. package/dist/features/seller/components/SellerProductShell.js +82 -11
  52. package/dist/features/seller/index.d.ts +1 -0
  53. package/dist/features/seller/index.js +1 -0
  54. package/dist/features/seller/schemas/product-form.d.ts +304 -0
  55. package/dist/features/seller/schemas/product-form.js +132 -0
  56. package/dist/features/seller/utils/product-draft-mapping.d.ts +117 -0
  57. package/dist/features/seller/utils/product-draft-mapping.js +147 -0
  58. package/dist/features/shell/SectionForm.js +62 -7
  59. package/dist/features/shell/form-sections.js +4 -0
  60. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +182 -0
  61. package/dist/index.d.ts +6 -3
  62. package/dist/index.js +12 -3
  63. package/dist/next/routing/route-map.d.ts +2 -0
  64. package/dist/next/routing/route-map.js +1 -0
  65. package/dist/schemas/registry.d.ts +271 -0
  66. package/dist/schemas/registry.js +9 -0
  67. package/dist/seed/_helpers/final-sale-fixtures.d.ts +35 -0
  68. package/dist/seed/_helpers/final-sale-fixtures.js +61 -0
  69. package/dist/seed/products-art-seed-data.js +9 -1
  70. package/dist/seed/products-auctions-seed-data.js +2 -1
  71. package/dist/seed/products-classifieds-seed-data.js +2 -1
  72. package/dist/seed/products-digital-codes-seed-data.js +2 -1
  73. package/dist/seed/products-live-items-seed-data.js +2 -1
  74. package/dist/seed/products-preorders-seed-data.d.ts +2 -138
  75. package/dist/seed/products-preorders-seed-data.js +2 -1
  76. package/dist/seed/products-prize-draws-seed-data.js +2 -1
  77. package/dist/seed/products-standard-seed-data.js +2 -1
  78. package/dist/seed/products-stickers-seed-data.js +9 -1
  79. package/dist/server.d.ts +6 -2
  80. package/dist/server.js +15 -2
  81. package/dist/styles.css +1 -1
  82. package/dist/tailwind-utilities.css +1 -1
  83. package/dist/tokens/motion.d.ts +12 -0
  84. package/dist/tokens/motion.js +12 -0
  85. package/dist/ui/components/CollapsibleSection.d.ts +18 -1
  86. package/dist/ui/components/CollapsibleSection.js +20 -3
  87. package/dist/ui/components/Motion.js +17 -1
  88. package/package.json +1 -1
@@ -0,0 +1,147 @@
1
+ function pick(v) {
2
+ return v === undefined || v === null || v === "" ? undefined : v;
3
+ }
4
+ /** Drop keys whose value is undefined so they never reach Firestore. */
5
+ function compact(obj) {
6
+ const out = {};
7
+ for (const [k, v] of Object.entries(obj)) {
8
+ if (v !== undefined)
9
+ out[k] = v;
10
+ }
11
+ return out;
12
+ }
13
+ /**
14
+ * The flat draft keys this mapper OWNS.
15
+ *
16
+ * Stripped from the payload after being folded into their nested homes — the
17
+ * write schema does not name them, so leaving them in would be harmless today
18
+ * and confusing forever ("is `classifiedCity` stored or not?").
19
+ */
20
+ const OWNED_FLAT_KEYS = [
21
+ "classifiedCity", "classifiedLocality", "classifiedPincode",
22
+ "classifiedContactMethod", "classifiedAcceptsShipping", "classifiedNegotiable",
23
+ "digitalCodeDelivery", "digitalCodePoolSize",
24
+ "digitalCodeRedemptionInstructions", "digitalCodeExpiresAt",
25
+ "liveSpecies", "liveAgeMonths", "liveSex", "liveCareInfo",
26
+ "liveTransportMethod", "liveHandlingFee", "liveJurisdictions", "liveCites",
27
+ "printSize", "printMaterial", "printFinish", "printEditionSize",
28
+ ];
29
+ /**
30
+ * Fold the draft's flat per-type keys into the nested blocks the write schema
31
+ * expects. Every other key passes through untouched.
32
+ *
33
+ * A block is emitted only when the draft actually carries something for it, so
34
+ * saving a standard listing does not write an empty `classified: {}`.
35
+ */
36
+ export function draftToProductInput(draft) {
37
+ const out = { ...draft };
38
+ for (const k of OWNED_FLAT_KEYS)
39
+ delete out[k];
40
+ // ── classified ────────────────────────────────────────────────────────────
41
+ if (pick(draft.classifiedCity) !== undefined) {
42
+ out.classified = compact({
43
+ meetupArea: compact({
44
+ city: draft.classifiedCity,
45
+ locality: pick(draft.classifiedLocality),
46
+ pincode: pick(draft.classifiedPincode),
47
+ }),
48
+ acceptsShipping: draft.classifiedAcceptsShipping,
49
+ negotiable: draft.classifiedNegotiable,
50
+ });
51
+ }
52
+ // ── digital code ──────────────────────────────────────────────────────────
53
+ if (pick(draft.digitalCodeDelivery) !== undefined) {
54
+ out.digitalCode = compact({
55
+ codeDeliveryMethod: draft.digitalCodeDelivery,
56
+ codePoolSize: pick(draft.digitalCodePoolSize),
57
+ redemptionInstructions: pick(draft.digitalCodeRedemptionInstructions),
58
+ expiresAt: pick(draft.digitalCodeExpiresAt),
59
+ });
60
+ }
61
+ // ── live item ─────────────────────────────────────────────────────────────
62
+ if (pick(draft.liveSpecies) !== undefined) {
63
+ out.liveItem = compact({
64
+ species: draft.liveSpecies,
65
+ ageMonths: pick(draft.liveAgeMonths),
66
+ sex: pick(draft.liveSex),
67
+ careInfo: pick(draft.liveCareInfo),
68
+ /*
69
+ * `transport` is a REQUIRED object on the schema, and `method` inside it
70
+ * is required too — so a live listing whose form left the transport
71
+ * method blank must still produce the object, or the whole `liveItem`
72
+ * block fails to parse and takes the species with it. "courier" is the
73
+ * form's own default.
74
+ */
75
+ transport: compact({
76
+ method: draft.liveTransportMethod ?? "courier",
77
+ handlingFee: pick(draft.liveHandlingFee),
78
+ }),
79
+ // Required, min 1. Left empty the schema rejects the listing with a
80
+ // real message — which is correct: shipping a live animal into a
81
+ // jurisdiction that forbids it is what the field exists to prevent.
82
+ jurisdictionAllowed: draft.liveJurisdictions ?? [],
83
+ // `cites` is a STRING permit number on the document, while the form
84
+ // carries a boolean "CITES applies" checkbox. Only the affirmative case
85
+ // means anything, and it means "documented" until a number is entered.
86
+ cites: draft.liveCites ? "documented" : undefined,
87
+ });
88
+ }
89
+ // ── print meta (art + stickers share one block) ───────────────────────────
90
+ const printEntries = compact({
91
+ size: pick(draft.printSize),
92
+ material: pick(draft.printMaterial),
93
+ finish: pick(draft.printFinish),
94
+ editionSize: pick(draft.printEditionSize),
95
+ });
96
+ if (Object.keys(printEntries).length > 0)
97
+ out.printMeta = printEntries;
98
+ return out;
99
+ }
100
+ /**
101
+ * The inverse — seed the form from a stored document.
102
+ *
103
+ * Tolerant of documents written before the nested blocks existed: a missing
104
+ * block yields undefined flat keys, which render as empty inputs rather than
105
+ * throwing.
106
+ */
107
+ export function productToDraft(product) {
108
+ const { classified, digitalCode, liveItem, printMeta } = product;
109
+ return compact({
110
+ /*
111
+ * Flat fields the edit pages' hand-written `initialValues` blocks omit.
112
+ *
113
+ * Not a nesting mismatch like the rest of this function — just fields
114
+ * nobody added to ten near-identical literals. `finalSale` matters most:
115
+ * unseeded it renders as `undefined`, which `isFinalSale` reads as TRUE,
116
+ * so a listing the seller had opted into returns would open its own edit
117
+ * form showing "final sale". The stored value survives an untouched save
118
+ * (an absent key is stripped, and the update is partial), but the form was
119
+ * telling the seller something untrue about their own listing.
120
+ */
121
+ finalSale: product.finalSale,
122
+ returnPolicy: product.returnPolicy,
123
+ classifiedCity: classified?.meetupArea?.city,
124
+ classifiedLocality: classified?.meetupArea?.locality,
125
+ classifiedPincode: classified?.meetupArea?.pincode,
126
+ classifiedAcceptsShipping: classified?.acceptsShipping,
127
+ classifiedNegotiable: classified?.negotiable,
128
+ digitalCodeDelivery: digitalCode?.codeDeliveryMethod,
129
+ digitalCodePoolSize: digitalCode?.codePoolSize,
130
+ digitalCodeRedemptionInstructions: digitalCode?.redemptionInstructions,
131
+ digitalCodeExpiresAt: digitalCode?.expiresAt,
132
+ liveSpecies: liveItem?.species,
133
+ liveAgeMonths: liveItem?.ageMonths,
134
+ liveSex: liveItem?.sex,
135
+ liveCareInfo: liveItem?.careInfo,
136
+ liveTransportMethod: liveItem?.transport?.method,
137
+ liveHandlingFee: liveItem?.transport?.handlingFee,
138
+ liveJurisdictions: liveItem?.jurisdictionAllowed,
139
+ // String -> boolean, mirroring the outbound direction: any recorded permit
140
+ // means the checkbox is ticked.
141
+ liveCites: liveItem?.cites ? true : undefined,
142
+ printSize: printMeta?.size,
143
+ printMaterial: printMeta?.material,
144
+ printFinish: printMeta?.finish,
145
+ printEditionSize: printMeta?.editionSize,
146
+ });
147
+ }
@@ -9,6 +9,7 @@ import { FormShellContext } from "../../ui/forms/FormShell";
9
9
  import { useFormBottomActions } from "../layout/hooks/useFormBottomActions";
10
10
  import { useSectionState } from "../account/hooks/useCollapsedSections";
11
11
  import { FormSchemaContext } from "./FormShell";
12
+ import { EASE_OUT_MS } from "../../tokens/motion";
12
13
  /** Sections in render order: required first, original order preserved within each group. */
13
14
  export function orderSections(sections) {
14
15
  return [...sections.filter((s) => s.required), ...sections.filter((s) => !s.required)];
@@ -78,8 +79,34 @@ export function scrollToSection(id) {
78
79
  return;
79
80
  requestAnimationFrame(() => {
80
81
  const el = document.getElementById(sectionAnchorId(id));
81
- el?.scrollIntoView({ behavior: "smooth", block: "start" });
82
- el?.querySelector("[aria-invalid='true']")?.focus();
82
+ if (!el)
83
+ return;
84
+ /*
85
+ * The header is already at its final position, so scrolling can happen on
86
+ * the next frame.
87
+ */
88
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
89
+ /*
90
+ * Focusing the offending field cannot.
91
+ *
92
+ * `<Collapse>` animates height 0 -> auto over EASE_OUT_MS, and Framer
93
+ * drives that with rAF rather than a CSS transition — so there is no
94
+ * `transitionend` to wait for, and one `requestAnimationFrame` lands while
95
+ * the panel is still ~0px tall inside `overflow: hidden`. Focusing there
96
+ * makes the browser scroll-anchor against a box that is about to grow,
97
+ * which is why "jump to error" could leave the field off-screen.
98
+ *
99
+ * A required section renders with no <Collapse> at all, so it is already
100
+ * at full height and this simply fires a tick later than it needs to.
101
+ */
102
+ const reduced = typeof window !== "undefined" &&
103
+ typeof window.matchMedia === "function" &&
104
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
105
+ const focusInvalid = () => el.querySelector("[aria-invalid='true']")?.focus();
106
+ if (reduced)
107
+ focusInvalid();
108
+ else
109
+ window.setTimeout(focusInvalid, EASE_OUT_MS);
83
110
  });
84
111
  }
85
112
  export function SectionForm({ sections, values, onChange, onSubmit, submitLabel = "Save", isLoading = false, schema, openIds: controlledOpenIds, onOpenChange, expandMode = "multi", hideActions = false, destructiveAction, onValidationChange, onCancel, cancelLabel, bottomBar = true, }) {
@@ -112,7 +139,20 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
112
139
  setUncontrolledOpenIds(ids);
113
140
  onOpenChange?.(ids);
114
141
  }, [controlledOpenIds, onOpenChange]);
115
- const fieldToSectionIndex = useMemo(() => buildFieldToSectionIndex(ordered), [ordered]);
142
+ /*
143
+ * `ordered` depends on `values`, so it is a NEW array on every keystroke.
144
+ * Anything derived from it must key on the visible section SET instead, or
145
+ * the effect below re-fires per character — running a second full-tree
146
+ * `safeParse` on top of the one `handleFieldChange` already performs, and
147
+ * rebuilding every downstream callback with it.
148
+ *
149
+ * `ordered` itself must keep recomputing: a `when()` predicate can flip
150
+ * mid-typing and a section can appear or disappear. It is only the DERIVED
151
+ * values that are stable across that.
152
+ */
153
+ const visibleSectionKey = ordered.map((s) => s.id).join("|");
154
+ // eslint-disable-next-line react-hooks/exhaustive-deps
155
+ const fieldToSectionIndex = useMemo(() => buildFieldToSectionIndex(ordered), [visibleSectionKey]);
116
156
  // Keep the latest callback without making runValidation depend on it — the
117
157
  // caller usually passes an inline arrow, which would otherwise rebuild the
118
158
  // validator on every render and re-fire the effect below in a loop.
@@ -227,14 +267,29 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
227
267
  }, [fieldErrors]);
228
268
  return (_jsxs(Stack, { gap: "md", children: [ordered.map((section) => {
229
269
  const isRequired = section.required === true;
230
- // A required section is always open — it holds the fields without
231
- // which the record cannot be saved, so hiding it is never useful.
270
+ /*
271
+ * A required section is always open — it holds the fields without
272
+ * which the record cannot be saved, so hiding it is never useful.
273
+ *
274
+ * `collapsible={!isRequired}` below is what makes that VISIBLE: the
275
+ * header renders as a plain heading with no control. Previously this
276
+ * flag only forced `isCollapsed` false while the header still drew a
277
+ * chevron button, so the section was permanently open AND appeared
278
+ * broken. `onToggle` no longer needs its `if (!isRequired)` guard,
279
+ * because a static header has nothing to click.
280
+ */
232
281
  const isCollapsed = isRequired
233
282
  ? false
234
283
  : !(openIds.includes(section.id) || forceOpenIds.includes(section.id));
235
284
  const errorCount = errorCountFor(section);
236
- return (_jsx(CollapsibleSection, { id: sectionAnchorId(section.id), title: _jsxs(Row, { align: "center", gap: "xs", children: [_jsx(Span, { weight: "semibold", size: "sm", children: section.heading ?? section.label }), isRequired && _jsx(Span, { size: "xs", color: "muted", children: "Required" })] }), isCollapsed: isCollapsed, onToggle: () => { if (!isRequired)
237
- toggle(section.id); }, keepMounted: section.keepMounted, renderHeaderExtra: errorCount > 0
285
+ return (_jsx(CollapsibleSection, { id: sectionAnchorId(section.id), title:
286
+ /*
287
+ * `as="span"` matters: a collapsible section's header renders
288
+ * this INSIDE a <button>, and <Row>'s default <div> made that
289
+ * <button><div><span><div> — invalid, since a button may only
290
+ * contain phrasing content. Same variant props, legal element.
291
+ */
292
+ _jsxs(Row, { as: "span", align: "center", gap: "xs", className: "min-w-0", children: [_jsx(Span, { weight: "semibold", size: "sm", children: section.heading ?? section.label }), isRequired && _jsx(Span, { size: "xs", color: "muted", children: "Required" })] }), isCollapsed: isCollapsed, collapsible: !isRequired, onToggle: () => toggle(section.id), keepMounted: section.keepMounted, renderHeaderExtra: errorCount > 0
238
293
  ? () => (_jsxs(Badge, { variant: "danger", children: [errorCount, " ", errorCount === 1 ? "issue" : "issues"] }))
239
294
  : undefined, children: section.render({ values, onChange: handleFieldChange, errors: fieldErrors }) }, section.id));
240
295
  }), !hideActions && (
@@ -92,6 +92,10 @@ export const SECTION_GROUP_DEFAULTS = {
92
92
  // logistics
93
93
  address: "logistics", shipping: "logistics", delivery: "logistics",
94
94
  routing: "logistics",
95
+ // Returns sit with logistics rather than with pricing: the seller is
96
+ // describing what happens to the goods after delivery, and the section
97
+ // reads directly after Shipping on the listing form.
98
+ returns: "logistics",
95
99
  // relationships
96
100
  owner: "relationships", members: "relationships", link: "relationships",
97
101
  google: "relationships", sync: "relationships", subscribe: "relationships",
@@ -1927,6 +1927,48 @@ const rawTesterChecklistItems = [
1927
1927
  },
1928
1928
  ],
1929
1929
  },
1930
+ {
1931
+ pageKey: "return-request",
1932
+ pageLabel: "Requesting a return \u2014 and what final sale actually blocks",
1933
+ href: "/user/orders",
1934
+ cases: [
1935
+ {
1936
+ key: "return-cta-only-when-returnable",
1937
+ label: "The \"Request Return\" button appears only on a DELIVERED order inside the 7-day window",
1938
+ description: "Added 2026-09-04. BEFORE there was no buyer-facing return flow at all: the action existed, the CTA was registered in ACTIONS.USER, and nothing anywhere rendered it. AFTER: open a delivered order's detail page and the button is there beside Track Shipment. Open a pending or shipped one and it is not. Open a delivered order older than 7 days and the page explains the window has closed rather than offering a form that would be refused.",
1939
+ },
1940
+ {
1941
+ key: "return-not-received-accepted-on-final-sale",
1942
+ label: "\"It never arrived\" is accepted on a FINAL SALE order",
1943
+ description: "This is the whole rule. Final sale refuses a change of mind; it can never refuse a claim that the buyer did not get what they paid for. On a final-sale order, choose \"It never arrived\", tick the three acknowledgements, submit \u2014 it must go through and the order must move to Return Requested.",
1944
+ },
1945
+ {
1946
+ key: "return-change-of-mind-refused-by-name",
1947
+ label: "On a final-sale order the change-of-mind reasons are NOT OFFERED, and the panel says why",
1948
+ description: "Open the return form for a final-sale order. A warning panel names the situation, and the reason picker lists only the five fault reasons \u2014 never-arrived, not-as-described, damaged, wrong item, counterfeit. \"I changed my mind\" must not be selectable. On a returnable order all ten reasons appear. A generic \"request refused\" with no explanation is the failure this replaced.",
1949
+ },
1950
+ {
1951
+ key: "return-reason-is-persisted",
1952
+ label: "The reason the buyer picked is actually STORED and visible to staff",
1953
+ description: "BEFORE: `requestReturnAction` parsed a free-text reason and then threw it away before writing \u2014 an order reached Return Requested with nothing recorded about why, and the seller had nothing to act on. AFTER: submit a return, then open the same order in /admin/return-requests and confirm the coded reason and any note are there.",
1954
+ },
1955
+ {
1956
+ key: "return-partial-gates-selected-lines-only",
1957
+ label: "On a mixed order, a final-sale line does not block a return of the returnable line",
1958
+ description: "Final sale is snapshotted PER LINE at purchase, not per order. On an order containing one final-sale item and one returnable item, a change-of-mind return scoped to the returnable line must be accepted; the same reason scoped to the final-sale line must not.",
1959
+ },
1960
+ {
1961
+ key: "return-prize-draw-still-refused-entirely",
1962
+ label: "A prize-draw order is still refused for EVERY reason \u2014 a different, earlier gate",
1963
+ description: "`isNonRefundable` is a property of the listing TYPE and admits no exception, including a seller-fault one. Open a prize-draw order's refund surface: it must refuse outright and say so, without offering a reason picker at all. Confusing this with final sale in either direction is the failure.",
1964
+ },
1965
+ {
1966
+ key: "return-terms-snapshotted-at-purchase",
1967
+ label: "Changing a listing's return terms does NOT change an order already placed",
1968
+ description: "Buy a returnable listing. Then, as the seller, switch that listing to final sale. Go back to the order and request a change-of-mind return \u2014 it must still be ACCEPTED, because the buyer agreed to the terms shown at checkout. The order carries its own copy; a seller must not be able to withdraw a right after the sale.",
1969
+ },
1970
+ ],
1971
+ },
1930
1972
  ]),
1931
1973
  ...group("selling", "Selling", [
1932
1974
  {
@@ -2162,6 +2204,30 @@ const rawTesterChecklistItems = [
2162
2204
  description: "BEFORE: an invalid field produced a banner listing the problem with no way to reach it, and on the site-settings page the summary was mounted outside every form context and so displayed NOTHING at all. AFTER: submit a form with a bad value (a negative refund on an order, a non-https third-party ad URL, an AdSense id that is not ca-pub-…) and the summary must list it, tag it with its section, collapse open that section when clicked, and show a count badge on the section header.",
2163
2205
  href: "/admin/ads",
2164
2206
  },
2207
+ {
2208
+ key: "required-section-has-no-dead-chevron",
2209
+ label: "A REQUIRED section renders as a plain heading \u2014 no chevron, nothing to click",
2210
+ description: "Fixed 2026-09-04. BEFORE: SectionDef documented a required section as rendering \"without a collapse control so it can't be hidden\", but CollapsibleSection had no way to say that, so SectionForm passed a no-op handler and the header still drew a full button with a rotating chevron. Clicking it did nothing, on ~73 required sections across the app \u2014 which reads as a broken form. AFTER: on any sectionised form (admin product, admin user, store product), the first section's header has no chevron and no hover state, while every optional section below it still expands and collapses normally.",
2211
+ href: "/admin/products/new",
2212
+ },
2213
+ {
2214
+ key: "open-section-does-not-clip-dropdowns",
2215
+ label: "A dropdown opened inside an expanded section is not clipped at the panel edge",
2216
+ description: "BEFORE: the collapse animation kept `overflow: hidden` in its SETTLED open state, not just during the transition, so anything escaping the panel box was cut off. AFTER: expand a section containing a select or picker near its bottom edge, open it, and the list must render in full rather than being sliced by the section border.",
2217
+ href: "/admin/products/new",
2218
+ },
2219
+ {
2220
+ key: "error-jump-lands-on-the-field",
2221
+ label: "Jumping to an error scrolls to the section AND lands focus on the offending field",
2222
+ description: "BEFORE: the scroll fired one animation frame after expanding, while the panel was still ~0px tall inside overflow:hidden \u2014 so focus landed against a box that was about to grow and the field could end up off-screen. AFTER: submit a form with an error inside a COLLAPSED section. It expands, scrolls, and the invalid input is both visible and focused once the animation finishes.",
2223
+ href: "/admin/products/new",
2224
+ },
2225
+ {
2226
+ key: "long-form-typing-is-smooth",
2227
+ label: "Typing in a long form does not stutter",
2228
+ description: "BEFORE: the section list was rebuilt on every keystroke, which re-fired the validation effect on top of the validation the keystroke already triggered \u2014 two full-tree schema parses per character on a 40-field listing form. AFTER: type a long description into the seller product form. It should keep up. This is a feel test, not a pass/fail assertion; report it if it lags.",
2229
+ href: "/store/products/new",
2230
+ },
2165
2231
  {
2166
2232
  key: "form-mobile-action-bar",
2167
2233
  label: "Save and Cancel appear in the pinned mobile bar, once",
@@ -2410,6 +2476,122 @@ const rawTesterChecklistItems = [
2410
2476
  },
2411
2477
  ],
2412
2478
  },
2479
+ {
2480
+ pageKey: "final-sale-authoring",
2481
+ pageLabel: "Final sale \u2014 the seller switch",
2482
+ href: "/store/products/new",
2483
+ cases: [
2484
+ {
2485
+ key: "final-sale-defaults-on",
2486
+ label: "A brand-new listing is FINAL SALE by default \u2014 the Returns section shows \"Accept change-of-mind returns\" switched OFF",
2487
+ description: "Added 2026-09-04. Open the Returns section on a new listing of any type. The toggle is off and the panel explains that not-received / damaged / wrong-item / not-as-described / counterfeit claims are still accepted. Absent means final sale, so a listing saved without ever opening this section is also final sale \u2014 which is the point.",
2488
+ href: "/store/products/new",
2489
+ },
2490
+ {
2491
+ key: "final-sale-opt-out-survives-reload",
2492
+ label: "Turning returns ON, saving, and RELOADING shows it still on",
2493
+ description: "This is the case that matters, and a reload is the only way to see it. `productBaseSchema` is a plain z.object() with no .passthrough(), so it strips every key it does not name \u2014 a field missing from it saves with a success toast and is gone. BEFORE such a field existed at all. AFTER: toggle Accept returns ON, Save, reload the edit page, and the toggle must still be ON. If it reverts to off, `finalSale` is being stripped on write.",
2494
+ href: "/store/products",
2495
+ },
2496
+ {
2497
+ key: "return-policy-authorable-by-seller",
2498
+ label: "A seller can write their own Return policy text \u2014 for the first time",
2499
+ description: "BEFORE: `returnPolicy` existed on the document and the ONLY editor for it was the admin product form, so a seller could never state their own terms. AFTER: the Returns section on the seller form has a Return policy textarea. Type into it, save, reload, and confirm it persists and appears on the public listing under Delivery & Returns.",
2500
+ href: "/store/products",
2501
+ },
2502
+ {
2503
+ key: "final-sale-admin-override",
2504
+ label: "An admin can flip the same switch on any listing, and it is the SAME field",
2505
+ description: "Open a listing in the admin product editor. The \"Accept change-of-mind returns\" toggle sits directly above the Return policy box and reflects whatever the seller chose. Change it, save, then reopen the SELLER edit form for that listing \u2014 it must show the admin's value. Two independent flags would be the failure.",
2506
+ href: "/admin/products",
2507
+ },
2508
+ {
2509
+ key: "final-sale-badges-render",
2510
+ label: "A final-sale listing shows the pill on its card AND the line on its detail page",
2511
+ description: "Seed data opts one listing per type OUT of final sale, so both states are visible. On /products, a final-sale listing carries a dark \"Final Sale\" pill; the opted-out one does not. On the detail page, the Delivery & Returns block always renders a returns line \u2014 even when the seller wrote no shipping or policy text at all, which previously hid the block entirely and with it the most consequential term of the sale.",
2512
+ href: "/products",
2513
+ },
2514
+ ],
2515
+ },
2516
+ {
2517
+ pageKey: "media-limits",
2518
+ pageLabel: "Media \u2014 ten images and one video, on every listing type",
2519
+ href: "/store/products/new",
2520
+ cases: [
2521
+ {
2522
+ key: "media-gallery-accepts-images-and-video",
2523
+ label: "The gallery takes BOTH images and a video, and shows a live per-type count",
2524
+ description: "Changed 2026-09-04. BEFORE the single-media control on the listing form was video-only (`kind=\"video\"`), so the one \"add media\" affordance a seller saw refused photos. AFTER: the gallery accepts image files and a video file in the same control, and a counter under it reads \"N/10 images \u00b7 N/1 video\" as you add them.",
2525
+ href: "/store/products/new",
2526
+ },
2527
+ {
2528
+ key: "media-tenth-image-uploads",
2529
+ label: "The 10th gallery image uploads \u2014 the 5th used to fail with a 400",
2530
+ description: "BEFORE: the gallery was capped at 5 and sent its Nth image as index N+1, while the server's signed-URL guard rejected any index above 5 \u2014 so the LAST slot of every product form returned a 400 that read as a random upload failure. AFTER: add ten images one at a time. All ten must succeed, including the tenth.",
2531
+ href: "/store/products/new",
2532
+ },
2533
+ {
2534
+ key: "media-eleventh-image-refused-client-side",
2535
+ label: "The 11th image is refused by the FORM with a clear message, not by the server",
2536
+ description: "Try to add an 11th image. The control must refuse it before uploading, naming the limit (\"You can upload up to 10 images.\"). A server-side 400 arriving after the bytes have gone up is the wrong behaviour \u2014 the user has already waited.",
2537
+ href: "/store/products/new",
2538
+ },
2539
+ {
2540
+ key: "media-second-video-refused-first-kept",
2541
+ label: "A second video is refused and the FIRST one is kept",
2542
+ description: "With one video already in the gallery, try to add another. The message must say only one video is allowed and tell you to remove the current one \u2014 and crucially the existing video must still be there afterwards. Silently replacing it would lose work.",
2543
+ href: "/store/products/new",
2544
+ },
2545
+ {
2546
+ key: "media-caps-flat-across-types",
2547
+ label: "The same 10 + 1 applies to an auction and a pre-order \u2014 there is no per-type table",
2548
+ description: "Repeat the ten-image check on /store/auctions/new and /store/pre-orders/new. Both must allow ten. BEFORE the server carried separate AUCTION_IMAGE_MAX and PREORDER_IMAGE_MAX constants, both 5, disagreeing with everything else.",
2549
+ href: "/store/auctions/new",
2550
+ },
2551
+ {
2552
+ key: "media-live-listing-still-requires-video",
2553
+ label: "A live listing STILL refuses to publish without a video",
2554
+ description: "Unchanged behaviour, re-verified because the surrounding control changed. Create a live listing with photos but no video and press Publish. It must be refused, and the Video field must be marked required. That is a requiredness rule, not a count rule, and the media-limit work must not have removed it.",
2555
+ href: "/store/live/new",
2556
+ },
2557
+ ],
2558
+ },
2559
+ {
2560
+ pageKey: "listing-type-fields-roundtrip",
2561
+ pageLabel: "Per-type fields survive a save \u2014 all nine listing types",
2562
+ href: "/store/products",
2563
+ cases: [
2564
+ {
2565
+ key: "roundtrip-classified-meetup",
2566
+ label: "A classified's meetup city, locality and pincode come back after a save",
2567
+ description: "Fixed 2026-09-04, and this was live data loss. BEFORE: the form held `classifiedCity` while the write schema expected `classified.meetupArea.city`, nothing translated between them, and the schema strips unnamed keys \u2014 so the seller pressed Publish, got a success, and the listing saved with none of the fields that made it a classified. AFTER: fill in the meetup details, save, reopen the edit form, and every value must be there.",
2568
+ href: "/store/classified/new",
2569
+ },
2570
+ {
2571
+ key: "roundtrip-live-species-jurisdictions",
2572
+ label: "A live listing's species, transport method and permitted jurisdictions survive",
2573
+ description: "Same bug, and the most serious instance of it: `jurisdictionAllowed` is the field that prevents shipping a live animal somewhere that forbids it, and it was being dropped. Create a live listing with species, age, sex, care info, transport method, handling fee and at least one jurisdiction. Save, reopen, confirm all of it returned.",
2574
+ href: "/store/live/new",
2575
+ },
2576
+ {
2577
+ key: "roundtrip-digital-code-delivery",
2578
+ label: "A digital code's delivery method, pool size and redemption instructions survive",
2579
+ href: "/store/digital-codes/new",
2580
+ },
2581
+ {
2582
+ key: "roundtrip-print-meta",
2583
+ label: "An art print's size, material, finish and edition size survive \u2014 and so do a sticker sheet's",
2584
+ description: "Art and stickers share one `printMeta` block, so check both: create one of each with all four fields filled, save, reopen.",
2585
+ href: "/store/art/new",
2586
+ },
2587
+ {
2588
+ key: "roundtrip-edit-form-opens-populated",
2589
+ label: "Opening the edit form for an EXISTING listing shows its per-type fields already filled",
2590
+ description: "The inverse half of the same bug. BEFORE: the edit pages hand-built their initial values and listed none of the per-type keys, so the form opened blank for them even when the document held real data \u2014 which read as \"my data was deleted\". AFTER: open the edit page for any seeded classified, live, digital-code, art or sticker listing and its per-type fields are populated on arrival.",
2591
+ href: "/store/classified",
2592
+ },
2593
+ ],
2594
+ },
2413
2595
  ]),
2414
2596
  ...group("content-discovery", "Content & Discovery", [
2415
2597
  {
package/dist/index.d.ts CHANGED
@@ -1095,7 +1095,6 @@ export { AdminReviewsView } from "./features/admin/index";
1095
1095
  export { AdminSectionsView } from "./features/admin/index";
1096
1096
  export { AdminSessionsManager } from "./features/admin/index";
1097
1097
  export { AdminSidebar } from "./features/admin/index";
1098
- export { AdminSiteView } from "./features/admin/index";
1099
1098
  export { AdminSublistingCategoriesView } from "./features/admin/index";
1100
1099
  export { AdminSublistingCategoryEditorView } from "./features/admin/index";
1101
1100
  export type { AdminSublistingCategoryEditorViewProps } from "./features/admin/index";
@@ -1198,7 +1197,6 @@ export type { AdminReviewsViewProps } from "./features/admin/index";
1198
1197
  export type { AdminSectionsViewProps } from "./features/admin/index";
1199
1198
  export type { AdminSessionsManagerProps } from "./features/admin/index";
1200
1199
  export type { AdminSidebarProps } from "./features/admin/index";
1201
- export type { AdminSiteViewProps } from "./features/admin/index";
1202
1200
  export type { AdminSiteSettingsViewProps } from "./features/admin/index";
1203
1201
  export type { AdminStatCardProps } from "./features/admin/index";
1204
1202
  export type { AdminStoresViewProps } from "./features/admin/index";
@@ -2175,6 +2173,7 @@ export { RefundHistoryTable } from "./features/orders/index";
2175
2173
  export type { RefundHistoryTableProps } from "./features/orders/index";
2176
2174
  export { RefundRequestView } from "./features/orders/index";
2177
2175
  export type { RefundRequestViewProps } from "./features/orders/index";
2176
+ export type { RefundRequestSubmission } from "./features/orders/components/RefundRequestView";
2178
2177
  export { PaymentGatewayValues } from "./features/orders/index";
2179
2178
  export { PaymentMethodValues } from "./features/orders/index";
2180
2179
  export { OutOfStockPolicyValues } from "./features/orders/index";
@@ -2386,6 +2385,7 @@ export { getProductFilterKeys } from "./features/products/index";
2386
2385
  export { getProductSortOptions } from "./features/products/index";
2387
2386
  export { getProductTableColumns } from "./features/products/index";
2388
2387
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/index";
2388
+ export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
2389
2389
  export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
2390
2390
  export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
2391
2391
  export { cartRequiresShipping, cartIsDigitalOnly, cartIsChatOnly, } from "./_internal/shared/listing-types/cart-shipping";
@@ -3072,6 +3072,8 @@ export type { BundleQueryRule } from "./features/categories/schemas/firestore";
3072
3072
  export { BUNDLE_MIN_ITEMS, BUNDLE_MAX_ITEMS, BUNDLE_MAX_PER_USER_DEFAULT, BUNDLES_PAGE_SIZE, BUNDLES_FEATURED_LIMIT, } from "./_internal/shared/features/categories/bundle-config";
3073
3073
  export { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "./_internal/shared/features/categories/bundle-copy";
3074
3074
  export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy";
3075
+ export { RETURN_REASON, FINAL_SALE_EXEMPT_REASONS, RETURN_REASON_LABEL, RETURN_REASON_GROUP_LABEL, isFinalSaleExempt, isReturnReason, selectableReturnReasons, } from "./_internal/shared/features/orders/return-reasons";
3076
+ export type { ReturnReason } from "./_internal/shared/features/orders/return-reasons";
3075
3077
  export { bundleCreateSchema, bundleUpdateSchema, bundleQueryRuleSchema, bundleDisplaySchema, type BundleCreateBody, type BundleUpdateBody, } from "./_internal/shared/features/categories/bundle-schemas";
3076
3078
  export type { GroupedListingDocument, GroupTheme, GroupVisibility } from "./features/grouped/schemas/firestore";
3077
3079
  export { navItemFormSchema, navItemCreateSchema, navItemUpdateSchema, type NavItemFormValues, } from "./features/admin/schemas/nav-item-form";
@@ -3144,7 +3146,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
3144
3146
  export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
3145
3147
  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";
3146
3148
  export type { ListingCheckoutRule, CategoryCheckoutRule, CartItemProductPair, OrderItemInput, RefundPolicy, } from "./_internal/shared/checkout/rules";
3147
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
3149
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, 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";
3148
3150
  export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
3149
3151
  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";
3150
3152
  export { SORT_DIR, sortBy } from "./constants/sort";
@@ -3226,3 +3228,4 @@ export { ROLES_TABS, isRolesTabId, ADMIN_SETTINGS_TABS, isAdminSettingsTabId, }
3226
3228
  export type { AdminPageTab, RolesTabId, AdminSettingsTabId, } from "./features/admin/constants/admin-page-tabs";
3227
3229
  export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry";
3228
3230
  export type { ActionIndexValidationContext, ActionIndexValidationIssue, } from "./features/search/action-index/validate-entry";
3231
+ export { draftToProductInput, productToDraft, } from "./features/seller/utils/product-draft-mapping";
package/dist/index.js CHANGED
@@ -2126,8 +2126,6 @@ export { AdminSessionsManager } from "./features/admin/index";
2126
2126
  // AdminSidebar - Shared export for admin sidebar.
2127
2127
  export { AdminSidebar } from "./features/admin/index";
2128
2128
  // [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
2129
- // AdminSiteView - Component for admin site view.
2130
- export { AdminSiteView } from "./features/admin/index";
2131
2129
  // AdminSublistingCategoriesView - Admin list view for sublisting categories (SC1).
2132
2130
  export { AdminSublistingCategoriesView } from "./features/admin/index";
2133
2131
  // AdminSublistingCategoryEditorView - Admin create/edit form for sublisting categories (SC1).
@@ -4367,6 +4365,9 @@ export { getProductTableColumns } from "./features/products/index";
4367
4365
  // normalizeListingType + predicate helpers â€" canonical SB1-G accessors.
4368
4366
  // SB-UNI-F 2026-05-13 â€" Phase 2 predicates added (classified/digital-code/live).
4369
4367
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/index";
4368
+ // Final sale — ABSENT MEANS TRUE, so these are the only legal reads of the
4369
+ // `finalSale` field. See the module docstring for why `!p.finalSale` is wrong.
4370
+ export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
4370
4371
  // SB-UNI-X4 2026-05-13 â€" per-type feature-flag helpers.
4371
4372
  export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
4372
4373
  // SB-UNI-X5 2026-05-13 â€" action telemetry sink.
@@ -5454,6 +5455,10 @@ export { BUNDLE_MIN_ITEMS, BUNDLE_MAX_ITEMS, BUNDLE_MAX_PER_USER_DEFAULT, BUNDLE
5454
5455
  export { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "./_internal/shared/features/categories/bundle-copy";
5455
5456
  // S-SBUNI-RULES 2026-05-13 â€" shared copy for refund / shipping / sibling-payment UI.
5456
5457
  export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy";
5458
+ // Return / refund reasons — the closed enum the final-sale gate branches on.
5459
+ // See the module docstring for why the exempt set is "seller or carrier fault"
5460
+ // rather than "serious reasons".
5461
+ export { RETURN_REASON, FINAL_SALE_EXEMPT_REASONS, RETURN_REASON_LABEL, RETURN_REASON_GROUP_LABEL, isFinalSaleExempt, isReturnReason, selectableReturnReasons, } from "./_internal/shared/features/orders/return-reasons";
5457
5462
  // S-SBUNI-4 follow-up â€" shared admin bundle zod schemas.
5458
5463
  export { bundleCreateSchema, bundleUpdateSchema, bundleQueryRuleSchema, bundleDisplaySchema, } from "./_internal/shared/features/categories/bundle-schemas";
5459
5464
  export { navItemFormSchema, navItemCreateSchema, navItemUpdateSchema, } from "./features/admin/schemas/nav-item-form";
@@ -5520,7 +5525,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
5520
5525
  // Checkout rule registry â€" pure TS, safe in both client and server bundles.
5521
5526
  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";
5522
5527
  // Media upload limits â€" pure constants, safe in both client and server bundles.
5523
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
5528
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, 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";
5524
5529
  // ── Field name constants — canonical source of truth for all Firestore field names ──
5525
5530
  // NOTE: STORE_FIELDS, CATEGORY_FIELDS, EVENT_FIELDS, EVENT_ENTRY_FIELDS, PAYOUT_FIELDS,
5526
5531
  // COUPON_FIELDS, CART_FIELDS, USER_FIELDS, ADDRESS_FIELDS, NOTIFICATION_FIELDS,
@@ -5629,3 +5634,7 @@ export { SITE_SETTINGS_TABS, SITE_SETTINGS_TAB_IDS, isSiteSettingsTabId, } from
5629
5634
  export { PAYOUTS_TABS, isPayoutsTabId, SHIPPING_TABS, isShippingTabId, FULFILLMENT_TABS, isFulfillmentTabId, STOREFRONT_TABS, isStorefrontTabId, } from "./features/seller/constants/store-tabs";
5630
5635
  export { ROLES_TABS, isRolesTabId, ADMIN_SETTINGS_TABS, isAdminSettingsTabId, } from "./features/admin/constants/admin-page-tabs";
5631
5636
  export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry";
5637
+ // Seller listing form draft <-> product write payload. The flat form keys and
5638
+ // the nested document blocks are different shapes, and nothing translated
5639
+ // between them — see the module docstring.
5640
+ export { draftToProductInput, productToDraft, } from "./features/seller/utils/product-draft-mapping";
@@ -146,6 +146,7 @@ export declare const DEFAULT_ROUTE_MAP: {
146
146
  readonly ORDER_DETAIL: (id: string) => string;
147
147
  readonly ORDER_CANCEL: (id: string) => string;
148
148
  readonly ORDER_TRACK: (id: string) => string;
149
+ readonly ORDER_RETURN: (id: string) => string;
149
150
  readonly ORDER_INVOICE: (id: string) => string;
150
151
  readonly ORDER_PAYMENT: (id: string) => string;
151
152
  readonly NOTIFICATIONS: "/user/notifications";
@@ -549,6 +550,7 @@ export declare const ROUTES: {
549
550
  readonly ORDER_DETAIL: (id: string) => string;
550
551
  readonly ORDER_CANCEL: (id: string) => string;
551
552
  readonly ORDER_TRACK: (id: string) => string;
553
+ readonly ORDER_RETURN: (id: string) => string;
552
554
  readonly ORDER_INVOICE: (id: string) => string;
553
555
  readonly ORDER_PAYMENT: (id: string) => string;
554
556
  readonly NOTIFICATIONS: "/user/notifications";
@@ -150,6 +150,7 @@ export const DEFAULT_ROUTE_MAP = {
150
150
  ORDER_DETAIL: (id) => `/user/orders/view/${id}`,
151
151
  ORDER_CANCEL: (id) => `/user/orders/${id}/cancel`,
152
152
  ORDER_TRACK: (id) => `/user/orders/${id}/track`,
153
+ ORDER_RETURN: (id) => `/user/orders/${id}/return`,
153
154
  ORDER_INVOICE: (id) => `/user/orders/${id}/invoice`,
154
155
  ORDER_PAYMENT: (id) => `/user/orders/${id}/payment`,
155
156
  NOTIFICATIONS: "/user/notifications",