@mohasinac/appkit 4.57.0 → 4.58.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 (134) hide show
  1. package/dist/.build-stamp.json +4 -4
  2. package/dist/_internal/server/features/beys/data.d.ts +44 -0
  3. package/dist/_internal/server/features/beys/data.js +107 -0
  4. package/dist/_internal/server/features/beys/og.d.ts +42 -0
  5. package/dist/_internal/server/features/beys/og.js +89 -0
  6. package/dist/_internal/server/features/parts/data.js +3 -2
  7. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  8. package/dist/_internal/shared/actions/action-registry.js +86 -0
  9. package/dist/client.d.ts +2 -0
  10. package/dist/client.js +1 -0
  11. package/dist/constants/api-endpoints.d.ts +12 -0
  12. package/dist/constants/api-endpoints.js +10 -0
  13. package/dist/constants/field-names.d.ts +46 -0
  14. package/dist/constants/field-names.js +46 -0
  15. package/dist/features/admin/components/AdminBeysView.d.ts +27 -0
  16. package/dist/features/admin/components/AdminBeysView.js +178 -0
  17. package/dist/features/admin/components/AdminPartsView.d.ts +27 -0
  18. package/dist/features/admin/components/AdminPartsView.js +160 -0
  19. package/dist/features/admin/components/OwnerQuickActions.d.ts +19 -2
  20. package/dist/features/admin/components/OwnerQuickActions.js +31 -3
  21. package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +31 -0
  22. package/dist/features/admin/components/RecordOwnerQuickActions.js +128 -0
  23. package/dist/features/admin/components/ReferenceRecordEditor.d.ts +45 -0
  24. package/dist/features/admin/components/ReferenceRecordEditor.js +153 -0
  25. package/dist/features/admin/schemas/firestore.d.ts +1 -1
  26. package/dist/features/auctions/components/MarketplaceAuctionCard.js +15 -2
  27. package/dist/features/auth/permissions/constants.d.ts +1 -1
  28. package/dist/features/auth/permissions/constants.js +12 -1
  29. package/dist/features/beys/components/BeyDetailPageView.d.ts +50 -0
  30. package/dist/features/beys/components/BeyDetailPageView.js +98 -0
  31. package/dist/features/beys/schemas/firestore.d.ts +417 -0
  32. package/dist/features/beys/schemas/firestore.js +65 -0
  33. package/dist/features/blog/components/BlogFeaturedCard.js +15 -2
  34. package/dist/features/blog/components/BlogPostView.js +2 -2
  35. package/dist/features/categories/components/BrandDetailPageView.js +4 -2
  36. package/dist/features/categories/components/BundleDetailView.js +2 -2
  37. package/dist/features/categories/components/CategoryDetailPageView.js +36 -3
  38. package/dist/features/categories/components/CategoryGrid.js +24 -2
  39. package/dist/features/categories/components/FranchiseDetailPageView.js +2 -3
  40. package/dist/features/events/components/EventCard.js +15 -2
  41. package/dist/features/grouped/components/GroupedListingDetailView.js +2 -3
  42. package/dist/features/homepage/components/CustomCardsSection.js +67 -50
  43. package/dist/features/homepage/components/ShopByCategorySection.js +23 -10
  44. package/dist/features/homepage/schemas/index.d.ts +10 -10
  45. package/dist/features/parts/components/PartDetailPageView.js +5 -3
  46. package/dist/features/parts/components/PartsSearchBox.js +79 -4
  47. package/dist/features/parts/schemas/admin-edit.d.ts +81 -0
  48. package/dist/features/parts/schemas/admin-edit.js +89 -0
  49. package/dist/features/parts/schemas/firestore.d.ts +2 -1
  50. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +15 -2
  51. package/dist/features/products/components/MarketplaceBundleCard.js +15 -2
  52. package/dist/features/products/components/MarketplacePrizeDrawCard.js +15 -2
  53. package/dist/features/products/components/ProductGrid.js +18 -2
  54. package/dist/features/products/schemas/firestore.d.ts +18 -0
  55. package/dist/features/seller/components/CategoryInlineSelect.d.ts +10 -1
  56. package/dist/features/seller/components/CategoryInlineSelect.js +53 -5
  57. package/dist/features/seller/components/SellerProductShell.d.ts +39 -0
  58. package/dist/features/seller/components/SellerProductShell.js +31 -3
  59. package/dist/index.d.ts +8 -0
  60. package/dist/index.js +13 -0
  61. package/dist/next/routing/route-map.d.ts +13 -1
  62. package/dist/next/routing/route-map.js +37 -0
  63. package/dist/schemas/registry.d.ts +6 -6
  64. package/dist/seed/_helpers/category-parts-glossary-map.js +38 -38
  65. package/dist/seed/_helpers/category-parts.js +39 -39
  66. package/dist/seed/bey-products-seed-data.d.ts +18 -0
  67. package/dist/seed/bey-products-seed-data.js +18011 -0
  68. package/dist/seed/beys/chunk0.d.ts +17 -0
  69. package/dist/seed/beys/chunk0.js +36733 -0
  70. package/dist/seed/beys/chunk1.d.ts +17 -0
  71. package/dist/seed/beys/chunk1.js +35728 -0
  72. package/dist/seed/beys/chunk2.d.ts +17 -0
  73. package/dist/seed/beys/chunk2.js +35857 -0
  74. package/dist/seed/beys/chunk3.d.ts +17 -0
  75. package/dist/seed/beys/chunk3.js +34476 -0
  76. package/dist/seed/beys-seed-data.d.ts +18 -0
  77. package/dist/seed/beys-seed-data.js +11 -0
  78. package/dist/seed/homepage-sections-seed-data.js +16 -0
  79. package/dist/seed/index.d.ts +2 -0
  80. package/dist/seed/index.js +3 -0
  81. package/dist/seed/media-assets-seed-data.js +4 -4
  82. package/dist/seed/products-catalogue/chunk0.d.ts +12 -0
  83. package/dist/seed/products-catalogue/chunk0.js +7262 -0
  84. package/dist/seed/products-catalogue/chunk1.d.ts +12 -0
  85. package/dist/seed/products-catalogue/chunk1.js +7290 -0
  86. package/dist/seed/products-catalogue/chunk10.d.ts +12 -0
  87. package/dist/seed/products-catalogue/chunk10.js +7290 -0
  88. package/dist/seed/products-catalogue/chunk11.d.ts +12 -0
  89. package/dist/seed/products-catalogue/chunk11.js +7005 -0
  90. package/dist/seed/products-catalogue/chunk12.d.ts +12 -0
  91. package/dist/seed/products-catalogue/chunk12.js +7002 -0
  92. package/dist/seed/products-catalogue/chunk13.d.ts +12 -0
  93. package/dist/seed/products-catalogue/chunk13.js +7002 -0
  94. package/dist/seed/products-catalogue/chunk14.d.ts +12 -0
  95. package/dist/seed/products-catalogue/chunk14.js +7002 -0
  96. package/dist/seed/products-catalogue/chunk15.d.ts +12 -0
  97. package/dist/seed/products-catalogue/chunk15.js +7002 -0
  98. package/dist/seed/products-catalogue/chunk16.d.ts +12 -0
  99. package/dist/seed/products-catalogue/chunk16.js +7002 -0
  100. package/dist/seed/products-catalogue/chunk17.d.ts +12 -0
  101. package/dist/seed/products-catalogue/chunk17.js +7002 -0
  102. package/dist/seed/products-catalogue/chunk18.d.ts +12 -0
  103. package/dist/seed/products-catalogue/chunk18.js +4398 -0
  104. package/dist/seed/products-catalogue/chunk2.d.ts +12 -0
  105. package/dist/seed/products-catalogue/chunk2.js +7268 -0
  106. package/dist/seed/products-catalogue/chunk3.d.ts +12 -0
  107. package/dist/seed/products-catalogue/chunk3.js +7264 -0
  108. package/dist/seed/products-catalogue/chunk4.d.ts +12 -0
  109. package/dist/seed/products-catalogue/chunk4.js +7268 -0
  110. package/dist/seed/products-catalogue/chunk5.d.ts +12 -0
  111. package/dist/seed/products-catalogue/chunk5.js +7258 -0
  112. package/dist/seed/products-catalogue/chunk6.d.ts +12 -0
  113. package/dist/seed/products-catalogue/chunk6.js +7268 -0
  114. package/dist/seed/products-catalogue/chunk7.d.ts +12 -0
  115. package/dist/seed/products-catalogue/chunk7.js +7311 -0
  116. package/dist/seed/products-catalogue/chunk8.d.ts +12 -0
  117. package/dist/seed/products-catalogue/chunk8.js +7304 -0
  118. package/dist/seed/products-catalogue/chunk9.d.ts +12 -0
  119. package/dist/seed/products-catalogue/chunk9.js +7309 -0
  120. package/dist/seed/products-catalogue-seed-data.d.ts +2 -22
  121. package/dist/seed/products-catalogue-seed-data.js +58 -86382
  122. package/dist/seed/seed-purpose.js +9 -0
  123. package/dist/server.d.ts +2 -0
  124. package/dist/server.js +8 -0
  125. package/dist/styles.css +468 -1
  126. package/dist/tailwind-utilities.css +1 -1
  127. package/dist/ui/components/PosterCard.d.ts +99 -0
  128. package/dist/ui/components/PosterCard.js +109 -0
  129. package/dist/ui/components/PosterCard.style.css +466 -0
  130. package/dist/ui/components/index.style.css +1 -0
  131. package/dist/ui/index.d.ts +2 -0
  132. package/dist/ui/index.js +2 -0
  133. package/package.json +1 -1
  134. package/scripts/seed-cli.mjs +108 -1
