@mohasinac/appkit 4.56.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 (172) 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/checkout/actions.d.ts +31 -0
  7. package/dist/_internal/server/features/checkout/actions.js +117 -5
  8. package/dist/_internal/server/features/orders/actions.d.ts +30 -0
  9. package/dist/_internal/server/features/orders/actions.js +103 -0
  10. package/dist/_internal/server/features/orders/adapters.js +7 -0
  11. package/dist/_internal/server/features/parts/data.js +3 -2
  12. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  13. package/dist/_internal/shared/actions/action-registry.js +139 -0
  14. package/dist/_internal/shared/shipping/quote.js +5 -6
  15. package/dist/client.d.ts +23 -0
  16. package/dist/client.js +3 -0
  17. package/dist/constants/api-endpoints.d.ts +12 -0
  18. package/dist/constants/api-endpoints.js +10 -0
  19. package/dist/constants/field-names.d.ts +46 -0
  20. package/dist/constants/field-names.js +46 -0
  21. package/dist/features/about/components/PublicProfileView.js +2 -1
  22. package/dist/features/account/components/UserOrdersView.js +25 -3
  23. package/dist/features/admin/components/AdminBeysView.d.ts +27 -0
  24. package/dist/features/admin/components/AdminBeysView.js +178 -0
  25. package/dist/features/admin/components/AdminPartsView.d.ts +27 -0
  26. package/dist/features/admin/components/AdminPartsView.js +160 -0
  27. package/dist/features/admin/components/OwnerQuickActions.d.ts +19 -2
  28. package/dist/features/admin/components/OwnerQuickActions.js +83 -8
  29. package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +31 -0
  30. package/dist/features/admin/components/RecordOwnerQuickActions.js +128 -0
  31. package/dist/features/admin/components/ReferenceRecordEditor.d.ts +45 -0
  32. package/dist/features/admin/components/ReferenceRecordEditor.js +153 -0
  33. package/dist/features/admin/schemas/firestore.d.ts +1 -1
  34. package/dist/features/auctions/components/MarketplaceAuctionCard.js +15 -2
  35. package/dist/features/auth/permissions/constants.d.ts +1 -1
  36. package/dist/features/auth/permissions/constants.js +12 -1
  37. package/dist/features/beys/components/BeyDetailPageView.d.ts +50 -0
  38. package/dist/features/beys/components/BeyDetailPageView.js +98 -0
  39. package/dist/features/beys/schemas/firestore.d.ts +417 -0
  40. package/dist/features/beys/schemas/firestore.js +65 -0
  41. package/dist/features/blog/components/BlogFeaturedCard.js +15 -2
  42. package/dist/features/blog/components/BlogPostView.js +2 -1
  43. package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
  44. package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
  45. package/dist/features/categories/components/BrandDetailPageView.js +4 -2
  46. package/dist/features/categories/components/BundleDetailView.js +2 -2
  47. package/dist/features/categories/components/CategoryDetailPageView.js +36 -3
  48. package/dist/features/categories/components/CategoryGrid.js +24 -2
  49. package/dist/features/categories/components/FranchiseDetailPageView.js +2 -1
  50. package/dist/features/events/components/EventCard.js +15 -2
  51. package/dist/features/faq/components/FAQPageContent.js +17 -4
  52. package/dist/features/faq/constants/help-guides.d.ts +36 -0
  53. package/dist/features/faq/constants/help-guides.js +64 -0
  54. package/dist/features/grouped/components/GroupedListingDetailView.js +2 -3
  55. package/dist/features/homepage/components/CustomCardsSection.js +67 -50
  56. package/dist/features/homepage/components/HeroCarousel.js +3 -1
  57. package/dist/features/homepage/components/ShopByCategorySection.js +23 -10
  58. package/dist/features/homepage/lib/section-renderer.js +2 -1
  59. package/dist/features/homepage/schemas/index.d.ts +10 -10
  60. package/dist/features/orders/components/EmiInstallmentsPanel.d.ts +19 -0
  61. package/dist/features/orders/components/EmiInstallmentsPanel.js +57 -0
  62. package/dist/features/orders/components/index.d.ts +1 -0
  63. package/dist/features/orders/components/index.js +1 -0
  64. package/dist/features/orders/schemas/index.d.ts +2 -2
  65. package/dist/features/orders/types/index.d.ts +15 -0
  66. package/dist/features/parts/components/PartDetailPageView.js +5 -3
  67. package/dist/features/parts/components/PartsSearchBox.js +79 -4
  68. package/dist/features/parts/schemas/admin-edit.d.ts +81 -0
  69. package/dist/features/parts/schemas/admin-edit.js +89 -0
  70. package/dist/features/parts/schemas/firestore.d.ts +2 -1
  71. package/dist/features/points/repository/points.repository.js +5 -5
  72. package/dist/features/points/schemas/firestore.d.ts +16 -0
  73. package/dist/features/points/schemas/firestore.js +17 -0
  74. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +15 -2
  75. package/dist/features/products/components/MarketplaceBundleCard.js +15 -2
  76. package/dist/features/products/components/MarketplacePrizeDrawCard.js +15 -2
  77. package/dist/features/products/components/ProductGrid.js +18 -2
  78. package/dist/features/products/schemas/firestore.d.ts +18 -0
  79. package/dist/features/reviews/components/ReviewDetailPageView.js +3 -2
  80. package/dist/features/seller/components/CategoryInlineSelect.d.ts +10 -1
  81. package/dist/features/seller/components/CategoryInlineSelect.js +53 -5
  82. package/dist/features/seller/components/SellerProductShell.d.ts +39 -0
  83. package/dist/features/seller/components/SellerProductShell.js +31 -3
  84. package/dist/features/stores/components/StoreHeader.js +3 -1
  85. package/dist/index.d.ts +8 -0
  86. package/dist/index.js +13 -0
  87. package/dist/next/routing/route-map.d.ts +17 -1
  88. package/dist/next/routing/route-map.js +39 -0
  89. package/dist/schemas/registry.d.ts +8 -8
  90. package/dist/seed/_helpers/category-models/chunk0.d.ts +2 -0
  91. package/dist/seed/_helpers/category-models/chunk0.js +2648 -0
  92. package/dist/seed/_helpers/category-models/chunk1.d.ts +2 -0
  93. package/dist/seed/_helpers/category-models/chunk1.js +2318 -0
  94. package/dist/seed/_helpers/category-models/chunk2.d.ts +2 -0
  95. package/dist/seed/_helpers/category-models/chunk2.js +2560 -0
  96. package/dist/seed/_helpers/category-models/chunk3.d.ts +2 -0
  97. package/dist/seed/_helpers/category-models/chunk3.js +2196 -0
  98. package/dist/seed/_helpers/category-models/chunk4.d.ts +2 -0
  99. package/dist/seed/_helpers/category-models/chunk4.js +1732 -0
  100. package/dist/seed/_helpers/category-models.d.ts +9 -2
  101. package/dist/seed/_helpers/category-models.js +19 -1368
  102. package/dist/seed/_helpers/category-parts-glossary-map.js +38 -38
  103. package/dist/seed/_helpers/category-parts.js +39 -39
  104. package/dist/seed/bey-products-seed-data.d.ts +18 -0
  105. package/dist/seed/bey-products-seed-data.js +18011 -0
  106. package/dist/seed/beys/chunk0.d.ts +17 -0
  107. package/dist/seed/beys/chunk0.js +36733 -0
  108. package/dist/seed/beys/chunk1.d.ts +17 -0
  109. package/dist/seed/beys/chunk1.js +35728 -0
  110. package/dist/seed/beys/chunk2.d.ts +17 -0
  111. package/dist/seed/beys/chunk2.js +35857 -0
  112. package/dist/seed/beys/chunk3.d.ts +17 -0
  113. package/dist/seed/beys/chunk3.js +34476 -0
  114. package/dist/seed/beys-seed-data.d.ts +18 -0
  115. package/dist/seed/beys-seed-data.js +11 -0
  116. package/dist/seed/homepage-sections-seed-data.js +16 -0
  117. package/dist/seed/index.d.ts +2 -0
  118. package/dist/seed/index.js +3 -0
  119. package/dist/seed/media-assets-seed-data.js +4 -4
  120. package/dist/seed/products-catalogue/chunk0.d.ts +12 -0
  121. package/dist/seed/products-catalogue/chunk0.js +7262 -0
  122. package/dist/seed/products-catalogue/chunk1.d.ts +12 -0
  123. package/dist/seed/products-catalogue/chunk1.js +7290 -0
  124. package/dist/seed/products-catalogue/chunk10.d.ts +12 -0
  125. package/dist/seed/products-catalogue/chunk10.js +7290 -0
  126. package/dist/seed/products-catalogue/chunk11.d.ts +12 -0
  127. package/dist/seed/products-catalogue/chunk11.js +7005 -0
  128. package/dist/seed/products-catalogue/chunk12.d.ts +12 -0
  129. package/dist/seed/products-catalogue/chunk12.js +7002 -0
  130. package/dist/seed/products-catalogue/chunk13.d.ts +12 -0
  131. package/dist/seed/products-catalogue/chunk13.js +7002 -0
  132. package/dist/seed/products-catalogue/chunk14.d.ts +12 -0
  133. package/dist/seed/products-catalogue/chunk14.js +7002 -0
  134. package/dist/seed/products-catalogue/chunk15.d.ts +12 -0
  135. package/dist/seed/products-catalogue/chunk15.js +7002 -0
  136. package/dist/seed/products-catalogue/chunk16.d.ts +12 -0
  137. package/dist/seed/products-catalogue/chunk16.js +7002 -0
  138. package/dist/seed/products-catalogue/chunk17.d.ts +12 -0
  139. package/dist/seed/products-catalogue/chunk17.js +7002 -0
  140. package/dist/seed/products-catalogue/chunk18.d.ts +12 -0
  141. package/dist/seed/products-catalogue/chunk18.js +4398 -0
  142. package/dist/seed/products-catalogue/chunk2.d.ts +12 -0
  143. package/dist/seed/products-catalogue/chunk2.js +7268 -0
  144. package/dist/seed/products-catalogue/chunk3.d.ts +12 -0
  145. package/dist/seed/products-catalogue/chunk3.js +7264 -0
  146. package/dist/seed/products-catalogue/chunk4.d.ts +12 -0
  147. package/dist/seed/products-catalogue/chunk4.js +7268 -0
  148. package/dist/seed/products-catalogue/chunk5.d.ts +12 -0
  149. package/dist/seed/products-catalogue/chunk5.js +7258 -0
  150. package/dist/seed/products-catalogue/chunk6.d.ts +12 -0
  151. package/dist/seed/products-catalogue/chunk6.js +7268 -0
  152. package/dist/seed/products-catalogue/chunk7.d.ts +12 -0
  153. package/dist/seed/products-catalogue/chunk7.js +7311 -0
  154. package/dist/seed/products-catalogue/chunk8.d.ts +12 -0
  155. package/dist/seed/products-catalogue/chunk8.js +7304 -0
  156. package/dist/seed/products-catalogue/chunk9.d.ts +12 -0
  157. package/dist/seed/products-catalogue/chunk9.js +7309 -0
  158. package/dist/seed/products-catalogue-seed-data.d.ts +2 -22
  159. package/dist/seed/products-catalogue-seed-data.js +58 -86382
  160. package/dist/seed/seed-purpose.js +9 -0
  161. package/dist/server.d.ts +5 -1
  162. package/dist/server.js +19 -0
  163. package/dist/styles.css +468 -1
  164. package/dist/tailwind-utilities.css +1 -1
  165. package/dist/ui/components/PosterCard.d.ts +99 -0
  166. package/dist/ui/components/PosterCard.js +109 -0
  167. package/dist/ui/components/PosterCard.style.css +466 -0
  168. package/dist/ui/components/index.style.css +1 -0
  169. package/dist/ui/index.d.ts +2 -0
  170. package/dist/ui/index.js +2 -0
  171. package/package.json +1 -1
  172. package/scripts/seed-cli.mjs +108 -1
