@mohasinac/appkit 4.71.2 → 4.72.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 (43) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/constants/api-endpoints.d.ts +3 -0
  3. package/dist/constants/api-endpoints.js +6 -0
  4. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -2
  5. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -2
  6. package/dist/features/events/components/EntryVoteButtons.d.ts +19 -0
  7. package/dist/features/events/components/EntryVoteButtons.js +65 -0
  8. package/dist/features/events/repository/entry-votes.repository.d.ts +45 -0
  9. package/dist/features/events/repository/entry-votes.repository.js +154 -0
  10. package/dist/features/events/schemas/entry-votes.d.ts +134 -0
  11. package/dist/features/events/schemas/entry-votes.js +114 -0
  12. package/dist/features/events/schemas/index.d.ts +29 -7
  13. package/dist/features/events/schemas/index.js +29 -2
  14. package/dist/features/inventory/components/AdminMoveLedgerView.d.ts +1 -1
  15. package/dist/features/live/components/LiveItemDetailPageView.js +2 -2
  16. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +3 -3
  17. package/dist/features/products/components/ClearanceDetailPageView.js +2 -2
  18. package/dist/features/products/components/index.d.ts +0 -2
  19. package/dist/features/products/components/index.js +0 -1
  20. package/dist/features/products/schemas/index.d.ts +6 -7
  21. package/dist/features/products/schemas/index.js +20 -2
  22. package/dist/features/promotions/schemas/firestore.d.ts +17 -1
  23. package/dist/features/promotions/schemas/firestore.js +22 -0
  24. package/dist/features/promotions/schemas/index.d.ts +45 -21
  25. package/dist/features/promotions/schemas/index.js +37 -8
  26. package/dist/features/promotions/types/index.d.ts +1 -1
  27. package/dist/features/reviews/schemas/index.d.ts +3 -0
  28. package/dist/features/reviews/schemas/index.js +25 -0
  29. package/dist/index.d.ts +0 -2
  30. package/dist/index.js +0 -3
  31. package/dist/next/routing/portals.d.ts +22 -0
  32. package/dist/next/routing/portals.js +43 -0
  33. package/dist/providers/db-firebase/admin.d.ts +2 -0
  34. package/dist/providers/db-firebase/admin.js +25 -2
  35. package/dist/schemas/registry.d.ts +11 -8
  36. package/dist/styles.css +1 -1
  37. package/dist/tailwind-utilities.css +1 -1
  38. package/dist/ui/components/ListingDetailShell.d.ts +68 -0
  39. package/dist/ui/components/ListingDetailShell.js +32 -0
  40. package/package.json +533 -525
  41. package/scripts/seed-cli.mjs +32 -2
  42. package/dist/features/products/components/PreOrderDetailView.d.ts +0 -18
  43. package/dist/features/products/components/PreOrderDetailView.js +0 -12
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Entry votes — like/dislike on a CONTEST ENTRY, and who cast them.
3
+ *
4
+ * ## Why a sibling of `likes.ts` rather than a field on the entry
5
+ *
6
+ * Deliberately the same shape as `EventLikeDocument`, for the same reasons its
7
+ * header gives: the document id IS the idempotency key, so a repeat vote
8
+ * overwrites itself, a retraction is a `.delete()`, and "how did I vote" is a
9
+ * single `.doc(id).get()` with no query, no index and no transaction.
10
+ *
11
+ * The rejected alternative is the same one: a `votedBy[]` array on the entry.
12
+ * It caps out at Firestore's 1 MiB document limit — a real ceiling for a
13
+ * popular entry and a silent one — and it has no clean un-vote.
14
+ *
15
+ * ## 🛑 The difference from a like: a vote has a VALUE
16
+ *
17
+ * `likes.ts` stores existence — the row is the like. A vote stores a
18
+ * direction, so the row carries `value` and changing your mind is a `.set()`
19
+ * over the same id rather than a delete-then-create. That keeps "one vote per
20
+ * user per entry" true by construction instead of by a rule somebody enforces.
21
+ *
22
+ * ## 🛑 Who may see WHO voted
23
+ *
24
+ * Owner, 2026-11-08: *"only admins see who voted, rest see numbers"*.
25
+ *
26
+ * So the voter list is an admin projection and the counts are the public one.
27
+ * This file stores only `userId` — never a denormalised name or email — for
28
+ * exactly the reason `likes.ts` states: a public surface reads the counts, and
29
+ * the cheapest way to never leak a field is to not store it. An admin screen
30
+ * joins the user's own public projection at read time.
31
+ *
32
+ * ## 🛑 Ranking is a CONFIG RULE, never a fixed formula
33
+ *
34
+ * This file first shipped with `entryScore = likes - dislikes` hardcoded, which
35
+ * is the exact mistake the rework exists to remove: a rule baked into code that
36
+ * an admin then cannot change. There are several legitimate situations — a
37
+ * like-only popularity contest, a net-score vote, a straight turnout count, a
38
+ * jury pick — and the event, not the source, decides which applies.
39
+ *
40
+ * So the rule is a value on the event and `resolveEntryRanking` is the one
41
+ * place it is interpreted. Adding a fifth rule is a case here plus a config
42
+ * option; it is never a second copy of the arithmetic somewhere else.
43
+ *
44
+ * @tag domain:events
45
+ * @tag layer:schema
46
+ */
47
+ export const EVENT_ENTRY_VOTES_COLLECTION = "eventEntryVotes";
48
+ /**
49
+ * The deterministic id — one row per user per entry.
50
+ *
51
+ * 🛑 `__` matches `eventLikeId`, `courierVoteId` and the tester-response id.
52
+ * Firestore ids may not contain `/`, and a single `_` appears inside real
53
+ * uids; a double underscore does not.
54
+ */
55
+ export function eventEntryVoteId(entryId, userId) {
56
+ return `${entryId}__${userId}`;
57
+ }
58
+ export const EVENT_ENTRY_VOTE_FIELDS = {
59
+ EVENT_ID: "eventId",
60
+ ENTRY_ID: "entryId",
61
+ USER_ID: "userId",
62
+ VALUE: "value",
63
+ CREATED_AT: "createdAt",
64
+ UPDATED_AT: "updatedAt",
65
+ };
66
+ /** The default when an event does not say. Matches "most liked photo wins". */
67
+ export const DEFAULT_ENTRY_RANKING = "most-likes";
68
+ /**
69
+ * The ONE place a ranking rule is interpreted.
70
+ *
71
+ * Returns the sort key, descending — so the gallery, the scoreboard, the winner
72
+ * resolution and any export all order entries identically. Two screens
73
+ * disagreeing about who is winning is the failure this prevents.
74
+ *
75
+ * `manual` returns 0 for every entry on purpose: with no computed order, the
76
+ * caller must fall back to the tie-break (earliest first) rather than invent
77
+ * one, and the UI shows a human decision is pending.
78
+ */
79
+ export function resolveEntryRanking(rule, entry) {
80
+ switch (rule) {
81
+ case "most-likes":
82
+ return entry.likes;
83
+ case "net-score":
84
+ return entry.likes - entry.dislikes;
85
+ case "most-votes":
86
+ return entry.likes + entry.dislikes;
87
+ case "earliest":
88
+ /*
89
+ * Negated so that, like every other rule, a HIGHER key sorts first —
90
+ * callers sort one way and need no per-rule direction flag.
91
+ */
92
+ return -entry.submittedAt.getTime();
93
+ case "manual":
94
+ return 0;
95
+ default:
96
+ /*
97
+ * An unknown rule means config names something this build does not
98
+ * implement — a newer rule set against an older deploy. Rank everything
99
+ * equally and let the tie-break order it, which degrades to "earliest
100
+ * first" rather than throwing on a public page. The admin screen is
101
+ * where an unimplemented rule should be surfaced, not the gallery.
102
+ */
103
+ return 0;
104
+ }
105
+ }
106
+ /**
107
+ * The published tie-break: the earlier entry wins.
108
+ *
109
+ * Stated here because it has to be announced BEFORE a contest closes. Deciding
110
+ * a tie afterwards, however fairly, looks like deciding it to suit someone.
111
+ */
112
+ export function entryTieBreak(a, b) {
113
+ return a.submittedAt.getTime() - b.submittedAt.getTime();
114
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./firestore";
2
+ export * from "./entry-votes";
2
3
  import { z } from "zod";
3
4
  /**
4
5
  * 🛑 WRITE enum — the current spellings only. The three chance-based types
@@ -11,7 +12,28 @@ import { z } from "zod";
11
12
  * `normalizeEventType`, and rewritten by
12
13
  * `appkit/scripts/migrate-collector-drop-rename.mjs`.
13
14
  */
14
- export declare const eventTypeSchema: z.ZodEnum<["sale", "offer", "poll", "survey", "feedback", "giveaway", "bonus-spin", "random-draw"]>;
15
+ /**
16
+ * 🛑 OPEN, not a closed enum. Owner, 2026-11-08: *"all event types are also
17
+ * configs … they should not be fixed, nothing in the code should be fixed"*
18
+ * and *"the types we have now are the DEFAULT TEMPLATES"*.
19
+ *
20
+ * The eight below are the shipped templates, kept as a named list so the
21
+ * common ones are discoverable and so a typo in one of THEM is still caught.
22
+ * Anything else is accepted as a slug and resolved against the event-type
23
+ * config (`seed/event-types-seed-data.ts`).
24
+ *
25
+ * The rejected shape was the previous one: a closed `z.enum` that 400s an
26
+ * unknown value. It made a new kind of event — a photo contest judged by votes,
27
+ * say — an enum edit, a schema change and a deploy, which is precisely the
28
+ * coupling the rework removes. The same change is owed to coupons, listings
29
+ * and reviews, which are all still closed today.
30
+ *
31
+ * The slug shape is still enforced, because an unvalidated string here becomes
32
+ * a URL segment and a Firestore query value: lowercase, digits and hyphens,
33
+ * 2-40 characters.
34
+ */
35
+ export declare const DEFAULT_EVENT_TYPE_TEMPLATES: readonly ["sale", "offer", "poll", "survey", "feedback", "giveaway", "bonus-spin", "random-draw"];
36
+ export declare const eventTypeSchema: z.ZodString;
15
37
  export declare const eventStatusSchema: z.ZodEnum<["draft", "active", "paused", "ended", "cancelled"]>;
16
38
  export declare const saleConfigSchema: z.ZodObject<{
17
39
  discountPercent: z.ZodNumber;
@@ -448,7 +470,7 @@ export declare const giveawayConfigClientSchema: z.ZodObject<{
448
470
  export declare const eventItemSchema: z.ZodObject<{
449
471
  id: z.ZodString;
450
472
  slug: z.ZodOptional<z.ZodString>;
451
- type: z.ZodEnum<["sale", "offer", "poll", "survey", "feedback", "giveaway", "bonus-spin", "random-draw"]>;
473
+ type: z.ZodString;
452
474
  title: z.ZodString;
453
475
  description: z.ZodString;
454
476
  status: z.ZodEnum<["draft", "active", "paused", "ended", "cancelled"]>;
@@ -954,7 +976,7 @@ export declare const eventItemSchema: z.ZodObject<{
954
976
  title: string;
955
977
  id: string;
956
978
  status: "cancelled" | "draft" | "active" | "paused" | "ended";
957
- type: "giveaway" | "sale" | "offer" | "poll" | "survey" | "feedback" | "bonus-spin" | "random-draw";
979
+ type: string;
958
980
  description: string;
959
981
  createdAt: string;
960
982
  updatedAt: string;
@@ -1111,7 +1133,7 @@ export declare const eventItemSchema: z.ZodObject<{
1111
1133
  title: string;
1112
1134
  id: string;
1113
1135
  status: "cancelled" | "draft" | "active" | "paused" | "ended";
1114
- type: "giveaway" | "sale" | "offer" | "poll" | "survey" | "feedback" | "bonus-spin" | "random-draw";
1136
+ type: string;
1115
1137
  description: string;
1116
1138
  createdAt: string;
1117
1139
  updatedAt: string;
@@ -1264,7 +1286,7 @@ export declare const eventItemSchema: z.ZodObject<{
1264
1286
  }>;
1265
1287
  /** Base Zod schema for list-query parameters. */
1266
1288
  export declare const eventListParamsSchema: z.ZodObject<{
1267
- type: z.ZodOptional<z.ZodEnum<["sale", "offer", "poll", "survey", "feedback", "giveaway", "bonus-spin", "random-draw"]>>;
1289
+ type: z.ZodOptional<z.ZodString>;
1268
1290
  status: z.ZodOptional<z.ZodEnum<["draft", "active", "paused", "ended", "cancelled"]>>;
1269
1291
  q: z.ZodOptional<z.ZodString>;
1270
1292
  page: z.ZodOptional<z.ZodNumber>;
@@ -1274,14 +1296,14 @@ export declare const eventListParamsSchema: z.ZodObject<{
1274
1296
  sort?: string | undefined;
1275
1297
  q?: string | undefined;
1276
1298
  status?: "cancelled" | "draft" | "active" | "paused" | "ended" | undefined;
1277
- type?: "giveaway" | "sale" | "offer" | "poll" | "survey" | "feedback" | "bonus-spin" | "random-draw" | undefined;
1299
+ type?: string | undefined;
1278
1300
  page?: number | undefined;
1279
1301
  perPage?: number | undefined;
1280
1302
  }, {
1281
1303
  sort?: string | undefined;
1282
1304
  q?: string | undefined;
1283
1305
  status?: "cancelled" | "draft" | "active" | "paused" | "ended" | undefined;
1284
- type?: "giveaway" | "sale" | "offer" | "poll" | "survey" | "feedback" | "bonus-spin" | "random-draw" | undefined;
1306
+ type?: string | undefined;
1285
1307
  page?: number | undefined;
1286
1308
  perPage?: number | undefined;
1287
1309
  }>;
@@ -1,4 +1,5 @@
1
1
  export * from "./firestore.js";
2
+ export * from "./entry-votes.js";
2
3
  import { z } from "zod";
3
4
  import { mediaUrlSchema } from "../../../validation/schemas.js";
4
5
  import { mediaFieldSchema } from "../../media/types/index.js";
@@ -23,7 +24,27 @@ const eventCoverImageSchema = z
23
24
  * `normalizeEventType`, and rewritten by
24
25
  * `appkit/scripts/migrate-collector-drop-rename.mjs`.
25
26
  */
26
- export const eventTypeSchema = z.enum([
27
+ /**
28
+ * 🛑 OPEN, not a closed enum. Owner, 2026-11-08: *"all event types are also
29
+ * configs … they should not be fixed, nothing in the code should be fixed"*
30
+ * and *"the types we have now are the DEFAULT TEMPLATES"*.
31
+ *
32
+ * The eight below are the shipped templates, kept as a named list so the
33
+ * common ones are discoverable and so a typo in one of THEM is still caught.
34
+ * Anything else is accepted as a slug and resolved against the event-type
35
+ * config (`seed/event-types-seed-data.ts`).
36
+ *
37
+ * The rejected shape was the previous one: a closed `z.enum` that 400s an
38
+ * unknown value. It made a new kind of event — a photo contest judged by votes,
39
+ * say — an enum edit, a schema change and a deploy, which is precisely the
40
+ * coupling the rework removes. The same change is owed to coupons, listings
41
+ * and reviews, which are all still closed today.
42
+ *
43
+ * The slug shape is still enforced, because an unvalidated string here becomes
44
+ * a URL segment and a Firestore query value: lowercase, digits and hyphens,
45
+ * 2-40 characters.
46
+ */
47
+ export const DEFAULT_EVENT_TYPE_TEMPLATES = [
27
48
  "sale",
28
49
  "offer",
29
50
  "poll",
@@ -32,7 +53,13 @@ export const eventTypeSchema = z.enum([
32
53
  "giveaway",
33
54
  "bonus-spin",
34
55
  "random-draw",
35
- ]);
56
+ ];
57
+ export const eventTypeSchema = z
58
+ .string()
59
+ .trim()
60
+ .min(2)
61
+ .max(40)
62
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use a lowercase slug, e.g. photo-contest");
36
63
  export const eventStatusSchema = z.enum(["draft", "active", "paused", "ended", "cancelled"]);
37
64
  export const saleConfigSchema = z.object({
38
65
  discountPercent: z.number(),
@@ -7,6 +7,6 @@ export interface AdminMoveLedgerViewProps {
7
7
  export declare function AdminMoveLedgerView({ pageRegionCards }?: AdminMoveLedgerViewProps): React.JSX.Element;
8
8
  /** The kind chips, derived from the union so a sixth kind cannot be missed. */
9
9
  export declare const MOVEMENT_KIND_TABS: {
10
- id: "move" | "receive" | "pick" | "adjust" | "audit";
10
+ id: "move" | "audit" | "receive" | "pick" | "adjust";
11
11
  label: string;
12
12
  }[];
@@ -21,7 +21,7 @@ import { Heading, Span, Text } from "../../../ui/components/Typography.js";
21
21
  import { Main, Nav, Section } from "../../../ui/components/Semantic.js";
22
22
  import { RichText } from "../../../ui/rich-text/RichText.js";
23
23
  import { SpecTable } from "../../../ui/components/SpecTable.js";
24
- import { PreOrderDetailView } from "../../products/components/PreOrderDetailView.js";
24
+ import { ListingDetailShell } from "../../../ui/components/ListingDetailShell.js";
25
25
  import { ProductGalleryClient } from "../../products/components/ProductGalleryClient.js";
26
26
  import { ProductTabsShell } from "../../products/components/ProductTabsShell.js";
27
27
  import { toCustomSectionTabs } from "../../products/components/CustomSectionTabContent.js";
@@ -98,5 +98,5 @@ export async function LiveItemDetailPageView({ slug, initialProduct, renderActio
98
98
  price: price ?? undefined,
99
99
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
100
100
  storeName: storeName ?? undefined,
101
- } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(AdminComposableRegion, { pageId: "live-slug", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "gallery", initialVisible: pdpSectionVisibility.gallery !== false, children: _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }) })), renderInfo: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "info", initialVisible: pdpSectionVisibility.info !== false, children: _jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: String(p.title ?? p.slug ?? p.id), storeId: p.storeId ? String(p.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(p.id))) }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", children: ["Method: ", transport.method, transport.handlingFee !== undefined && (_jsx(PricesOnly, { children: ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}` })), transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", children: meta.careInfo })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] }) })), renderTabs: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "tabs", initialVisible: pdpSectionVisibility.tabs !== false, children: _jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: toCustomSectionTabs(p.customSections) }) })), renderBuyBar: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "actions", initialVisible: pdpSectionVisibility.actions !== false, children: _jsx(Div, { id: "live-item-buy-bar", children: !hasPrimaryActionsSlotOverride && primaryActionsNode }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "grouped-listings", initialVisible: pdpSectionVisibility["grouped-listings"] !== false, children: _jsx(GroupedListingsCarousel, { groups: groups }) }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "related", initialVisible: pdpSectionVisibility.related !== false, children: _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined }) })] })) }), _jsx(ListingBottomActions, { listingType: "live", anchorId: "live-item-buy-bar", price: price, currency: currency })] })] }));
101
+ } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: CLS_BREADCRUMB_LINK, children: "Live" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(AdminComposableRegion, { pageId: "live-slug", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } }), _jsx(ListingDetailShell, { layout: "grid-2", renderGallery: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "gallery", initialVisible: pdpSectionVisibility.gallery !== false, children: _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }) })), renderInfo: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "info", initialVisible: pdpSectionVisibility.info !== false, children: _jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_ITEM_BADGE, children: "Live Item" }), meta?.vendorVerified && (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Verified Seller" })), meta?.cites && (_jsxs(Span, { color: "warning", surface: "warning-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: ["CITES: ", meta.cites] }))] }), _jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: String(p.title ?? p.slug ?? p.id), storeId: p.storeId ? String(p.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(p.id))) }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), meta?.species && (_jsxs(Text, { className: "italic mt-1", color: "muted", size: "sm", children: [meta.species, meta.sex && meta.sex !== "n/a" && ` · ${meta.sex}`, meta.ageMonths !== undefined && ` · ${meta.ageMonths}mo`] }))] }), price !== null && (_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { children: formatCurrency(price, currency) }) })), jurisdictions.length > 0 && (_jsxs(Div, { className: CLS_WARN_BOX, children: [_jsx(Text, { className: CLS_WARN_TITLE, children: "Delivery restrictions" }), _jsxs(Text, { className: CLS_WARN_BODY, children: ["This item can only be shipped to: ", jurisdictions.join(", ")] })] })), transport && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Transport" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", children: ["Method: ", transport.method, transport.handlingFee !== undefined && (_jsx(PricesOnly, { children: ` · Handling: ${formatCurrency(transport.handlingFee, "INR")}` })), transport.insuranceIncluded && " · Insurance included"] })] })), meta?.careInfo && (_jsxs(Div, { textSize: "sm", border: "subtle", surface: "muted", padding: "md", rounded: "lg", children: [_jsx(Text, { weight: "medium", children: "Care information" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", children: meta.careInfo })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] }) })), renderTabs: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "tabs", initialVisible: pdpSectionVisibility.tabs !== false, children: _jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: toCustomSectionTabs(p.customSections) }) })), renderAside: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "actions", initialVisible: pdpSectionVisibility.actions !== false, children: _jsx(Div, { id: "live-item-buy-bar", children: !hasPrimaryActionsSlotOverride && primaryActionsNode }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "grouped-listings", initialVisible: pdpSectionVisibility["grouped-listings"] !== false, children: _jsx(GroupedListingsCarousel, { groups: groups }) }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "related", initialVisible: pdpSectionVisibility.related !== false, children: _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand, storeLabel: storeName ?? undefined }) })] })) }), _jsx(ListingBottomActions, { listingType: "live", anchorId: "live-item-buy-bar", price: price, currency: currency })] })] }));
102
102
  }
@@ -28,7 +28,7 @@ import { Heading, Span, Text } from "../../../ui/components/Typography.js";
28
28
  import { Main, Nav, Section, Ul, Li } from "../../../ui/components/Semantic.js";
29
29
  import { RichText } from "../../../ui/rich-text/RichText.js";
30
30
  import { SpecTable } from "../../../ui/components/SpecTable.js";
31
- import { PreOrderDetailView } from "../../products/components/PreOrderDetailView.js";
31
+ import { ListingDetailShell } from "../../../ui/components/ListingDetailShell.js";
32
32
  import { PreOrderBottomActions } from "./PreOrderBottomActions.js";
33
33
  import { ProductTabsShell } from "../../products/components/ProductTabsShell.js";
34
34
  import { toCustomSectionTabs } from "../../products/components/CustomSectionTabContent.js";
@@ -185,9 +185,9 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
185
185
  price: typeof p.price === "number" ? p.price : undefined,
186
186
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
187
187
  storeName: typeof p.storeName === "string" ? p.storeName : undefined,
188
- } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRE_ORDERS), className: CLS_BREADCRUMB_LINK, children: "Pre-Orders" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(AdminComposableRegion, { pageId: "pre-orders-id", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "gallery", initialVisible: pdpSectionVisibility.gallery !== false, children: _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }) })), renderInfo: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "info", initialVisible: pdpSectionVisibility.info !== false, children: _jsx(PreOrderInfoSection, { title: title, productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined, productionStatus: productionStatus, maxPerUser: maxPerUser, deliveryDate: deliveryDate, featured: featured, freeShipping: freeShipping, condition: condition, isCancellable: isCancellable, finalSale: isFinalSale(p), category: category, categoryName: categoryName, brand: brand, brandSlug: brandSlug, productFeatures: productFeatures, features: features, descriptionHtml: descriptionHtml, safeSeller: safeSeller, storeHref: storeHref }) })), renderSublistingSection: sublistingCategoryId
188
+ } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs(Nav, { "aria-label": "Breadcrumb", layout: "flex-wrap", gap: "2xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRE_ORDERS), className: CLS_BREADCRUMB_LINK, children: "Pre-Orders" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(AdminComposableRegion, { pageId: "pre-orders-id", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } }), _jsx(ListingDetailShell, { layout: "grid-2", renderGallery: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "gallery", initialVisible: pdpSectionVisibility.gallery !== false, children: _jsx(ProductGalleryClient, { images: images, video: productVideo, productName: title }) })), renderInfo: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "info", initialVisible: pdpSectionVisibility.info !== false, children: _jsx(PreOrderInfoSection, { title: title, productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined, productionStatus: productionStatus, maxPerUser: maxPerUser, deliveryDate: deliveryDate, featured: featured, freeShipping: freeShipping, condition: condition, isCancellable: isCancellable, finalSale: isFinalSale(p), category: category, categoryName: categoryName, brand: brand, brandSlug: brandSlug, productFeatures: productFeatures, features: features, descriptionHtml: descriptionHtml, safeSeller: safeSeller, storeHref: storeHref }) })), renderSublistingSection: sublistingCategoryId
189
189
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
190
190
  : undefined, renderGroupSection: groupId
191
191
  ? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
192
- : undefined, renderTabs: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "tabs", initialVisible: pdpSectionVisibility.tabs !== false, children: _jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, customTabs: toCustomSectionTabs(p.customSections) }) })), renderBuyBar: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "actions", initialVisible: pdpSectionVisibility.actions !== false, children: !hasPrimaryActionsSlotOverride && primaryActionsNode })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "grouped-listings", initialVisible: pdpSectionVisibility["grouped-listings"] !== false, children: _jsx(GroupedListingsCarousel, { groups: groups }) }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "related", initialVisible: pdpSectionVisibility.related !== false, children: _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined }) })] })) }), _jsx(PreOrderBottomActions, { price: price, currency: currency })] })] }));
192
+ : undefined, renderTabs: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "tabs", initialVisible: pdpSectionVisibility.tabs !== false, children: _jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, customTabs: toCustomSectionTabs(p.customSections) }) })), renderAside: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "actions", initialVisible: pdpSectionVisibility.actions !== false, children: !hasPrimaryActionsSlotOverride && primaryActionsNode })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "grouped-listings", initialVisible: pdpSectionVisibility["grouped-listings"] !== false, children: _jsx(GroupedListingsCarousel, { groups: groups }) }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "related", initialVisible: pdpSectionVisibility.related !== false, children: _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined }) })] })) }), _jsx(PreOrderBottomActions, { price: price, currency: currency })] })] }));
193
193
  }