@@ -5,7 +5,8 @@ import { categoryCrumbs, withTrailHead } from "../utils/breadcrumb-chain.js";
5
5
  import { ownTotalFor, descendantTotalFor } from "../schemas/category-counts.js";
6
6
  import { categoriesRepository, productRepository, storeRepository, } from "../../../repositories/index.js";
7
7
  import { Container, Div, Heading, ImageMosaic, Main, MOSAIC_MIN, Section, Span, SplitTile, Stack, Text } from "../../../ui/index.js";
8
- import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
8
+ import { TextLink } from "../../../ui/components/TextLink.js";
9
+ import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
9
10
  import { MediaImage } from "../../media/MediaImage.js";
10
11
  import { RichTextRenderer } from "../../../ui/rich-text/RichTextRenderer.js";
11
12
  import { resolveTaxonomyVisual } from "../utils/taxonomy-visual.js";
@@ -18,6 +19,7 @@ import { LastCategoryBeacon } from "./LastCategoryBeacon.js";
18
19
  import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel.js";
19
20
  import { ModelCompositionPanel } from "../../parts/components/ModelCompositionPanel.js";
20
21
  import { getCompositionsForCategory } from "../../../_internal/server/features/parts/data.js";
22
+ import { getBeysForCategory } from "../../../_internal/server/features/beys/data.js";
21
23
  import { ROUTES } from "../../../next/routing/route-map.js";
22
24
  import { getGroupsForCategory } from "../../../_internal/server/features/grouped/data.js";
23
25
  import { listingTabCounts, } from "../../../_internal/server/features/products/listing-tab-counts.js";