@@ -51,6 +51,27 @@ const ORDER_LANE_TABS = [
51
51
  { id: "auction", label: "Auction wins" },
52
52
  { id: "offer", label: "Offer wins" },
53
53
  ];
54
+ /**
55
+ * Payment-method tabs — a THIRD axis, independent of the lane tabs above and
56
+ * the lifecycle scope beside them.
57
+ *
58
+ * 🛑 EMI is not a lane and not a status, which is why it could not join
59
+ * either existing control: `orderType` records how the order was WON
60
+ * (standard / auction / offer) and the scope records where it IS. How it is
61
+ * being PAID is orthogonal to both — an auction win can be on an EMI plan.
62
+ *
63
+ * Every id is exactly a stored `OrderDocument.paymentMethod` value, and
64
+ * `paymentMethod` is `canFilter: true` in the orders SIEVE_FIELDS — checked
65
+ * before wiring this, because an unallowlisted field does not raise: the
66
+ * clause is silently dropped and the chip returns the ENTIRE order list while
67
+ * looking like a filter (Root Cause #62).
68
+ */
69
+ const PAYMENT_METHOD_TABS = [
70
+ { id: "", label: "Any" },
71
+ { id: "emi", label: "EMI plans" },
72
+ { id: "cod", label: "Cash on delivery" },
73
+ { id: "online", label: "Paid online" },
74
+ ];
54
75
  const STATUS_OPTIONS = [
55
76
  { value: "pending", label: "Pending" },
56
77
  { value: "processing", label: "Processing" },
@@ -85,7 +106,7 @@ export function UserOrdersView({ onOrderClick }) {
85
106
  * (CLAUDE.md, Root Cause #35).
86
107
  */
87
108
  const searchParams = useSearchParams();
88
- const hasNarrowingFilter = ["q", "search", "status", "orderType", "filters"].some((k) => Boolean(searchParams.get(k)));
109
+ const hasNarrowingFilter = ["q", "search", "status", "orderType", "paymentMethod", "filters"].some((k) => Boolean(searchParams.get(k)));
89
110
  const emptyMessage = hasNarrowingFilter
90
111
  ? "No orders match your search or filters."
91
112
  : "You haven't placed any orders yet.";
@@ -104,7 +125,7 @@ export function UserOrdersView({ onOrderClick }) {
104
125
  fields: ["id", "productTitle"],
105
126
  },
106
127
  emptyLabel: emptyMessage,
107
- filterKeys: ["status", "orderType"],
128
+ filterKeys: ["status", "orderType", "paymentMethod"],
108
129
  defaultSort: sortBy("createdAt", "DESC"),
109
130
  queryKey: ["user", "orders", "listing"],
110
131
  endpoint: ACCOUNT_ENDPOINTS.ORDERS,
@@ -115,6 +136,7 @@ export function UserOrdersView({ onOrderClick }) {
115
136
  buildFilters: (state) => [
116
137
  state.status ? sieveFilter("status", SIEVE_OP.EQ, state.status) : null,
117
138
  state.orderType ? sieveFilter("orderType", SIEVE_OP.EQ, state.orderType) : null,
139
+ state.paymentMethod ? sieveFilter("paymentMethod", SIEVE_OP.EQ, state.paymentMethod) : null,
118
140
  ]
119
141
  .filter(Boolean)
120
142
  .join(",") || undefined,
@@ -123,7 +145,7 @@ export function UserOrdersView({ onOrderClick }) {
123
145
  // in flight" without those two questions fighting over one control.
124
146
  buildExtraParams: () => orderScope.extraParams,
125
147
  renderAboveContent: orderScope.renderAboveContent,
126
- renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(Stack, { gap: "md", children: [_jsx(FilterChipGroup, { label: "Type", tabs: ORDER_LANE_TABS, value: pendingFilters.orderType ?? "", onChange: (id) => setPendingFilters((prev) => ({ ...prev, orderType: id })) }), _jsx(Div, { children: _jsx(PaginatedSelect, { value: pendingFilters.status || null, onChange: (v) => setPendingFilters((p) => ({ ...p, status: v ?? "" })), options: STATUS_OPTIONS, placeholder: "All statuses", ariaLabel: "Filter by order status" }) })] })),
148
+ renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(Stack, { gap: "md", children: [_jsx(FilterChipGroup, { label: "Type", tabs: ORDER_LANE_TABS, value: pendingFilters.orderType ?? "", onChange: (id) => setPendingFilters((prev) => ({ ...prev, orderType: id })) }), _jsx(FilterChipGroup, { label: "Payment", tabs: PAYMENT_METHOD_TABS, value: pendingFilters.paymentMethod ?? "", onChange: (id) => setPendingFilters((prev) => ({ ...prev, paymentMethod: id })) }), _jsx(Div, { children: _jsx(PaginatedSelect, { value: pendingFilters.status || null, onChange: (v) => setPendingFilters((p) => ({ ...p, status: v ?? "" })), options: STATUS_OPTIONS, placeholder: "All statuses", ariaLabel: "Filter by order status" }) })] })),
127
149
  renderCards: (rows, _view, _selection, isLoading) => {
128
150
  if (isLoading) {
129
151
  return (_jsx(Stack, { gap: "md", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Div, { className: "h-24 animate-pulse border border-[var(--appkit-color-border)]", rounded: "xl" }, i))) }));
@@ -0,0 +1,27 @@
1
+ /**
2
+ * AdminBeysView — browse and correct the generated Beyblade database.
3
+ *
4
+ * ── 🛑 WHY THIS IS A CORRECTION SURFACE, NOT A CRUD SCREEN ────────────────
5
+ *
6
+ * `beys` is 1,988 rows built by committed generators from a crawled corpus. So
7
+ * there is no "Add Beyblade" button: a row authored here would have no source,
8
+ * would vanish on the next regeneration, and would make the claim "this dataset
9
+ * rebuilds from `appkit/src/seed/` alone" false. Adding one is a change to the
10
+ * generator.
11
+ *
12
+ * What this surface IS for is the thing the corpus demonstrably needs — fixing
13
+ * a wrong extraction. On mode data alone the source was wrong five times in one
14
+ * session: an inverted tip transition, a mechanism attributed to the wrong
15
+ * part, a part with no modes at all, and two misexpanded abbreviations. A
16
+ * correction made here survives the next reseed, field by field
17
+ * (`adminEditedFields`).
18
+ *
19
+ * @tag domain:beys
20
+ * @tag layer:feature
21
+ * @tag access:client
22
+ * @tag consumers:app/[locale]/admin/beys/page.tsx
23
+ */
24
+ import React from "react";
25
+ import type { ListingLayoutProps } from "../../../ui";
26
+ export type AdminBeysViewProps = ListingLayoutProps;
27
+ export declare function AdminBeysView({ children, ...props }: AdminBeysViewProps): React.JSX.Element;
@@ -0,0 +1,178 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * AdminBeysView — browse and correct the generated Beyblade database.
5
+ *
6
+ * ── 🛑 WHY THIS IS A CORRECTION SURFACE, NOT A CRUD SCREEN ────────────────
7
+ *
8
+ * `beys` is 1,988 rows built by committed generators from a crawled corpus. So
9
+ * there is no "Add Beyblade" button: a row authored here would have no source,
10
+ * would vanish on the next regeneration, and would make the claim "this dataset
11
+ * rebuilds from `appkit/src/seed/` alone" false. Adding one is a change to the
12
+ * generator.
13
+ *
14
+ * What this surface IS for is the thing the corpus demonstrably needs — fixing
15
+ * a wrong extraction. On mode data alone the source was wrong five times in one
16
+ * session: an inverted tip transition, a mechanism attributed to the wrong
17
+ * part, a part with no modes at all, and two misexpanded abbreviations. A
18
+ * correction made here survives the next reseed, field by field
19
+ * (`adminEditedFields`).
20
+ *
21
+ * @tag domain:beys
22
+ * @tag layer:feature
23
+ * @tag access:client
24
+ * @tag consumers:app/[locale]/admin/beys/page.tsx
25
+ */
26
+ import React from "react";
27
+ import { Heading, ListingLayout, Stack, Text } from "../../../ui/index.js";
28
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
29
+ import { BEY_FIELDS } from "../../../constants/field-names.js";
30
+ import { sortBy } from "../../../constants/sort.js";
31
+ import { ROUTES } from "../../../next/routing/route-map.js";
32
+ import { DataListingView } from "./DataListingView.js";
33
+ /**
34
+ * 🛑 Rows are read from WHICHEVER envelope shape arrives.
35
+ *
36
+ * `apiClient.get()` unwraps `{success, data}` and returns `data` itself, so an
37
+ * endpoint that puts rows directly in `data` hands `mapRows` an ARRAY while one
38
+ * that nests them hands it an object. Reading `.data` off an array yields
39
+ * undefined — which is exactly how the categories listing rendered "No
40
+ * categories found" against a 200 carrying 58 rows. This route returns the
41
+ * object shape, and tolerating both is what keeps that from mattering.
42
+ */
43
+ function extractRows(response) {
44
+ if (Array.isArray(response))
45
+ return response;
46
+ if (!response || typeof response !== "object")
47
+ return [];
48
+ const obj = response;
49
+ if (Array.isArray(obj.data))
50
+ return obj.data;
51
+ return Array.isArray(obj.items) ? obj.items : [];
52
+ }
53
+ function extractTotal(response) {
54
+ if (Array.isArray(response) || !response || typeof response !== "object")
55
+ return undefined;
56
+ const total = response.total;
57
+ return typeof total === "number" ? total : undefined;
58
+ }
59
+ /*
60
+ * audit-create-affordance-ok: this listing is deliberately inbound-only.
61
+ * The 1,988 rows are produced by committed generators from a crawled
62
+ * corpus, so there is no POST endpoint and no create route key to point a
63
+ * button at — a Beyblade authored by hand would have no source, would vanish
64
+ * on the next regeneration, and would make "this dataset rebuilds from
65
+ * appkit/src/seed/ alone" false. Adding one is a change to the generator.
66
+ * The surface exists to CORRECT a wrong extraction and to REMOVE a row that
67
+ * should not exist, which are the two operations the data actually needs.
68
+ */
69
+ const COLUMNS = [
70
+ {
71
+ key: "primary",
72
+ header: "Beyblade",
73
+ sortable: true,
74
+ render: (row) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", children: row.secondary })] })),
75
+ },
76
+ {
77
+ key: "weight",
78
+ header: "Mass",
79
+ className: "w-28",
80
+ render: (row) => (_jsx(Text, { size: "sm", color: "muted", children: row.weight })),
81
+ },
82
+ {
83
+ key: "edited",
84
+ header: "Corrected",
85
+ className: "w-28",
86
+ /*
87
+ * 🛑 Shown as a COLUMN, because a hand-corrected row behaves differently
88
+ * from its neighbours — the generator no longer owns the fields somebody
89
+ * fixed. Leaving that invisible is how a later maintainer concludes the
90
+ * generator is producing a value it is actually being prevented from
91
+ * writing. Text, not a colour-only dot (colour is never the only signal).
92
+ */
93
+ render: (row) => row.edited ? (_jsx(Text, { size: "xs", weight: "medium", children: "By hand" })) : (_jsx(Text, { size: "xs", color: "faint", children: "Generated" })),
94
+ },
95
+ ];
96
+ const ADMIN_BEYS_CONFIG = {
97
+ portal: "admin",
98
+ title: "Beyblades",
99
+ subtitle: "The generated Beyblade database. Corrections made here are kept when the database is regenerated.",
100
+ searchPlaceholder: "Search Beyblades by name — Dranzer F, Variares…",
101
+ /*
102
+ * 🛑 `searchCommit: "enter"`, not debounce. The route's search is an indexed
103
+ * PREFIX range, so a partial word is a different query rather than a
104
+ * narrowing one — debouncing would fire a range query per pause and show
105
+ * confusing intermediate results. Enter also makes the prefix-only behaviour
106
+ * legible: you typed a prefix and asked for it.
107
+ */
108
+ searchCommit: "enter",
109
+ /*
110
+ * 🛑 DECLARED, so the placeholder can be CHECKED rather than trusted.
111
+ * The route matches exactly one field — an indexed prefix range on
112
+ * `name` — and the placeholder must not imply more. It is a prefix and
113
+ * it is case-sensitive; `mode` says so where a reader will see it.
114
+ */
115
+ search: {
116
+ placeholder: "Search Beyblades by name — a prefix, case-sensitive",
117
+ fields: ["name"],
118
+ },
119
+ emptyLabel: "No Beyblade matches that name",
120
+ filterKeys: [],
121
+ /*
122
+ * 🛑 Through `sortBy`, not a bare "name". The route currently ALWAYS
123
+ * orders by name and ignores the `sorts` param, so this is the only
124
+ * option — but writing it as a literal is what lets a field rename
125
+ * leave a sort string that silently sorts nothing.
126
+ */
127
+ defaultSort: sortBy(BEY_FIELDS.NAME, "ASC"),
128
+ pageSize: 25,
129
+ queryKey: ["admin", "beys"],
130
+ endpoint: ADMIN_ENDPOINTS.BEYS,
131
+ sortOptions: [{ value: sortBy(BEY_FIELDS.NAME, "ASC"), label: "Name A-Z" }],
132
+ columns: COLUMNS,
133
+ buildFilters: () => undefined,
134
+ getTotal: (response, rows) => extractTotal(response) ?? rows.length,
135
+ mapRows: (response) => extractRows(response).map((raw) => {
136
+ const r = raw;
137
+ /*
138
+ * 🛑 `≈` when the mass was SUMMED from the parts rather than stated by
139
+ * the source. 93% of beys have a mass but only 248 state it, and a
140
+ * derived figure shown as an exact one is the easiest way for this
141
+ * dataset to start lying — the same marking the detail page and the OG
142
+ * card use.
143
+ */
144
+ const summed = typeof r.weightGFromParts === "number" ? r.weightGFromParts : null;
145
+ const weight = typeof r.weightG === "number"
146
+ ? `${r.weightG} g`
147
+ : summed !== null
148
+ ? `≈ ${Math.round(summed * 10) / 10} g`
149
+ : "—";
150
+ return {
151
+ id: String(r.id ?? ""),
152
+ primary: String(r.name ?? r.id ?? "Unnamed Beyblade"),
153
+ /*
154
+ * System first: it is the one line a collector uses to place a bey in
155
+ * time, and it is present on 94% of them. The product code follows
156
+ * because two vendor releases of one model differ by little else.
157
+ */
158
+ secondary: [r.system, Array.isArray(r.productCodes) ? r.productCodes[0] : null]
159
+ .filter(Boolean)
160
+ .join(" · ") || "—",
161
+ weight,
162
+ edited: r.adminEdited === true,
163
+ };
164
+ }),
165
+ /*
166
+ * 🛑 `rowHrefTemplate`, not `rowHref`. Verified against the interface
167
+ * rather than guessed — there is no `rowHref` field, and an unknown key on
168
+ * an object literal typed by an interface is a compile error I could not
169
+ * have seen under this session's no-build cap.
170
+ */
171
+ rowHrefTemplate: (row) => String(ROUTES.ADMIN.BEYS_EDIT(row.id)),
172
+ };
173
+ export function AdminBeysView({ children, ...props }) {
174
+ if (React.Children.count(children) > 0) {
175
+ return (_jsxs(ListingLayout, { portal: "admin", ...props, children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Beyblades" }), children] }));
176
+ }
177
+ return (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Beyblades" }), _jsx(DataListingView, { config: ADMIN_BEYS_CONFIG })] }));
178
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * AdminPartsView — browse and correct the generated part glossary.
3
+ *
4
+ * ── 🛑 WHY THIS IS A CORRECTION SURFACE, NOT A CRUD SCREEN ────────────────
5
+ *
6
+ * `partGlossary` is 2,488 rows built by committed generators from a crawled
7
+ * corpus. So there is no "Add part" button: a row authored here would have no
8
+ * source, would vanish on the next regeneration, and would make the claim
9
+ * "this dataset rebuilds from `appkit/src/seed/` alone" false. Adding a part is
10
+ * a change to the generator.
11
+ *
12
+ * What this surface IS for is the thing the corpus demonstrably needs — fixing
13
+ * a wrong extraction. On mode data alone the source was wrong five times in one
14
+ * session: an inverted tip transition, a mechanism attributed to the wrong
15
+ * part, a part with no modes at all, and two misexpanded abbreviations. A
16
+ * correction made here survives the next reseed, field by field
17
+ * (`adminEditedFields`).
18
+ *
19
+ * @tag domain:parts
20
+ * @tag layer:feature
21
+ * @tag access:client
22
+ * @tag consumers:app/[locale]/admin/parts/page.tsx
23
+ */
24
+ import React from "react";
25
+ import type { ListingLayoutProps } from "../../../ui";
26
+ export type AdminPartsViewProps = ListingLayoutProps;
27
+ export declare function AdminPartsView({ children, ...props }: AdminPartsViewProps): React.JSX.Element;
@@ -0,0 +1,160 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * AdminPartsView — browse and correct the generated part glossary.
5
+ *
6
+ * ── 🛑 WHY THIS IS A CORRECTION SURFACE, NOT A CRUD SCREEN ────────────────
7
+ *
8
+ * `partGlossary` is 2,488 rows built by committed generators from a crawled
9
+ * corpus. So there is no "Add part" button: a row authored here would have no
10
+ * source, would vanish on the next regeneration, and would make the claim
11
+ * "this dataset rebuilds from `appkit/src/seed/` alone" false. Adding a part is
12
+ * a change to the generator.
13
+ *
14
+ * What this surface IS for is the thing the corpus demonstrably needs — fixing
15
+ * a wrong extraction. On mode data alone the source was wrong five times in one
16
+ * session: an inverted tip transition, a mechanism attributed to the wrong
17
+ * part, a part with no modes at all, and two misexpanded abbreviations. A
18
+ * correction made here survives the next reseed, field by field
19
+ * (`adminEditedFields`).
20
+ *
21
+ * @tag domain:parts
22
+ * @tag layer:feature
23
+ * @tag access:client
24
+ * @tag consumers:app/[locale]/admin/parts/page.tsx
25
+ */
26
+ import React from "react";
27
+ import { Heading, ListingLayout, Stack, Text } from "../../../ui/index.js";
28
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
29
+ import { PART_GLOSSARY_FIELDS } from "../../../constants/field-names.js";
30
+ import { sortBy } from "../../../constants/sort.js";
31
+ import { ROUTES } from "../../../next/routing/route-map.js";
32
+ import { DataListingView } from "./DataListingView.js";
33
+ /**
34
+ * 🛑 Rows are read from WHICHEVER envelope shape arrives.
35
+ *
36
+ * `apiClient.get()` unwraps `{success, data}` and returns `data` itself, so an
37
+ * endpoint that puts rows directly in `data` hands `mapRows` an ARRAY while one
38
+ * that nests them hands it an object. Reading `.data` off an array yields
39
+ * undefined — which is exactly how the categories listing rendered "No
40
+ * categories found" against a 200 carrying 58 rows. This route returns the
41
+ * object shape, and tolerating both is what keeps that from mattering.
42
+ */
43
+ function extractRows(response) {
44
+ if (Array.isArray(response))
45
+ return response;
46
+ if (!response || typeof response !== "object")
47
+ return [];
48
+ const obj = response;
49
+ if (Array.isArray(obj.data))
50
+ return obj.data;
51
+ return Array.isArray(obj.items) ? obj.items : [];
52
+ }
53
+ function extractTotal(response) {
54
+ if (Array.isArray(response) || !response || typeof response !== "object")
55
+ return undefined;
56
+ const total = response.total;
57
+ return typeof total === "number" ? total : undefined;
58
+ }
59
+ /*
60
+ * audit-create-affordance-ok: this listing is deliberately inbound-only.
61
+ * The 2,488 rows are produced by committed generators from a crawled
62
+ * corpus, so there is no POST endpoint and no create route key to point a
63
+ * button at — a part authored by hand would have no source, would vanish
64
+ * on the next regeneration, and would make "this dataset rebuilds from
65
+ * appkit/src/seed/ alone" false. Adding one is a change to the generator.
66
+ * The surface exists to CORRECT a wrong extraction and to REMOVE a row that
67
+ * should not exist, which are the two operations the data actually needs.
68
+ */
69
+ const COLUMNS = [
70
+ {
71
+ key: "primary",
72
+ header: "Part",
73
+ sortable: true,
74
+ render: (row) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", children: row.secondary })] })),
75
+ },
76
+ {
77
+ key: "weight",
78
+ header: "Weight",
79
+ className: "w-24",
80
+ render: (row) => (_jsx(Text, { size: "sm", color: "muted", children: row.weight })),
81
+ },
82
+ {
83
+ key: "edited",
84
+ header: "Corrected",
85
+ className: "w-28",
86
+ /*
87
+ * 🛑 Shown as a COLUMN, because a hand-corrected row behaves differently
88
+ * from its neighbours — the generator no longer owns the fields somebody
89
+ * fixed. Leaving that invisible is how a later maintainer concludes the
90
+ * generator is producing a value it is actually being prevented from
91
+ * writing. Text, not a colour-only dot (colour is never the only signal).
92
+ */
93
+ render: (row) => row.edited ? (_jsx(Text, { size: "xs", weight: "medium", children: "By hand" })) : (_jsx(Text, { size: "xs", color: "faint", children: "Generated" })),
94
+ },
95
+ ];
96
+ const ADMIN_PARTS_CONFIG = {
97
+ portal: "admin",
98
+ title: "Parts",
99
+ subtitle: "The generated part glossary. Corrections made here are kept when the database is regenerated.",
100
+ searchPlaceholder: "Search parts by name — Rubber Flat, Dranzer…",
101
+ /*
102
+ * 🛑 `searchCommit: "enter"`, not debounce. The route's search is an indexed
103
+ * PREFIX range, so a partial word is a different query rather than a
104
+ * narrowing one — debouncing would fire a range query per pause and show
105
+ * confusing intermediate results. Enter also makes the prefix-only behaviour
106
+ * legible: you typed a prefix and asked for it.
107
+ */
108
+ searchCommit: "enter",
109
+ /*
110
+ * 🛑 DECLARED, so the placeholder can be CHECKED rather than trusted.
111
+ * The route matches exactly one field — an indexed prefix range on
112
+ * `name` — and the placeholder must not imply more. It is a prefix and
113
+ * it is case-sensitive; `mode` says so where a reader will see it.
114
+ */
115
+ search: {
116
+ placeholder: "Search parts by name — a prefix, case-sensitive",
117
+ fields: ["name"],
118
+ },
119
+ emptyLabel: "No part matches that name",
120
+ filterKeys: [],
121
+ /*
122
+ * 🛑 Through `sortBy`, not a bare "name". The route currently ALWAYS
123
+ * orders by name and ignores the `sorts` param, so this is the only
124
+ * option — but writing it as a literal is what lets a field rename
125
+ * leave a sort string that silently sorts nothing.
126
+ */
127
+ defaultSort: sortBy(PART_GLOSSARY_FIELDS.NAME, "ASC"),
128
+ pageSize: 25,
129
+ queryKey: ["admin", "parts"],
130
+ endpoint: ADMIN_ENDPOINTS.PARTS,
131
+ sortOptions: [{ value: sortBy(PART_GLOSSARY_FIELDS.NAME, "ASC"), label: "Name A-Z" }],
132
+ columns: COLUMNS,
133
+ buildFilters: () => undefined,
134
+ getTotal: (response, rows) => extractTotal(response) ?? rows.length,
135
+ mapRows: (response) => extractRows(response).map((raw) => {
136
+ const r = raw;
137
+ const weight = typeof r.weightG === "number" ? `${r.weightG} g` : "—";
138
+ return {
139
+ id: String(r.id ?? ""),
140
+ primary: String(r.name ?? r.id ?? "Unnamed part"),
141
+ /* The classification is what disambiguates a bare code like "105". */
142
+ secondary: [r.classification, r.code].filter(Boolean).join(" · ") || "—",
143
+ weight,
144
+ edited: r.adminEdited === true,
145
+ };
146
+ }),
147
+ /*
148
+ * 🛑 `rowHrefTemplate`, not `rowHref`. Verified against the interface
149
+ * rather than guessed — there is no `rowHref` field, and an unknown key on
150
+ * an object literal typed by an interface is a compile error I could not
151
+ * have seen under this session's no-build cap.
152
+ */
153
+ rowHrefTemplate: (row) => String(ROUTES.ADMIN.PARTS_EDIT(row.id)),
154
+ };
155
+ export function AdminPartsView({ children, ...props }) {
156
+ if (React.Children.count(children) > 0) {
157
+ return (_jsxs(ListingLayout, { portal: "admin", ...props, children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Parts" }), children] }));
158
+ }
159
+ return (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Parts" }), _jsx(DataListingView, { config: ADMIN_PARTS_CONFIG })] }));
160
+ }
@@ -1,5 +1,5 @@
1
1
  /** Which registry bucket supplies the labels and the confirm copy. */
2
- export type OwnerQuickActionsResource = "PRODUCT" | "CATEGORY" | "BRAND" | "BUNDLE" | "GROUP" | "SUBLISTING" | "EVENT" | "BLOG" | "REVIEW";
2
+ export type OwnerQuickActionsResource = "PRODUCT" | "CATEGORY" | "BRAND" | "FRANCHISE" | "PART" | "BEY" | "BUNDLE" | "GROUP" | "SUBLISTING" | "EVENT" | "BLOG" | "REVIEW" | "STORE" | "USER" | "CAROUSEL" | "SECTION";
3
3
  export interface OwnerQuickActionsProps {
4
4
  resource: OwnerQuickActionsResource;
5
5
  /** Where the pencil goes — that record's dashboard editor. */
@@ -24,6 +24,23 @@ export interface OwnerQuickActionsProps {
24
24
  * `detail` sits in a header row beside the title.
25
25
  */
26
26
  placement?: "card" | "detail";
27
+ /**
28
+ * 🛑 Set this whenever `placement="detail"` sits on a DARK cover hero.
29
+ *
30
+ * `detail` otherwise renders the `ghost` variant, which is
31
+ * `color: var(--appkit-color-zinc-600)` — a dark grey that is correct on a
32
+ * page-surface header row and invisible on a cover photo. Every detail view
33
+ * that has a hero already computes a `hasCover` flag and flips each of its
34
+ * siblings to `color="inverse"`; this toolbar was the one element in that
35
+ * block that did not, so the pencil rendered near-black on near-black and
36
+ * read as "admins get no edit button" (Root Cause #67 — a theme-relative ink
37
+ * on a theme-invariant dark surface).
38
+ *
39
+ * Pass the SAME flag the rest of the hero branches on, not a literal `true`:
40
+ * a coverless hero falls back to the page surface, where the scrim's white
41
+ * ink would be invisible in the mirror-image way.
42
+ */
43
+ onMedia?: boolean;
27
44
  className?: string;
28
45
  }
29
- export declare function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement, className, }: OwnerQuickActionsProps): import("react").JSX.Element | null;
46
+ export declare function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement, onMedia, className, }: OwnerQuickActionsProps): import("react").JSX.Element | null;
@@ -54,14 +54,63 @@ import { IconButton } from "../../../ui/components/IconButton.js";
54
54
  import { Row, useToast } from "../../../ui/index.js";
55
55
  import { normalizeError } from "../../../errors/normalize.js";
56
56
  import { toUserMessage } from "../../../errors/error-display-map.js";
57
- export function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement = "card", className = "", }) {
57
+ const RESOURCE_LEAVES = {
58
+ PRODUCT: { bucket: "PRODUCT", edit: "edit", remove: "delete", duplicate: "duplicate" },
59
+ CATEGORY: { bucket: "CATEGORY", edit: "edit", remove: "delete" },
60
+ /*
61
+ * Both get a bin. Unlike the three pencil-only resources below, the blast
62
+ * radius of removing one reference row is SMALL and bounded: a bey keeps
63
+ * its parts list (`partName` sits beside `partId` for exactly this), a
64
+ * boxed set keeps its contents, and a category page keeps its listings.
65
+ * Nothing cascades, and the committed seed can put the row back.
66
+ */
67
+ PART: { bucket: "PART", edit: "edit", remove: "delete" },
68
+ BEY: { bucket: "BEY", edit: "edit", remove: "delete" },
69
+ BRAND: { bucket: "BRAND", edit: "edit", remove: "delete" },
70
+ BUNDLE: { bucket: "BUNDLE", edit: "edit", remove: "delete" },
71
+ GROUP: { bucket: "GROUP", edit: "edit", remove: "delete" },
72
+ SUBLISTING: { bucket: "SUBLISTING", edit: "edit", remove: "delete" },
73
+ EVENT: { bucket: "EVENT", edit: "edit", remove: "delete", duplicate: "duplicate" },
74
+ REVIEW: { bucket: "REVIEW", edit: "edit", remove: "delete" },
75
+ /** 🛑 `edit-post` / `delete-post`, not `edit` / `delete`. See the note above. */
76
+ BLOG: { bucket: "BLOG", edit: "edit-post", remove: "delete-post", duplicate: "duplicate-post" },
77
+ /**
78
+ * A franchise row IS a category (`categoryType: "franchise"`), so it reuses
79
+ * the category leaves rather than duplicating copy that would drift.
80
+ */
81
+ FRANCHISE: { bucket: "CATEGORY", edit: "edit", remove: "delete" },
82
+ /** Carousel slides have no bucket of their own; the leaves live under ADMIN. */
83
+ CAROUSEL: { bucket: "ADMIN", edit: "edit-carousel", remove: "delete-carousel" },
84
+ /*
85
+ * ── The three PENCIL-ONLY resources ─────────────────────────────────────
86
+ *
87
+ * `remove` is omitted on purpose for all three. Each is a delete whose blast
88
+ * radius is far larger than the page the bin would sit on, and a public
89
+ * surface is the worst possible place to offer it:
90
+ *
91
+ * STORE — deleting a store orphans its products, orders and payouts. The
92
+ * dashboard offers `suspend-store` / `verify-store`, which are the
93
+ * operations an admin actually wants from a store page.
94
+ * USER — deleting a user cascades across orders, reviews, bids and
95
+ * addresses. `ADMIN.ban-user` is the reversible tool, and it has
96
+ * its own flow with a reason.
97
+ * SECTION — homepage section `order` is POSITIONAL, so removing one silently
98
+ * renumbers its neighbours. There is also no per-section public
99
+ * page; the pencil goes to the list that owns them.
100
+ */
101
+ STORE: { bucket: "ADMIN", edit: "manage-store" },
102
+ USER: { bucket: "ADMIN", edit: "manage-user" },
103
+ SECTION: { bucket: "ADMIN", edit: "edit-section" },
104
+ };
105
+ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement = "card", onMedia = false, className = "", }) {
58
106
  const session = useOptionalSession();
59
107
  const isOwnerOrAdmin = useIsOwner(ownerId);
60
108
  const isAdmin = useIsAdmin();
61
109
  const { showToast } = useToast();
62
110
  const [confirmOpen, setConfirmOpen] = useState(false);
63
111
  const [deleting, setDeleting] = useState(false);
64
- const bucket = ACTIONS[resource];
112
+ const leaves = RESOURCE_LEAVES[resource];
113
+ const bucket = ACTIONS[leaves.bucket];
65
114
  const handleConfirmedDelete = useCallback(async () => {
66
115
  if (!onDelete)
67
116
  return;
@@ -113,10 +162,36 @@ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, owner
113
162
  */
114
163
  if (!isAdmin && !isOwnerOrAdmin && !ownsStore)
115
164
  return null;
116
- const editDef = bucket.edit;
117
- const deleteDef = bucket.delete;
165
+ const editDef = bucket[leaves.edit];
166
+ const deleteDef = leaves.remove ? bucket[leaves.remove] : undefined;
118
167
  const confirmation = deleteDef?.confirmation;
119
- const showClone = Boolean(onClone) && isAdmin;
168
+ const showClone = Boolean(onClone) && isAdmin && Boolean(leaves.duplicate);
169
+ /*
170
+ * 🛑 `leaves.remove` gates the bin, not just `onDelete`. A resource whose
171
+ * mapping omits a delete leaf (store, user, homepage section) must render no
172
+ * bin even if a caller passes `onDelete` — the safety decision belongs to the
173
+ * resource, not to each of its call sites, or the next call site re-opens it.
174
+ */
175
+ const showDelete = Boolean(onDelete) && Boolean(leaves.remove);
176
+ /*
177
+ * 🛑 The scrim is for ANY dark backdrop, not only a card.
178
+ *
179
+ * `detail` used to hard-map to `ghost` (`color: var(--appkit-color-zinc-600)`),
180
+ * which is right on a page-surface header row and invisible on a cover photo
181
+ * — and every detail view with a hero puts this toolbar inside that hero,
182
+ * beside siblings it has already flipped to `color="inverse"`. The pencil
183
+ * therefore rendered near-black on near-black and read as "admins get no
184
+ * edit button at all". See `onMedia` above and Root Cause #67.
185
+ */
186
+ const onDark = placement === "card" || onMedia;
187
+ const neutralVariant = onDark ? "scrim" : "ghost";
188
+ /*
189
+ * Danger ink is dropped on a dark backdrop for the same reason: `danger`
190
+ * resolves to a theme-relative red that disappears against the hero. The bin
191
+ * keeps its confirm dialog, which is where the destructive intent is
192
+ * actually communicated — colour was never the safeguard.
193
+ */
194
+ const dangerVariant = onDark ? "scrim" : "danger";
120
195
  return (_jsxs(_Fragment, { children: [_jsxs(Row, { gap: "xs", align: "center", className: className,
121
196
  /*
122
197
  * Stops the click reaching an ancestor `<Link>` or the card's own
@@ -125,7 +200,7 @@ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, owner
125
200
  */
126
201
  onClick: (e) => {
127
202
  e.stopPropagation();
128
- }, children: [_jsx(IconButton, { "aria-label": editDef?.ariaLabel ?? editDef?.label ?? "Edit", size: "touch", variant: placement === "card" ? "scrim" : "ghost", icon: _jsx(Icon, { name: "edit" }), onClick: () => {
203
+ }, children: [_jsx(IconButton, { "aria-label": editDef?.ariaLabel ?? editDef?.label ?? "Edit", size: "touch", variant: neutralVariant, icon: _jsx(Icon, { name: "edit" }), onClick: () => {
129
204
  /*
130
205
  * `window.location`, not `next/link`. This component is rendered
131
206
  * inside cards whose whole body is already an anchor, and nesting
@@ -135,8 +210,8 @@ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, owner
135
210
  */
136
211
  if (typeof window !== "undefined")
137
212
  window.location.assign(editHref);
138
- } }), showClone && (_jsx(IconButton, { "aria-label": (bucket.duplicate?.ariaLabel ?? bucket.duplicate?.label) ||
139
- "Duplicate", size: "touch", variant: placement === "card" ? "scrim" : "ghost", icon: _jsx(Icon, { name: "duplicate" }), onClick: () => void onClone?.() })), onDelete && (_jsx(IconButton, { "aria-label": deleteDef?.ariaLabel ?? deleteDef?.label ?? "Delete", size: "touch", variant: placement === "card" ? "scrim" : "danger", icon: _jsx(Icon, { name: "delete" }), onClick: () => setConfirmOpen(true) }))] }), onDelete && (_jsx(ConfirmDeleteModal, { isOpen: confirmOpen, onClose: () => setConfirmOpen(false), onConfirm: handleConfirmedDelete, isDeleting: deleting, title: confirmation?.title ?? "Delete this record?", message: recordLabel && confirmation
213
+ } }), showClone && (_jsx(IconButton, { "aria-label": (leaves.duplicate ? bucket[leaves.duplicate]?.ariaLabel ?? bucket[leaves.duplicate]?.label : undefined) ||
214
+ "Duplicate", size: "touch", variant: neutralVariant, icon: _jsx(Icon, { name: "duplicate" }), onClick: () => void onClone?.() })), showDelete && (_jsx(IconButton, { "aria-label": deleteDef?.ariaLabel ?? deleteDef?.label ?? "Delete", size: "touch", variant: dangerVariant, icon: _jsx(Icon, { name: "delete" }), onClick: () => setConfirmOpen(true) }))] }), showDelete && (_jsx(ConfirmDeleteModal, { isOpen: confirmOpen, onClose: () => setConfirmOpen(false), onConfirm: handleConfirmedDelete, isDeleting: deleting, title: confirmation?.title ?? "Delete this record?", message: recordLabel && confirmation
140
215
  ? `${confirmation.body}\n\nDeleting: ${recordLabel}`
141
216
  : confirmation?.body, confirmText: confirmation?.confirmLabel ?? deleteDef?.label ?? "Delete", variant: "danger" }))] }));
142
217
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The admin-owned resources this wrapper can delete.
3
+ *
4
+ * 🛑 Deliberately NOT every `OwnerQuickActionsResource`. `STORE`, `USER` and
5
+ * `SECTION` omit a `remove` leaf in `RESOURCE_LEAVES` because their blast
6
+ * radius is far wider than the page a bin would sit on, and `REVIEW` has no
7
+ * admin edit route to send the pencil to. Adding one here without adding its
8
+ * leaf there still renders no bin — the resource owns that decision, which is
9
+ * the point of gating it in two places.
10
+ */
11
+ export type RecordQuickActionResource = "CATEGORY" | "BRAND" | "BUNDLE" | "GROUP" | "EVENT" | "BLOG" | "FRANCHISE" | "SUBLISTING" | "PART" | "BEY";
12
+ export interface RecordOwnerQuickActionsProps {
13
+ resource: RecordQuickActionResource;
14
+ /** The record's document id — what the endpoint and the editor are keyed on. */
15
+ recordId: string;
16
+ /** Names the record in the delete confirmation. */
17
+ recordLabel?: string;
18
+ /**
19
+ * Forwarded to `OwnerQuickActions` so a store-owned record (a grouped
20
+ * listing, a store-authored bundle) stays visible to the seller who owns it
21
+ * and not only to an admin. `SessionUser.storeId` holds the store SLUG, not
22
+ * the Auth UID — two different id spaces, hence two props.
23
+ */
24
+ ownerId?: string;
25
+ storeId?: string;
26
+ placement?: "card" | "detail";
27
+ /** Pass the hero's own `hasCover` flag. See `OwnerQuickActions.onMedia`. */
28
+ onMedia?: boolean;
29
+ className?: string;
30
+ }
31
+ export declare function RecordOwnerQuickActions({ resource, recordId, recordLabel, ownerId, storeId, placement, onMedia, className, }: RecordOwnerQuickActionsProps): import("react").JSX.Element;