@@ -20,7 +20,7 @@ import { Div } from "../../../ui/components/Div.js";
20
20
  import { Heading, Span, Text } from "../../../ui/components/Typography.js";
21
21
  import { Main, Section } from "../../../ui/components/Semantic.js";
22
22
  import { RichText } from "../../../ui/rich-text/RichText.js";
23
- import { PreOrderDetailView } from "./PreOrderDetailView.js";
23
+ import { ListingDetailShell } from "../../../ui/components/ListingDetailShell.js";
24
24
  import { ClearanceBottomActions } from "./ClearanceBottomActions.js";
25
25
  import { ProductTabsShell } from "./ProductTabsShell.js";
26
26
  import { ShareButton } from "./ShareButton.js";
@@ -152,5 +152,5 @@ export async function ClearanceDetailPageView({ id, initialClearance, currentUse
152
152
  price: pricePerEntry,
153
153
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
154
154
  storeName: storeName ?? undefined,
155
- } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CLEARANCE), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Clearance" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(PreOrderDetailView, { renderGallery: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "gallery", initialVisible: pdpSectionVisibility.gallery !== false, children: _jsx(ClearanceCollage, { items: items, hideWonState: true }) })), renderInfo: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "info", initialVisible: pdpSectionVisibility.info !== false, children: _jsxs(Stack, { gap: "md", children: [_jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: String(product.title ?? product.id), storeId: product.storeId ? String(product.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(product.id))) }), _jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: "Clearance" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "medium", children: revealEnd.toLocaleString() })] }))] }) }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) })), _jsx(Disclaimer, { id: "clearance" })] }) })), renderTabs: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "tabs", initialVisible: pdpSectionVisibility.tabs !== false, children: _jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined }) })), renderBuyBar: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "actions", initialVisible: pdpSectionVisibility.actions !== false, children: _jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: !hasPrimaryActionsSlotOverride && primaryActionsNode }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "grouped-listings", initialVisible: pdpSectionVisibility["grouped-listings"] !== false, children: _jsx(GroupedListingsCarousel, { groups: groups }) }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "related", initialVisible: pdpSectionVisibility.related !== false, children: _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName, brandLabel: brand, storeLabel: storeName ?? undefined }) }), _jsx(AdminComposableRegion, { pageId: "clearance-slug", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } })] })) }), _jsx(ClearanceBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
155
+ } }), _jsxs(Container, { size: "xl", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CLEARANCE), className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Clearance" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(ListingDetailShell, { layout: "grid-2", renderGallery: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "gallery", initialVisible: pdpSectionVisibility.gallery !== false, children: _jsx(ClearanceCollage, { items: items, hideWonState: true }) })), renderInfo: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "info", initialVisible: pdpSectionVisibility.info !== false, children: _jsxs(Stack, { gap: "md", children: [_jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: String(product.title ?? product.id), storeId: product.storeId ? String(product.storeId) : undefined, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(String(product.id))) }), _jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: "Clearance" }), _jsx(Span, { size: "xs", weight: "semibold", className: `inline-block ${statusClass(revealStatus)}`, padding: "pill-sm", rounded: "full", children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsx(Div, { className: `${__P.p4}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Entries" }), _jsxs(Text, { size: "sm", weight: "semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window opens" }), _jsx(Text, { size: "sm", weight: "medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Reveal window closes" }), _jsx(Text, { size: "sm", weight: "medium", children: revealEnd.toLocaleString() })] }))] }) }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) })), _jsx(Disclaimer, { id: "clearance" })] }) })), renderTabs: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "tabs", initialVisible: pdpSectionVisibility.tabs !== false, children: _jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined }) })), renderAside: () => (_jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "actions", initialVisible: pdpSectionVisibility.actions !== false, children: _jsx(Div, { padding: "5", id: "prize-draw-buy-bar", border: "subtle", surface: "muted", rounded: "xl", children: !hasPrimaryActionsSlotOverride && primaryActionsNode }) })), renderRelated: () => (_jsxs(Stack, { gap: "xl", children: [_jsx(PurchaseReviewAffordance, { productId: String(product.id), storeId: product.storeId ? String(product.storeId) : undefined }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "grouped-listings", initialVisible: pdpSectionVisibility["grouped-listings"] !== false, children: _jsx(GroupedListingsCarousel, { groups: groups }) }), _jsx(SectionVisibilityGate, { surfaceId: "pdp-sections", sectionKey: "related", initialVisible: pdpSectionVisibility.related !== false, children: _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName, brandLabel: brand, storeLabel: storeName ?? undefined }) }), _jsx(AdminComposableRegion, { pageId: "clearance-slug", initialCards: pageRegionCards, functionalSlots: { "pdp-primary-actions": primaryActionsNode } })] })) }), _jsx(ClearanceBottomActions, { pricePerEntry: pricePerEntry, currency: currency, closed: revealStatus === "closed" || remaining === 0 })] })] }));
156
156
  }