@@ -158,7 +160,7 @@ export async function CategoryDetailPageView({ slug }) {
158
160
  // tree deeper than a couple of levels would have turned into a silently blank
159
161
  // page rather than an error (Root Cause #59).
160
162
  const categoriesIn = category?.id ? [category.id] : null;
161
- const [productsResult, tabCounts, bundlesResult, childCategories, rootSiblingCategories, groupedListings, settings, liveCounts, compositions] = await Promise.all([
163
+ const [productsResult, tabCounts, bundlesResult, childCategories, rootSiblingCategories, groupedListings, settings, liveCounts, compositions, pairedBeys] = await Promise.all([
162
164
  categoriesIn
163
165
  ? safeRead(() => productRepository.list({
164
166
  filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("listingType", SIEVE_OP.EQ, "standard")),
@@ -273,6 +275,29 @@ export async function CategoryDetailPageView({ slug }) {
273
275
  fallback: [],
274
276
  })
275
277
  : Promise.resolve([]),
278
+ /*
279
+ * B5 — the bey(s) this leaf names, so the reference page is REACHABLE.
280
+ *
281
+ * 🛑 `ROUTES.PUBLIC.BEY_DETAIL` existed with ZERO call sites: 1,988 bey
282
+ * pages were built, routable and unreachable from anywhere in the product.
283
+ * The link already ran the other way (`/beys/{slug}` -> its category leaf),
284
+ * so this closes the loop rather than inventing an edge.
285
+ *
286
+ * Same shape as the compositions read directly above — one indexed
287
+ * equality against this category's OWN id, in the SAME `Promise.all` so it
288
+ * costs no extra round trip, wrapped in `safeRead` so a missing index
289
+ * records a DEGRADED_READ instead of taking the page down.
290
+ *
291
+ * A line or generation node matches nothing and renders nothing, which is
292
+ * correct: "Burst GT" is not a bey, the 1,858 leaves under it are.
293
+ */
294
+ category?.id
295
+ ? safeRead(() => getBeysForCategory(category.id), {
296
+ route: "/categories/[slug]",
297
+ key: "beys.beysForCategory",
298
+ fallback: [],
299
+ })
300
+ : Promise.resolve([]),
276
301
  ]);
277
302
  // 🛑 SAME TIER ONLY. `rootSiblingCategories` spans the whole root subtree —
278
303
  // siblings AND cousins across every depth — so without a tier check this
@@ -573,7 +598,15 @@ export async function CategoryDetailPageView({ slug }) {
573
598
  const ldFaq = category?.faqs && category.faqs.length > 0
574
599
  ? faqJsonLd(category.faqs.map((f) => ({ question: f.question, answer: f.answer })))
575
600
  : null;
576
- return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && hasMosaic && (_jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", padding: "x-md", children: _jsx(Div, { padding: "t-lg", children: _jsx(ImageMosaic, { images: mosaicImages, alt: `${category?.name ?? slug} on LetItRip` }) }) })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsx(BreadcrumbTrail, { crumbs: trail, className: "mb-4", tone: hasCover ? "on-media" : "default" }), category?.id && _jsx(LastCategoryBeacon, { categoryId: category.id }), category?.id && (_jsx(OwnerQuickActions, { resource: "CATEGORY", placement: "detail", className: "mb-3", recordLabel: category.name ?? slug, editHref: String(ROUTES.ADMIN.CATEGORIES_EDIT(category.id)) })), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), showCountSplit && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "muted", size: "xs", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [ownHere.toLocaleString(), " here \u00B7 ", inSubcategories.toLocaleString(), " in subcategories"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), category?.hasbroName && (_jsx(Section, { padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Row, { gap: "md", align: "center", wrap: true, children: [category.hasbroCoverImage && (_jsx(MediaImage, { src: category.hasbroCoverImage, alt: `${category.hasbroName} logo`, size: "thumbnail", className: "h-12 w-auto flex-shrink-0" })), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", transform: "uppercase", weight: "medium", children: "Also known as" }), _jsx(Text, { weight: "semibold", children: category.hasbroName }), _jsxs(Text, { size: "xs", color: "muted", children: ["The same wave, named differently outside Japan \u2014 Takara-Tomy releases it as ", category.name, ", Hasbro as ", category.hasbroName, ". Both names find these listings."] })] })] }) }) })), contentBody && contentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: contentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: category?.highlights, faqs: category?.faqs }), compositions.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "md", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "What it is made of" }), _jsx(ModelCompositionPanel, { compositions: compositions, hrefForPart: (slot, tail) => String(ROUTES.PUBLIC.PART_DETAIL(slot, tail)) })] }) }) })), (() => {
601
+ return (_jsxs(Main, { children: [_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldBreadcrumb) } }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldCollection) } }), ldFaq && (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(ldFaq) } })), _jsxs(Section, { className: `relative ${__O.hidden} ${hasCover ? "min-h-[220px] md:min-h-[280px]" : "bg-[var(--appkit-color-bg)]"}`, children: [hasCover && (_jsxs(_Fragment, { children: [_jsx(Div, { className: "absolute inset-0", children: _jsx(MediaImage, { src: coverImage, alt: "", size: "banner" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), !hasCover && hasMosaic && (_jsx(Div, { className: "relative z-10 max-w-7xl mx-auto", padding: "x-md", children: _jsx(Div, { padding: "t-lg", children: _jsx(ImageMosaic, { images: mosaicImages, alt: `${category?.name ?? slug} on LetItRip` }) }) })), _jsxs(Div, { className: "relative z-10 max-w-7xl mx-auto", paddingY: hasCover ? "y-3xl" : "y-xl", padding: "x-md", children: [_jsx(BreadcrumbTrail, { crumbs: trail, className: "mb-4", tone: hasCover ? "on-media" : "default" }), category?.id && _jsx(LastCategoryBeacon, { categoryId: category.id }), category?.id && (_jsx(RecordOwnerQuickActions, { resource: "CATEGORY", recordId: category.id, placement: "detail",
602
+ /*
603
+ * Same flag the rest of this hero branches on. Without it the
604
+ * pencil renders `ghost` (zinc-600) on the dark cover and is
605
+ * invisible — see Root Cause #67 and `OwnerQuickActions.onMedia`.
606
+ */
607
+ onMedia: hasCover, className: "mb-3", recordLabel: category.name ?? slug })), _jsx(Heading, { color: "inverse", level: 1, className: `mb-2 ${hasCover ? "" : "text-[var(--appkit-color-text)]"}`, size: "3xl", mdSize: "4xl", weight: "bold", children: category?.name ?? slug }), category?.description && typeof category.description === "string" && !category.description.startsWith("{") && (_jsx(Text, { color: hasCover ? "inverse" : "muted", className: `max-w-2xl mb-4 ${hasCover ? "/80" : ""}`, size: "base", children: category.description })), _jsxs(Row, { wrap: true, gap: "sm", children: [productCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: "inverse", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "bg-primary/10 text-primary-700 dark:text-primary-400", rounded: "full", padding: "pill-sm-tall", children: [productCount.toLocaleString(), " ", productCount === 1 ? "product" : "products"] })), showCountSplit && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "muted", size: "xs", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [ownHere.toLocaleString(), " here \u00B7 ", inSubcategories.toLocaleString(), " in subcategories"] })), auctionCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "warning", surface: hasCover ? undefined : "warning-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [auctionCount.toLocaleString(), " ", auctionCount === 1 ? "auction" : "auctions"] })), preOrderCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "info", surface: hasCover ? undefined : "info-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [preOrderCount.toLocaleString(), " ", preOrderCount === 1 ? "pre-order" : "pre-orders"] })), storeCount > 0 && (_jsxs(Span, { layout: "inline-flex", gap: "xs", color: hasCover ? "inverse" : "success", surface: hasCover ? undefined : "success-surface", size: "xs", weight: "medium", className: hasCover ? "bg-[rgba(255,255,255,0.2)] backdrop-blur-sm" : "", rounded: "full", padding: "pill-sm-tall", children: [storeCount.toLocaleString(), " ", storeCount === 1 ? "store" : "stores"] }))] })] })] }), category?.hasbroName && (_jsx(Section, { padding: "y-md", children: _jsx(Container, { size: "xl", children: _jsxs(Row, { gap: "md", align: "center", wrap: true, children: [category.hasbroCoverImage && (_jsx(MediaImage, { src: category.hasbroCoverImage, alt: `${category.hasbroName} logo`, size: "thumbnail", className: "h-12 w-auto flex-shrink-0" })), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", transform: "uppercase", weight: "medium", children: "Also known as" }), _jsx(Text, { weight: "semibold", children: category.hasbroName }), _jsxs(Text, { size: "xs", color: "muted", children: ["The same wave, named differently outside Japan \u2014 Takara-Tomy releases it as ", category.name, ", Hasbro as ", category.hasbroName, ". Both names find these listings."] })] })] }) }) })), contentBody && contentBody.trim().length > 0 && (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "xl", children: _jsx(RichTextRenderer, { html: contentBody }) }) })), _jsx(CategoryHighlightsAndFaqSection, { highlights: category?.highlights, faqs: category?.faqs }), pairedBeys.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: pairedBeys.length === 1 ? "Specifications" : "Releases" }), _jsx(Stack, { gap: "xs", className: "min-w-0", children: pairedBeys.map((bey) => (_jsxs(Row, { gap: "sm", align: "center", wrap: true, className: "min-w-0", children: [_jsx(TextLink, { href: String(ROUTES.PUBLIC.BEY_DETAIL(bey.slug)), children: bey.name }), [bey.system, bey.productCodes?.[0]]
608
+ .filter(Boolean)
609
+ .map((bit) => (_jsx(Text, { size: "sm", color: "muted", children: String(bit) }, String(bit))))] }, bey.id))) })] }) }) })), compositions.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-b", children: _jsx(Div, { className: __W, padding: "section", children: _jsxs(Stack, { gap: "md", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "What it is made of" }), _jsx(ModelCompositionPanel, { compositions: compositions, hrefForPart: (slot, tail) => String(ROUTES.PUBLIC.PART_DETAIL(slot, tail)) })] }) }) })), (() => {
577
610
  if (category?.tier !== 0)
578
611
  return null;
579
612
  const branches = childCategories.filter((c) => c.tier === 1);
@@ -5,6 +5,7 @@ import { Div, Grid, Row, Span, Stack, Text } from "../../../ui/index.js";
5
5
  import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv.js";
6
6
  import { MediaImage } from "../../media/MediaImage.js";
7
7
  import { ImageMosaic, MOSAIC_MIN } from "../../../ui/components/ImageMosaic.js";
8
+ import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
8
9
  const __P = {
9
10
  p3: "p-[var(--appkit-space-3)]",
10
11
  };
@@ -35,13 +36,34 @@ export function CategoryCard({ category, href, onClick, className = "", renderOw
35
36
  const card = (_jsx(Div, { role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: onClick
36
37
  ? (e) => (e.key === "Enter" || e.key === " ") && onClick(category)
37
38
  : undefined, onClick: onClick ? () => onClick(category) : undefined, className: cardClass, children: inner }));
38
- if (!renderOwnerActions)
39
+ /*
40
+ * 🛑 The quick actions are the DEFAULT, not an opt-in.
41
+ *
42
+ * This was `if (!renderOwnerActions) return card;` and NOTHING in either
43
+ * repo ever passed `renderOwnerActions` — measured across all nine card
44
+ * components that declare it. So the pencil was unreachable on every grid in
45
+ * the app, and the slot read as wired because the prop, the docstring and
46
+ * the positioning were all present. Root Cause #8's shape: a slot shell
47
+ * rendered with zero render props.
48
+ *
49
+ * A card is resource-specific by construction — a CategoryCard is always a
50
+ * category — so it can name its own resource and needs no threading through
51
+ * the dozens of listing views that mount it. `RecordOwnerQuickActions` is a
52
+ * Client Component receiving only strings, which is the ordinary RSC
53
+ * boundary; the thing that cannot cross it is a FUNCTION prop, which is why
54
+ * the detail views could not pass `onDelete` directly.
55
+ *
56
+ * The override seam stays for a caller that genuinely needs different
57
+ * actions.
58
+ */
59
+ const ownerActions = renderOwnerActions ? (renderOwnerActions()) : category.id ? (_jsx(RecordOwnerQuickActions, { resource: "CATEGORY", recordId: String(category.id), recordLabel: category.name, placement: "card" })) : null;
60
+ if (!ownerActions)
39
61
  return card;
40
62
  /*
41
63
  * Sibling of the card, never a child — see the prop's docstring. `z-20`
42
64
  * beats the featured dot at `left-2 top-2`.
43
65
  */
44
- return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
66
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: ownerActions })] }));
45
67
  }
46
68
  export function CategoryGrid({ categories, getHref, onCategoryClick, emptyLabel = "No categories found", className = "", }) {
47
69
  if (categories.length === 0) {
@@ -9,8 +9,7 @@ import { Heading, Text } from "../../../ui/components/Typography.js";
9
9
  import { MediaImage } from "../../media/MediaImage.js";
10
10
  import { ProductGrid } from "../../products/components/ProductGrid.js";
11
11
  import { Alert } from "../../../ui/components/Alert.js";
12
- import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
13
- import { ROUTES } from "../../../next/routing/route-map.js";
12
+ import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
14
13
  const PAGE_SIZE = 24;
15
14
  export async function FranchiseDetailPageView({ slug, franchise, }) {
16
15
  const result = await safeRead(() => listPublicProducts({
@@ -39,7 +38,7 @@ export async function FranchiseDetailPageView({ slug, franchise, }) {
39
38
  const products = rows.map((r) => toProductItem(r));
40
39
  const total = reportedTotal ?? products.length;
41
40
  const cover = franchise.display?.coverImage;
42
- return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsxs(Row, { align: "center", gap: "comfortable", className: "mb-8", children: [cover ? (_jsx(MediaImage, { src: cover, alt: franchise.name ?? slug, size: "thumbnail", rounded: "lg" })) : null, _jsxs(Stack, { gap: "dense", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, children: [_jsx(Heading, { level: 1, color: "primary", size: "3xl", weight: "semibold", children: franchise.name }), franchise.id ? (_jsx(OwnerQuickActions, { resource: "FRANCHISE", placement: "detail", recordLabel: franchise.name ?? slug, editHref: String(ROUTES.ADMIN.CATEGORIES_EDIT(franchise.id)) })) : null] }), franchise.description ? (_jsx(Text, { color: "muted", className: "max-w-2xl", children: franchise.description })) : null] })] }), franchise.isActive === false ? (_jsxs(Alert, { variant: "info", className: "mb-6", children: ["We are not stocking ", franchise.name, " yet. This page is here so the link works \u2014 check back, or browse what we do carry."] })) : null, readFailed ? (_jsxs(Alert, { variant: "warning", children: ["We could not load ", franchise.name, " listings just now. Please try again."] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "muted", className: "mb-4", children: total === 0
41
+ return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsxs(Row, { align: "center", gap: "comfortable", className: "mb-8", children: [cover ? (_jsx(MediaImage, { src: cover, alt: franchise.name ?? slug, size: "thumbnail", rounded: "lg" })) : null, _jsxs(Stack, { gap: "dense", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, children: [_jsx(Heading, { level: 1, color: "primary", size: "3xl", weight: "semibold", children: franchise.name }), franchise.id ? (_jsx(RecordOwnerQuickActions, { resource: "FRANCHISE", recordId: franchise.id, placement: "detail", recordLabel: franchise.name ?? slug })) : null] }), franchise.description ? (_jsx(Text, { color: "muted", className: "max-w-2xl", children: franchise.description })) : null] })] }), franchise.isActive === false ? (_jsxs(Alert, { variant: "info", className: "mb-6", children: ["We are not stocking ", franchise.name, " yet. This page is here so the link works \u2014 check back, or browse what we do carry."] })) : null, readFailed ? (_jsxs(Alert, { variant: "warning", children: ["We could not load ", franchise.name, " listings just now. Please try again."] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "muted", className: "mb-4", children: total === 0
43
42
  ? "No listings yet."
44
43
  : `${total}${truncated ? "+" : ""} listing${total === 1 ? "" : "s"}` }), _jsx(ProductGrid, { products: products, emptyLabel: "No listings yet." })] }))] }) }) }));
45
44
  }
@@ -8,6 +8,7 @@ import { EVENT_FIELDS } from "../schemas/index.js";
8
8
  import { EventStatusBadge } from "./EventStatusBadge.js";
9
9
  import { ROUTES } from "../../../next/routing/route-map.js";
10
10
  import { useLongPress } from "../../../react/hooks/useLongPress.js";
11
+ import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
11
12
  const __P = {
12
13
  p4: "p-[var(--appkit-space-4)]",
13
14
  };