@@ -36,8 +36,6 @@ export { ProductDetailView } from "./ProductDetailView";
36
36
  export type { ProductDetailViewProps } from "./ProductDetailView";
37
37
  export { CollectorDropDetailView } from "./CollectorDropDetailView";
38
38
  export type { CollectorDropDetailViewProps } from "./CollectorDropDetailView";
39
- export { PreOrderDetailView } from "./PreOrderDetailView";
40
- export type { PreOrderDetailViewProps } from "./PreOrderDetailView";
41
39
  export { ProductInfo } from "./ProductInfo";
42
40
  export type { ProductInfoProps } from "./ProductInfo";
43
41
  export { ProductTabs } from "./ProductTabs";
@@ -23,7 +23,6 @@ export { FeatureBadge, FeatureBadgeList } from "./FeatureBadge.js";
23
23
  export { ProductFeaturesProvider, useProductFeatures, } from "./ProductFeaturesContext.js";
24
24
  export { ProductDetailView } from "./ProductDetailView.js";
25
25
  export { CollectorDropDetailView } from "./CollectorDropDetailView.js";
26
- export { PreOrderDetailView } from "./PreOrderDetailView.js";
27
26
  export { ProductInfo } from "./ProductInfo.js";
28
27
  export { ProductTabs } from "./ProductTabs.js";
29
28
  export { BidHistory } from "./BidHistory.js";
@@ -1,6 +1,5 @@
1
1
  export * from "./firestore";
2
2
  import { z } from "zod";
3
- import type { ListingType } from "../types/index";
4
3
  export declare const productImageSchema: z.ZodObject<{
5
4
  url: z.ZodString;
6
5
  thumbnailUrl: z.ZodOptional<z.ZodString>;
@@ -119,7 +118,7 @@ export declare const productItemSchema: z.ZodObject<{
119
118
  status: z.ZodEnum<["draft", "published", "in_review", "archived"]>;
120
119
  condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken", "graded"]>>;
121
120
  madeToOrder: z.ZodOptional<z.ZodBoolean>;
122
- listingType: z.ZodOptional<z.ZodEnum<[ListingType, ...ListingType[]]>>;
121
+ listingType: z.ZodOptional<z.ZodString>;
123
122
  maxPerUser: z.ZodOptional<z.ZodNumber>;
124
123
  partOfBundleIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
125
124
  partOfBundleTitles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -360,7 +359,7 @@ export declare const productItemSchema: z.ZodObject<{
360
359
  insurance?: boolean | undefined;
361
360
  insuranceCost?: number | undefined;
362
361
  shippingPaidBy?: "seller" | "buyer" | undefined;
363
- listingType?: ListingType | undefined;
362
+ listingType?: string | undefined;
364
363
  collectorDropEndDate?: string | Date | undefined;
365
364
  startingBid?: number | undefined;
366
365
  currentBid?: number | undefined;
@@ -505,7 +504,7 @@ export declare const productItemSchema: z.ZodObject<{
505
504
  insurance?: boolean | undefined;
506
505
  insuranceCost?: number | undefined;
507
506
  shippingPaidBy?: "seller" | "buyer" | undefined;
508
- listingType?: ListingType | undefined;
507
+ listingType?: string | undefined;
509
508
  collectorDropEndDate?: string | Date | undefined;
510
509
  startingBid?: number | undefined;
511
510
  currentBid?: number | undefined;
@@ -624,7 +623,7 @@ export declare const productListParamsSchema: z.ZodObject<{
624
623
  minPrice: z.ZodOptional<z.ZodNumber>;
625
624
  maxPrice: z.ZodOptional<z.ZodNumber>;
626
625
  inStock: z.ZodOptional<z.ZodBoolean>;
627
- listingType: z.ZodOptional<z.ZodEnum<[ListingType, ...ListingType[]]>>;
626
+ listingType: z.ZodOptional<z.ZodString>;
628
627
  storeId: z.ZodOptional<z.ZodString>;
629
628
  sort: z.ZodOptional<z.ZodString>;
630
629
  page: z.ZodOptional<z.ZodNumber>;
@@ -638,7 +637,7 @@ export declare const productListParamsSchema: z.ZodObject<{
638
637
  storeId?: string | undefined;
639
638
  featured?: boolean | undefined;
640
639
  condition?: "new" | "like_new" | "good" | "fair" | "poor" | "used" | "refurbished" | "broken" | "graded" | undefined;
641
- listingType?: ListingType | undefined;
640
+ listingType?: string | undefined;
642
641
  page?: number | undefined;
643
642
  minPrice?: number | undefined;
644
643
  maxPrice?: number | undefined;
@@ -652,7 +651,7 @@ export declare const productListParamsSchema: z.ZodObject<{
652
651
  storeId?: string | undefined;
653
652
  featured?: boolean | undefined;
654
653
  condition?: "new" | "like_new" | "good" | "fair" | "poor" | "used" | "refurbished" | "broken" | "graded" | undefined;
655
- listingType?: ListingType | undefined;
654
+ listingType?: string | undefined;
656
655
  page?: number | undefined;
657
656
  minPrice?: number | undefined;
658
657
  maxPrice?: number | undefined;
@@ -1,7 +1,6 @@
1
1
  export * from "./firestore.js";
2
2
  import { z } from "zod";
3
3
  import { mediaFieldSchema } from "../../media/types/index.js";
4
- import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags.js";
5
4
  import { CLEARANCE_DURATION_DAYS_MIN, CLEARANCE_DURATION_DAYS_MAX, } from "../../../_internal/shared/checkout/rules/_limits.js";
6
5
  // --- Sub-schemas --------------------------------------------------------------
7
6
  export const productImageSchema = z.object({
@@ -54,7 +53,26 @@ export const printMetaSchema = z.object({
54
53
  * Registered in `audit-listing-type-tab-coverage`'s STAYS_DERIVED so nobody
55
54
  * re-expands it into a literal for readability.
56
55
  */
57
- const LISTING_TYPE_ENUM = z.enum(ALL_LISTING_TYPES);
56
+ /*
57
+ * 🛑 OPEN, not a closed enum. Owner, 2026-11-08: *"nothing in the code should
58
+ * be fixed … the types we have now are the DEFAULT TEMPLATES"*, and listings
59
+ * were named explicitly alongside events, coupons and reviews.
60
+ *
61
+ * `ALL_LISTING_TYPES` stays the shipped template list — it still drives the
62
+ * admin picker, the seed and every default — but a site that wants a listing
63
+ * kind we never imagined adds a config row rather than waiting for a release.
64
+ *
65
+ * The SHAPE is still enforced, because this value becomes a URL segment, a
66
+ * Firestore query value and an index key: lowercase, digits, hyphen/underscore
67
+ * separated, 2-40 characters.
68
+ */
69
+ const SLUG_RE = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
70
+ const LISTING_TYPE_ENUM = z
71
+ .string()
72
+ .trim()
73
+ .min(2)
74
+ .max(40)
75
+ .regex(SLUG_RE, "Use a lowercase slug, e.g. art-and-stickers");
58
76
  export const productItemSchema = z.object({
59
77
  id: z.string(),
60
78
  title: z.string(),
@@ -181,7 +181,23 @@ export declare const COUPON_FIELDS: {
181
181
  readonly SELLER: "seller";
182
182
  };
183
183
  };
184
- export declare const COUPON_TYPE_LABELS: Record<CouponType, string>;
184
+ /**
185
+ * Labels for the SHIPPED coupon types.
186
+ *
187
+ * 🛑 Since `CouponType` opened (2026-11-08) this is no longer exhaustive — a
188
+ * site-defined type has no entry here and a bare lookup yields `undefined`,
189
+ * which renders as an empty chip. Read it through `couponTypeLabel()`.
190
+ */
191
+ export declare const COUPON_TYPE_LABELS: Record<string, string>;
192
+ /**
193
+ * The label for a coupon type, with a readable fallback.
194
+ *
195
+ * An unknown type is a site-defined one, not an error: it renders its own slug
196
+ * de-slugged ("first_order" -> "First order") rather than blank. A coupon that
197
+ * shows no type at all is worse than one showing a plain name, and it must
198
+ * never throw on a checkout page.
199
+ */
200
+ export declare function couponTypeLabel(type: string): string;
185
201
  export declare const DEFAULT_COUPON_DATA: Partial<CouponDocument>;
186
202
  export declare const COUPONS_PUBLIC_FIELDS: readonly ["id", "code", "name", "description", "type", "discount", "validity.startDate", "validity.endDate", "usage.totalLimit", "usage.perUserLimit"];
187
203
  export declare const COUPONS_UPDATABLE_FIELDS: readonly ["name", "description", "discount", "bxgy", "tiers", "usage", "validity", "restrictions"];
@@ -55,6 +55,13 @@ export const COUPON_FIELDS = {
55
55
  SELLER: "seller",
56
56
  },
57
57
  };
58
+ /**
59
+ * Labels for the SHIPPED coupon types.
60
+ *
61
+ * 🛑 Since `CouponType` opened (2026-11-08) this is no longer exhaustive — a
62
+ * site-defined type has no entry here and a bare lookup yields `undefined`,
63
+ * which renders as an empty chip. Read it through `couponTypeLabel()`.
64
+ */
58
65
  export const COUPON_TYPE_LABELS = {
59
66
  percentage: "Percentage Discount",
60
67
  fixed: "Fixed Amount Discount",
@@ -64,6 +71,21 @@ export const COUPON_TYPE_LABELS = {
64
71
  // our word for it; "Points back" is what appears on their balance.
65
72
  points_earn: "Points Back",
66
73
  };
74
+ /**
75
+ * The label for a coupon type, with a readable fallback.
76
+ *
77
+ * An unknown type is a site-defined one, not an error: it renders its own slug
78
+ * de-slugged ("first_order" -> "First order") rather than blank. A coupon that
79
+ * shows no type at all is worse than one showing a plain name, and it must
80
+ * never throw on a checkout page.
81
+ */
82
+ export function couponTypeLabel(type) {
83
+ const known = COUPON_TYPE_LABELS[type];
84
+ if (known)
85
+ return known;
86
+ const words = type.replace(/[-_]+/g, " ").trim();
87
+ return words ? words.charAt(0).toUpperCase() + words.slice(1) : type;
88
+ }
67
89
  export const DEFAULT_COUPON_DATA = {
68
90
  usage: { currentUsage: 0 },
69
91
  validity: { isActive: true, startDate: new Date(), endDate: undefined },