@@ -34,11 +35,23 @@ export function EventCard({ event, labels = {}, onParticipate, className = "", s
34
35
  const detailHref = String(ROUTES.PUBLIC.EVENT_DETAIL(event.slug ?? event.id));
35
36
  const card = (_jsxs(Stack, { as: "article", border: "default", rounded: "xl", shadow: "hover-md", gap: "none", className: `group relative h-full overflow-hidden bg-[var(--appkit-color-surface)] ${isSelected ? "border-primary outline outline-2 outline-primary" : " "} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(event.id, !isSelected); }, label: isSelected ? "Deselect event" : "Select event", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsx(Link, { href: detailHref, className: "block flex-shrink-0", children: event.coverImageUrl || event.coverImage?.url ? (_jsx(Div, { className: `relative aspect-video ${__O.hidden}`, children: _jsx(MediaImage, { src: event.coverImageUrl || event.coverImage?.url || "", alt: safeTitle, size: "card", className: "transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Row, { surface: "muted", className: "aspect-video", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-40", size: "5xl", "aria-hidden": "true", children: TYPE_ICONS[event.type] }) })) }), _jsxs(Stack, { className: `flex-1 ${__P.p4}`, children: [_jsxs(Row, { className: "mb-2", align: "start", justify: "between", gap: "sm", children: [_jsx(Span, { size: "lg", "aria-hidden": "true", children: TYPE_ICONS[event.type] }), _jsx(EventStatusBadge, { status: event.status })] }), _jsx(Link, { href: detailHref, className: "block", children: _jsx(Heading, { level: 3, className: "text-[var(--appkit-color-text)] leading-snug mb-1 group-hover:text-primary transition-colors", size: "base", weight: "semibold", children: safeTitle }) }), _jsx(RichText, { html: normalizeRichTextHtml(event.description ?? ""), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mb-3 line-clamp-3 text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)]" }), _jsxs(Row, { color: "muted", textSize: "xs", className: "mb-3 mt-auto", align: "center", justify: "between", children: [event.status === EVENT_FIELDS.STATUS_VALUES.ACTIVE &&
36
37
  daysLeft > 0 && _jsxs(Span, { children: ["\u23F1 ", daysLeft, "d remaining"] }), _jsxs(Span, { children: ["\uD83D\uDC65 ", event.stats.totalEntries, " ", labels.entries ?? "entries"] })] }), event.status === EVENT_FIELDS.STATUS_VALUES.ACTIVE && onParticipate ? (_jsx(Button, { rounded: "lg", type: "button", onClick: () => onParticipate(event), className: "w-full bg-primary py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-white transition-colors hover:bg-primary-600", children: labels.participate ?? "Participate" })) : (_jsxs(TextLink, { rounded: "lg", paddingX: "sm", paddingY: "xs", href: detailHref, layout: "inline-flex", align: "center", justify: "center", gap: "xs", className: "w-full border border-[var(--appkit-color-border)] transition-colors hover:bg-[var(--appkit-color-bg)]", color: "primary", size: "sm", weight: "medium", children: [labels.viewDetails ?? "View details", " \u2192"] }))] })] }));
37
- if (!renderOwnerActions)
38
+ /*
39
+ * 🛑 Default, not opt-in. Nothing in either repo ever passed
40
+ * `renderOwnerActions`, so this toolbar was unreachable on every grid that
41
+ * mounts this card (Root Cause #8 — a slot shell rendered with zero render
42
+ * props). A card is resource-specific by construction, so it names its own
43
+ * resource rather than being threaded one; the prop stays as an override.
44
+ */
45
+ const ownerActions = renderOwnerActions
46
+ ? renderOwnerActions()
47
+ : event.id
48
+ ? (_jsx(RecordOwnerQuickActions, { resource: "EVENT", recordId: String(event.id), recordLabel: event.title, placement: "card" }))
49
+ : null;
50
+ if (!ownerActions)
38
51
  return card;
39
52
  /*
40
53
  * Sibling of the card, never a child — see the prop's docstring. `z-20`
41
54
  * beats this card's own overlays, which all sit at `z-10`.
42
55
  */
43
- return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: renderOwnerActions() })] }));
56
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 top-2 z-20", children: ownerActions })] }));
44
57
  }
@@ -4,8 +4,7 @@ import { Container, Div, Heading, Section, Span, Stack, Text, } from "../../../u
4
4
  import { MediaImage } from "../../media/MediaImage.js";
5
5
  import { GroupMemberPicker } from "../../products/components/GroupMemberPicker.js";
6
6
  import { readGroupRules } from "../../../_internal/shared/features/products/group-rules.js";
7
- import { OwnerQuickActions } from "../../../features/admin/components/OwnerQuickActions.js";
8
- import { ROUTES } from "../../../next/routing/route-map.js";
7
+ import { RecordOwnerQuickActions } from "../../../features/admin/components/RecordOwnerQuickActions.js";
9
8
  const __O = { hidden: "overflow-hidden" };
10
9
  /** Human label per theme — mirrors GROUP_THEME_TITLE on the carousel. */
11
10
  const THEME_LABEL = {
@@ -16,7 +15,7 @@ const THEME_LABEL = {
16
15
  generic: "Grouped listing",
17
16
  };
18
17
  export function GroupedListingDetailView({ group, members }) {
19
- return (_jsx(Section, { padding: "section", children: _jsx(Container, { children: _jsxs(Stack, { gap: "comfortable", children: [group.coverImage && (_jsx(Div, { className: `relative h-48 w-full md:h-64 ${__O.hidden}`, rounded: "xl", surface: "muted", children: _jsx(MediaImage, { src: group.coverImage, alt: group.title, size: "banner" }) })), _jsxs(Stack, { gap: "xs", children: [_jsx(Span, { size: "xs", color: "muted", transform: "uppercase", weight: "semibold", children: THEME_LABEL[group.groupTheme] }), _jsx(OwnerQuickActions, { resource: "GROUP", placement: "detail", recordLabel: String(group.title), storeId: group.storeId ? String(group.storeId) : undefined, editHref: String(ROUTES.ADMIN.GROUPED_LISTINGS_EDIT(String(group.id))) }), _jsx(Heading, { level: 1, children: group.title }), group.description && (_jsx(Text, { color: "muted", children: group.description })), _jsxs(Text, { size: "sm", color: "muted", children: [members.length, " item", members.length === 1 ? "" : "s", " in this group"] })] }), members.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "lg", children: "Pick what you want" }), _jsx(Text, { size: "sm", color: "muted", children: "Choose a quantity for each item \u2014 they'll go into your cart as a single line you can edit later." }), _jsx(GroupMemberPicker, { groupId: group.id, groupSource: "grouped-listing", members: members,
18
+ return (_jsx(Section, { padding: "section", children: _jsx(Container, { children: _jsxs(Stack, { gap: "comfortable", children: [group.coverImage && (_jsx(Div, { className: `relative h-48 w-full md:h-64 ${__O.hidden}`, rounded: "xl", surface: "muted", children: _jsx(MediaImage, { src: group.coverImage, alt: group.title, size: "banner" }) })), _jsxs(Stack, { gap: "xs", children: [_jsx(Span, { size: "xs", color: "muted", transform: "uppercase", weight: "semibold", children: THEME_LABEL[group.groupTheme] }), _jsx(RecordOwnerQuickActions, { resource: "GROUP", recordId: String(group.id), placement: "detail", recordLabel: String(group.title), storeId: group.storeId ? String(group.storeId) : undefined }), _jsx(Heading, { level: 1, children: group.title }), group.description && (_jsx(Text, { color: "muted", children: group.description })), _jsxs(Text, { size: "sm", color: "muted", children: [members.length, " item", members.length === 1 ? "" : "s", " in this group"] })] }), members.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "lg", children: "Pick what you want" }), _jsx(Text, { size: "sm", color: "muted", children: "Choose a quantity for each item \u2014 they'll go into your cart as a single line you can edit later." }), _jsx(GroupMemberPicker, { groupId: group.id, groupSource: "grouped-listing", members: members,
20
19
  /*
21
20
  * Off the group document this view already loaded. The picker
22
21
  * and `addGroupLineToCart` run the SAME `checkGroupRules`, so
@@ -1,61 +1,73 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Row } from "@mohasinac/appkit/ui";
4
- import { Anchor, Div, Heading, HorizontalScroller, Section, Stack, Text } from "../../../ui/index.js";
5
- import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv.js";
3
+ import Link from "next/link.js";
4
+ import { Div, Heading, HorizontalScroller, PlainCard, PLAIN_CLASS, PosterCard, PosterGrid, POSTER_CLASS, Section, } from "../../../ui/index.js";
6
5
  import { THEMED_TEXT_PRIMARY } from "../../../_internal/shared/styles/themed.js";
6
+ /* Not re-exported from `ui/index` — import from the defining module (Root Cause #18). */
7
+ import { Iframe } from "../../../ui/components/Iframe.js";
7
8
  import { SectionCarousel } from "./SectionCarousel.js";
8
9
  import { MediaImage } from "../../media/MediaImage.js";
9
- const __P = {
10
- p4: "p-[var(--appkit-space-4)]",
11
- };
12
- const __O = {
13
- hidden: "overflow-hidden",
14
- };
15
10
  // --- Constants ---------------------------------------------------------------
16
11
  const CLS_CONTAINER = "w-full max-w-7xl mx-auto px-[var(--appkit-space-4)]";
17
- // --- Helpers -----------------------------------------------------------------
18
- const COLS_CLASS = {
19
- 1: "grid-cols-1",
20
- 2: "grid-cols-1 sm:grid-cols-2",
21
- 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
22
- 4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
23
- };
24
- const RADIUS_CLASS = {
25
- none: "rounded-none",
26
- sm: "rounded-sm",
27
- md: "rounded-md",
28
- lg: "rounded-lg",
29
- xl: "rounded-xl",
30
- full: "rounded-full",
31
- };
32
- const SHADOW_CLASS = {
33
- none: "",
34
- sm: "shadow-sm",
35
- md: "shadow-md",
36
- lg: "shadow-lg",
37
- };
38
- const VARIANT_CLASS = {
39
- primary: "inline-flex items-center justify-center px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] rounded-md bg-[var(--appkit-color-primary)] text-white text-[length:var(--appkit-text-sm)] font-medium hover:opacity-90 transition-opacity",
40
- secondary: "inline-flex items-center justify-center px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] rounded-md bg-[var(--appkit-color-secondary)] text-white text-[length:var(--appkit-text-sm)] font-medium hover:opacity-90 transition-opacity",
41
- outline: "inline-flex items-center justify-center px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] rounded-md border border-current text-[length:var(--appkit-text-sm)] font-medium hover:bg-black/5 transition-colors",
42
- ghost: "inline-flex items-center justify-center px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium hover:underline transition-colors",
43
- };
12
+ function isKeyChip(variant) {
13
+ return variant !== "ghost";
14
+ }
15
+ /**
16
+ * 🛑 `next/link` for an internal href, a plain `<a>` only for an external one.
17
+ *
18
+ * A bare anchor on an internal route does a FULL PAGE LOAD — the regression
19
+ * documented on `OverflowRow.linkAs`, and invisible apart from the flash. The
20
+ * hrefs here are admin-authored, so both kinds genuinely occur.
21
+ */
22
+ function Chip({ button, className, }) {
23
+ const external = button.target === "_blank" || /^https?:\/\//.test(button.href);
24
+ if (external) {
25
+ return (_jsx("a", { href: button.href, className: className, target: button.target ?? "_blank", rel: "noopener noreferrer", children: button.label }));
26
+ }
27
+ return (_jsx(Link, { href: button.href, className: className, children: button.label }));
28
+ }
29
+ /**
30
+ * Peers first, destinations last.
31
+ *
32
+ * The seeded order interleaves them ("… Heavy Metal System, Parts, Launchers"
33
+ * on one card, "Parts" mid-row on another), so the solid chips landed in a
34
+ * different place on every card and the row had no readable shape. Sorting is
35
+ * stable, so the authored order within each group is preserved.
36
+ */
37
+ function orderedButtons(buttons) {
38
+ return [...buttons].sort((a, b) => Number(isKeyChip(a.variant)) - Number(isKeyChip(b.variant)));
39
+ }
44
40
  // --- Card Item ---------------------------------------------------------------
45
41
  function CardItem({ card }) {
46
- const radiusClass = RADIUS_CLASS[card.borderRadius ?? "lg"] ?? "rounded-lg";
47
- const shadowClass = SHADOW_CLASS[card.shadowLevel ?? "sm"] ?? "shadow-sm";
48
- return (_jsxs(DynamicBgDiv, { color: card.backgroundColor, textColor: card.textColor, className: `flex flex-col overflow-hidden ${radiusClass} ${shadowClass} bg-[var(--appkit-color-surface)]`, children: [card.image && (_jsx(Div, { className: `relative w-full aspect-video ${__O.hidden}`, children: _jsx(MediaImage, { src: card.image, alt: card.imageAlt ?? "", size: "card" }) })), _jsxs(Stack, { className: `${__P.p4} flex-1`, gap: "sm", children: [card.eyebrow && (_jsx(Text, { className: "tracking-widest opacity-70", size: "xs", weight: "semibold", transform: "uppercase", children: card.eyebrow })), card.title && (_jsx(Text, { className: "leading-snug", size: "base", weight: "bold", children: card.title })), card.body && (_jsx(Text, { className: "opacity-80 leading-relaxed flex-1", size: "sm", children: card.body })), card.buttons && card.buttons.length > 0 && (_jsx(Row, { gap: "sm", wrap: true, className: "mt-2", children: card.buttons.map((btn, i) => (_jsx(Anchor, { href: btn.href, target: btn.target ?? "_self", rel: btn.target === "_blank" ? "noopener noreferrer" : undefined, tone: "none", underline: "none", className: VARIANT_CLASS[btn.variant] ?? VARIANT_CLASS.primary, children: btn.label }, i))) })), card.formEmbed && (_jsx("iframe", { src: card.formEmbed, sandbox: "allow-forms allow-scripts", loading: "lazy", className: "w-full mt-3 border-0 rounded-md min-h-[200px]", title: "Embedded form" }))] })] }));
42
+ const buttons = card.buttons?.length ? orderedButtons(card.buttons) : [];
43
+ /*
44
+ * With artwork → the poster treatment: the image fills the card and the
45
+ * scrim carries the type. Without → the plain card. Branching on the data
46
+ * rather than on a config flag keeps every OTHER `custom-cards` section an
47
+ * admin has authored rendering exactly as before.
48
+ */
49
+ if (card.image) {
50
+ return (_jsx(PosterCard, { height: "tall", media: _jsx(MediaImage, { src: card.image, alt: card.imageAlt ?? "", size: "card", objectFit: "cover" }), eyebrow: card.eyebrow, title: card.title, text: card.body, chips: buttons.length > 0
51
+ ? buttons.map((btn, i) => (_jsx(Chip, { button: btn, className: isKeyChip(btn.variant)
52
+ ? `${POSTER_CLASS.chip} ${POSTER_CLASS.chipKey}`
53
+ : POSTER_CLASS.chip }, `${btn.href}-${i}`)))
54
+ : undefined }));
55
+ }
56
+ return (_jsx(PlainCard, { eyebrow: card.eyebrow, title: card.title, text: card.body, chips: buttons.length > 0
57
+ ? buttons.map((btn, i) => (_jsx(Chip, { button: btn, className: isKeyChip(btn.variant)
58
+ ? `${PLAIN_CLASS.chip} ${PLAIN_CLASS.chipKey}`
59
+ : PLAIN_CLASS.chip }, `${btn.href}-${i}`)))
60
+ : undefined, footer: card.formEmbed ? (_jsx(Iframe, { src: card.formEmbed, title: "Embedded form", sandbox: "allow-forms allow-scripts" })) : undefined }));
49
61
  }
50
62
  // --- Section Header ----------------------------------------------------------
51
63
  function SectionHeader({ title }) {
52
64
  const themed = { textPrimary: THEMED_TEXT_PRIMARY };
53
65
  if (!title)
54
66
  return null;
55
- return (_jsx(Div, { className: "mb-6", children: _jsx(Heading, { level: 2, className: themed.textPrimary, children: title }) }));
67
+ return (_jsx(Div, { className: "mb-6 text-left", children: _jsx(Heading, { level: 2, className: themed.textPrimary, size: "2xl", mdSize: "3xl", weight: "bold", children: title }) }));
56
68
  }
57
69
  export function CustomCardsSection(config) {
58
- const { title, layout, columns = 3, cards, autoScroll, scrollIntervalMs, loop = true } = config;
70
+ const { title, layout, columns = 3, cards, autoScroll, scrollIntervalMs, loop = true, } = config;
59
71
  if (!cards?.length)
60
72
  return null;
61
73
  // autoScroll: wrap all cards in SectionCarousel (client carousel)
@@ -66,16 +78,21 @@ export function CustomCardsSection(config) {
66
78
  if (layout === "row") {
67
79
  return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(HorizontalScroller, { gap: 16, showArrows: true, snapToItems: true, loop: loop, items: cards, keyExtractor: (card) => card.id, renderItem: (card) => (_jsx(Div, { className: "w-72 flex-shrink-0", children: _jsx(CardItem, { card: card }) })) })] }) }));
68
80
  }
69
- // Masonry layout: CSS columns
81
+ /*
82
+ * Masonry. `columns-*` on a container whose children are poster cards would
83
+ * fragment them across column boundaries, so the cards still carry
84
+ * `break-inside-avoid` — unchanged behaviour, just the new card shapes.
85
+ */
70
86
  if (layout === "masonry") {
71
- const colsClass = COLS_CLASS[columns] ?? COLS_CLASS[3];
72
- const masonryClass = colsClass
73
- .split(" ")
74
- .map((c) => c.replace("grid-cols-", "columns-"))
75
- .join(" ");
76
- return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(Stack, { className: `${masonryClass}`, gap: "md", children: cards.map((card) => (_jsx(Div, { className: "break-inside-avoid", children: _jsx(CardItem, { card: card }) }, card.id))) })] }) }));
87
+ const masonryClass = columns === 4
88
+ ? "columns-1 sm:columns-2 lg:columns-4"
89
+ : columns === 2
90
+ ? "columns-1 sm:columns-2"
91
+ : columns === 1
92
+ ? "columns-1"
93
+ : "columns-1 sm:columns-2 lg:columns-3";
94
+ return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(Div, { gap: "4", className: masonryClass, children: cards.map((card) => (_jsx(Div, { className: "break-inside-avoid mb-4", children: _jsx(CardItem, { card: card }) }, card.id))) })] }) }));
77
95
  }
78
96
  // Grid layout (default)
79
- const colsClass = COLS_CLASS[columns] ?? COLS_CLASS[3];
80
- return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(Div, { layout: "grid", gap: "4", className: `${colsClass}`, children: cards.map((card) => (_jsx(CardItem, { card: card }, card.id))) })] }) }));
97
+ return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(PosterGrid, { columns: columns, children: cards.map((card) => (_jsx(CardItem, { card: card }, card.id))) })] }) }));
81
98
  }
@@ -4,26 +4,39 @@ import { useState } from "react";
4
4
  import Image from "next/image.js";
5
5
  import Link from "next/link.js";
6
6
  import { THEMED_TEXT_PRIMARY } from "../../../_internal/shared/styles/themed.js";
7
- import { Button, Div, Heading, HorizontalScroller, Row, Section, Span, Stack, Text } from "../../../ui/index.js";
7
+ import { Button, Div, Heading, HorizontalScroller, POSTER_CLASS, Row, Section, Span, Text, } from "../../../ui/index.js";
8
8
  import { ROUTES } from "../../../next/routing/route-map.js";
9
9
  import { resolveMediaUrl } from "../../../utils/media-url.js";
10
10
  import { useTopCategories } from "../hooks/useTopCategories.js";
11
11
  import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view.js";
12
- const __P = {
13
- p3: "p-[var(--appkit-space-3)]",
14
- };
15
- const __O = {
16
- hidden: "overflow-hidden",
17
- };
18
12
  function isImageUrl(s) {
19
13
  return s.startsWith("http") || s.startsWith("/") || s.startsWith("data:");
20
14
  }
15
+ /**
16
+ * One category tile.
17
+ *
18
+ * 🛑 Image-LED, not image-on-top-of-a-body. The previous shape was a 16:9 crop
19
+ * stacked over a white block holding an icon square, the name, a count and a
20
+ * "Browse category →" line — four separate type sizes and a redundant CTA on a
21
+ * tile that is already entirely a link. It also letterboxed the artwork: every
22
+ * generation cover is portrait key art, so `aspect-video` cropped it to a
23
+ * horizontal band through the middle.
24
+ *
25
+ * Now it is the same `PosterCard` the systems matrix uses, so the two bands on
26
+ * the homepage read as one system rather than two designs.
27
+ */
21
28
  function CategoryChip({ category }) {
22
29
  const iconSrc = category.display?.icon;
23
30
  const coverImage = category.display?.coverImage;
24
- const initial = category.name[0]?.toUpperCase() ?? "?";
31
+ const resolvedCover = coverImage && isImageUrl(coverImage) ? resolveMediaUrl(coverImage) : undefined;
25
32
  const productCount = category.metrics?.totalProductCount ?? category.metrics?.productCount ?? 0;
26
- return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug), className: "group flex w-full min-h-[180px] sm:min-h-[220px] flex-col overflow-hidden rounded-xl border bg-[var(--appkit-color-surface)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] dark:hover:border-primary-600", children: [coverImage && isImageUrl(coverImage) ? (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, surface: "subtle", children: _jsx(Image, { src: resolveMediaUrl(coverImage), alt: category.name, width: 320, height: 180, className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Div, { surface: "muted", className: "aspect-video w-full" })), _jsxs(Stack, { className: `flex-1 ${__P.p3} text-left`, children: [_jsx(Row, { textWeight: "bold", textSize: "sm", className: "mb-2 h-9 w-9 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: iconSrc && isImageUrl(iconSrc) ? (_jsx(Image, { src: resolveMediaUrl(iconSrc), alt: "", width: 24, height: 24, className: "h-6 w-6 rounded object-cover", "aria-hidden": true })) : iconSrc ? (_jsx(Span, { size: "lg", "aria-hidden": "true", className: "leading-none", children: iconSrc })) : (initial) }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", truncate: 2, children: category.name }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: [productCount.toLocaleString(), " items"] }), _jsx(Text, { className: "mt-auto text-primary dark:text-primary-400 pt-[0.75rem]", size: "xs", weight: "medium", children: "Browse category \u2192" })] })] }));
33
+ /*
34
+ * `display.icon` holds an EMOJI in the seed and a URL in other writers —
35
+ * the ambiguity `resolveTaxonomyVisual` exists for. Only the emoji form is
36
+ * used as an eyebrow glyph here; a URL would print as raw text.
37
+ */
38
+ const emoji = iconSrc && !isImageUrl(iconSrc) ? iconSrc : undefined;
39
+ return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug), className: `${POSTER_CLASS.root} ${POSTER_CLASS.compact} w-full`, children: [resolvedCover ? (_jsx(Div, { className: POSTER_CLASS.media, children: _jsx(Image, { src: resolvedCover, alt: "", fill: true, sizes: "(min-width: 1024px) 20rem, (min-width: 640px) 45vw, 80vw", className: "h-full w-full object-cover" }) })) : null, _jsx(Div, { className: POSTER_CLASS.scrim, "aria-hidden": "true" }), _jsxs(Div, { className: POSTER_CLASS.body, children: [emoji && (_jsx(Span, { className: POSTER_CLASS.eyebrow, "aria-hidden": "true", children: emoji })), _jsx(Span, { className: POSTER_CLASS.title, children: category.name }), _jsxs(Span, { className: POSTER_CLASS.meta, children: [productCount.toLocaleString(), " items"] })] })] }));
27
40
  }
28
41
  const CTA_CLASSES = {
29
42
  filled: "rounded-lg bg-[var(--appkit-color-primary)] px-[var(--appkit-space-5)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-semibold text-white hover:opacity-90",
@@ -69,5 +82,5 @@ export function ShopByCategorySection({ title = "Shop by Category", subtitle, li
69
82
  if (!cat)
70
83
  return null;
71
84
  return (_jsx(FilterChip, { label: cat.name, active: activeFilter === id, onClick: () => setActiveFilter(activeFilter === id ? "all" : id) }, id));
72
- })] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", className: `${__O.hidden} px-[0.25rem]`, children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: "flex-none h-[104px] w-[108px] animate-pulse", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: CAROUSEL_PER_VIEW.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: autoScroll, autoScrollInterval: scrollInterval, showArrows: true, snapToItems: true, showScrollbar: false, loop: loop, pauseOnHover: true })), cta && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) })), !cta && viewMoreHref && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: viewMoreHref, className: "inline-flex items-center gap-[0.375rem] rounded-[var(--appkit-radius-md)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-semibold transition-colors bg-primary text-white hover:bg-primary-600", children: viewMoreLabel }) }))] }) }));
85
+ })] })), isLoading ? (_jsx(Div, { layout: "flex", gap: "3", overflow: "hidden", className: "px-[0.25rem]", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: "flex-none h-[104px] w-[108px] animate-pulse", surface: "subtle", rounded: "xl" }, i))) })) : (_jsx(HorizontalScroller, { items: categories, renderItem: (cat) => _jsx(CategoryChip, { category: cat }), perView: CAROUSEL_PER_VIEW.standard, gap: 16, keyExtractor: (cat) => cat.id, autoScroll: autoScroll, autoScrollInterval: scrollInterval, showArrows: true, snapToItems: true, showScrollbar: false, loop: loop, pauseOnHover: true })), cta && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: cta.href, className: CTA_CLASSES[cta.variant], children: cta.label }) })), !cta && viewMoreHref && !isLoading && (_jsx(Div, { className: "mt-6 text-left", children: _jsx(Link, { href: viewMoreHref, className: "inline-flex items-center gap-[0.375rem] rounded-[var(--appkit-radius-md)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-sm)] font-semibold transition-colors bg-primary text-white hover:bg-primary-600", children: viewMoreLabel }) }))] }) }));
73
86
  }
@@ -215,15 +215,15 @@ export declare const carouselCardSchema: z.ZodObject<{
215
215
  description?: string | undefined;
216
216
  textColor?: string | undefined;
217
217
  textAlign?: "center" | "left" | "right" | undefined;
218
- subtitle?: string | undefined;
219
218
  eyebrow?: string | undefined;
219
+ subtitle?: string | undefined;
220
220
  }, {
221
221
  title?: string | undefined;
222
222
  description?: string | undefined;
223
223
  textColor?: string | undefined;
224
224
  textAlign?: "center" | "left" | "right" | undefined;
225
- subtitle?: string | undefined;
226
225
  eyebrow?: string | undefined;
226
+ subtitle?: string | undefined;
227
227
  }>>;
228
228
  buttons: z.ZodOptional<z.ZodArray<z.ZodObject<{
229
229
  id: z.ZodOptional<z.ZodString>;
@@ -282,8 +282,8 @@ export declare const carouselCardSchema: z.ZodObject<{
282
282
  description?: string | undefined;
283
283
  textColor?: string | undefined;
284
284
  textAlign?: "center" | "left" | "right" | undefined;
285
- subtitle?: string | undefined;
286
285
  eyebrow?: string | undefined;
286
+ subtitle?: string | undefined;
287
287
  } | undefined;
288
288
  hover?: {
289
289
  effect: "none" | "color" | "scale" | "glow";
@@ -323,8 +323,8 @@ export declare const carouselCardSchema: z.ZodObject<{
323
323
  description?: string | undefined;
324
324
  textColor?: string | undefined;
325
325
  textAlign?: "center" | "left" | "right" | undefined;
326
- subtitle?: string | undefined;
327
326
  eyebrow?: string | undefined;
327
+ subtitle?: string | undefined;
328
328
  } | undefined;
329
329
  hover?: {
330
330
  effect: "none" | "color" | "scale" | "glow";
@@ -555,15 +555,15 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
555
555
  description?: string | undefined;
556
556
  textColor?: string | undefined;
557
557
  textAlign?: "center" | "left" | "right" | undefined;
558
- subtitle?: string | undefined;
559
558
  eyebrow?: string | undefined;
559
+ subtitle?: string | undefined;
560
560
  }, {
561
561
  title?: string | undefined;
562
562
  description?: string | undefined;
563
563
  textColor?: string | undefined;
564
564
  textAlign?: "center" | "left" | "right" | undefined;
565
- subtitle?: string | undefined;
566
565
  eyebrow?: string | undefined;
566
+ subtitle?: string | undefined;
567
567
  }>>;
568
568
  buttons: z.ZodOptional<z.ZodArray<z.ZodObject<{
569
569
  id: z.ZodOptional<z.ZodString>;
@@ -622,8 +622,8 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
622
622
  description?: string | undefined;
623
623
  textColor?: string | undefined;
624
624
  textAlign?: "center" | "left" | "right" | undefined;
625
- subtitle?: string | undefined;
626
625
  eyebrow?: string | undefined;
626
+ subtitle?: string | undefined;
627
627
  } | undefined;
628
628
  hover?: {
629
629
  effect: "none" | "color" | "scale" | "glow";
@@ -663,8 +663,8 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
663
663
  description?: string | undefined;
664
664
  textColor?: string | undefined;
665
665
  textAlign?: "center" | "left" | "right" | undefined;
666
- subtitle?: string | undefined;
667
666
  eyebrow?: string | undefined;
667
+ subtitle?: string | undefined;
668
668
  } | undefined;
669
669
  hover?: {
670
670
  effect: "none" | "color" | "scale" | "glow";
@@ -802,8 +802,8 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
802
802
  description?: string | undefined;
803
803
  textColor?: string | undefined;
804
804
  textAlign?: "center" | "left" | "right" | undefined;
805
- subtitle?: string | undefined;
806
805
  eyebrow?: string | undefined;
806
+ subtitle?: string | undefined;
807
807
  } | undefined;
808
808
  hover?: {
809
809
  effect: "none" | "color" | "scale" | "glow";
@@ -911,8 +911,8 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
911
911
  description?: string | undefined;
912
912
  textColor?: string | undefined;
913
913
  textAlign?: "center" | "left" | "right" | undefined;
914
- subtitle?: string | undefined;
915
914
  eyebrow?: string | undefined;
915
+ subtitle?: string | undefined;
916
916
  } | undefined;
917
917
  hover?: {
918
918
  effect: "none" | "color" | "scale" | "glow";
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Section, Container, Stack, Row, Div, Heading, Text, Span, TextLink, Badge, } from "../../../ui/index.js";
2
+ import { Section, Container, Stack, Row, Div, Heading, Text, Span, TextLink, Badge, Anchor, } from "../../../ui/index.js";
3
3
  import { SpecTable } from "../../../ui/components/SpecTable.js";
4
+ import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
5
+ import { ROUTES } from "../../../next/routing/route-map.js";
4
6
  import { PART_SLOT_LABEL, } from "../schemas/firestore.js";
5
7
  /**
6
8
  * The buyer-facing label for each WBO tournament format, in the order a
@@ -100,12 +102,12 @@ export function PartDetailPageView({ part, usedBy, listings, listingsTruncated,
100
102
  : [];
101
103
  /* Only the formats this part is actually ruled on — see the note below. */
102
104
  const legalFormats = Object.entries(part.wboLegality ?? {}).filter(([, v]) => typeof v === "boolean");
103
- return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: "muted", children: _jsx(TextLink, { href: hrefForClassification(part.slot), children: classification }) }), _jsx(Heading, { level: 1, children: part.name }), _jsxs(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: [part.code && _jsx(Badge, { variant: "info", children: part.code }), typeof part.weightG === "number" && (_jsxs(Badge, { variant: "default", children: [part.weightG, " g"] }))] }), aliases.length > 0 && (_jsxs(Text, { size: "sm", color: "muted", children: ["Also written: ", aliases.join(" · ")] }))] }), specRows.length > 1 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Specifications" }), _jsx(SpecTable, { rows: specRows })] })), ratingRows.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Ratings" }), _jsx(SpecTable, { rows: ratingRows.map((r) => ({
105
+ return (_jsx(Section, { padding: "y-lg", children: _jsx(Container, { size: "lg", children: _jsxs(Stack, { gap: "lg", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: "muted", children: _jsx(TextLink, { href: hrefForClassification(part.slot), children: classification }) }), _jsx(Heading, { level: 1, children: part.name }), _jsx(RecordOwnerQuickActions, { resource: "PART", recordId: part.id, placement: "detail", recordLabel: part.name }), _jsxs(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: [part.code && _jsx(Badge, { variant: "info", children: part.code }), typeof part.weightG === "number" && (_jsxs(Badge, { variant: "default", children: [part.weightG, " g"] }))] }), aliases.length > 0 && (_jsxs(Text, { size: "sm", color: "muted", children: ["Also written: ", aliases.join(" · ")] }))] }), specRows.length > 1 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Specifications" }), _jsx(SpecTable, { rows: specRows })] })), ratingRows.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Ratings" }), _jsx(SpecTable, { rows: ratingRows.map((r) => ({
104
106
  name: r.label,
105
107
  value: `${r.value} / 100`,
106
108
  })) }), _jsxs(Text, { size: "xs", color: "faint", children: ["Rescaled to 0\u2013100 for comparison across generations", typeof part.ratingScale === "number"
107
109
  ? `; rated 0–${part.ratingScale} at source`
108
- : "", "."] })] })), legalFormats.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Tournament legality" }), _jsx(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: legalFormats.map(([format, allowed]) => (_jsxs(Badge, { variant: allowed ? "success" : "danger", children: [WBO_FORMAT_LABEL[format] ?? format, allowed ? " ✓" : " ✗"] }, format))) }), _jsx(Text, { size: "xs", color: "faint", children: "World Beyblade Organization formats this part is ruled on. A format not listed has no ruling for it." })] })), _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: usedBy.length > 0
110
+ : "", "."] })] })), legalFormats.length > 0 && (_jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: "Tournament legality" }), _jsx(Row, { gap: "xs", wrap: true, align: "center", className: "min-w-0", children: legalFormats.map(([format, allowed]) => (_jsxs(Badge, { variant: allowed ? "success" : "danger", children: [WBO_FORMAT_LABEL[format] ?? format, allowed ? " ✓" : " ✗"] }, format))) }), _jsx(Text, { size: "xs", color: "faint", children: "World Beyblade Organization formats this part is ruled on. A format not listed has no ruling for it." })] })), part.sourceUrl && (_jsx(Div, { children: _jsxs(Text, { size: "xs", color: "faint", children: ["Facts on this page are drawn from a community wiki under CC\u00A0BY-SA.", " ", _jsx(Anchor, { href: part.sourceUrl, tone: "muted", underline: "always", children: "Source" }), " · ", _jsx(TextLink, { href: String(ROUTES.PUBLIC.CREDITS), children: "Credits" })] }) })), _jsxs(Stack, { gap: "sm", className: "min-w-0", children: [_jsx(Heading, { level: 2, size: "lg", children: usedBy.length > 0
109
111
  ? `Used by ${usedBy.length} ${usedBy.length === 1 ? "model" : "models"}`
110
112
  : "Used by" }), usedBy.length === 0 ? (_jsx(Text, { color: "muted", size: "sm", children: "No model in our database records this part yet." })) : (_jsx(Stack, { gap: "xs", className: "min-w-0", children: usedBy.map((m) => (_jsxs(Row, { gap: "sm", align: "center", padding: "inline", surface: "card", rounded: "md", border: "subtle", className: "min-w-0", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [m.categoryId ? (_jsx(TextLink, { href: hrefForModel(m.categoryId), children: m.name })) : (
111
113
  /* 49 of 259 compositions did not join to a category.