@mohasinac/appkit 4.53.2 → 4.54.0-next.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 (179) hide show
  1. package/dist/.build-stamp.json +5 -5
  2. package/dist/_internal/server/features/auth/capabilities.js +7 -5
  3. package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
  4. package/dist/_internal/server/features/products/listing-tab-counts.js +81 -25
  5. package/dist/_internal/server/features/search/index-build/sources.js +43 -2
  6. package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
  7. package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
  8. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  9. package/dist/_internal/shared/actions/action-registry.js +128 -0
  10. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
  11. package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
  12. package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
  13. package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
  14. package/dist/_internal/shared/listing-types/tiers.js +150 -0
  15. package/dist/cli/index.js +0 -2
  16. package/dist/client.d.ts +21 -2
  17. package/dist/client.js +23 -4
  18. package/dist/constants/api-endpoints.d.ts +21 -0
  19. package/dist/constants/api-endpoints.js +7 -0
  20. package/dist/features/account/components/UserOrdersView.js +26 -7
  21. package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
  22. package/dist/features/addresses/utils/postal-autofill.js +54 -0
  23. package/dist/features/admin/components/AdminStoreEditorView.js +73 -34
  24. package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
  25. package/dist/features/admin/components/OwnerQuickActions.js +142 -0
  26. package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
  27. package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
  28. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
  29. package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
  30. package/dist/features/auth/permissions/constants.d.ts +29 -2
  31. package/dist/features/auth/permissions/constants.js +68 -1
  32. package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
  33. package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
  34. package/dist/features/cart/components/CartDrawer.js +3 -2
  35. package/dist/features/cart/components/CartGroupLineRow.js +2 -1
  36. package/dist/features/cart/hooks/useAddToCart.js +10 -5
  37. package/dist/features/cart/utils/pending-ops.d.ts +11 -1
  38. package/dist/features/cart/utils/pending-ops.js +14 -2
  39. package/dist/features/categories/components/BrandDetailPageView.js +2 -1
  40. package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
  41. package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
  42. package/dist/features/categories/components/CategoryGrid.js +9 -2
  43. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
  44. package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
  45. package/dist/features/events/components/EventCard.d.ts +11 -1
  46. package/dist/features/events/components/EventCard.js +9 -2
  47. package/dist/features/layout/AppLayoutShell.js +2 -1
  48. package/dist/features/layout/BottomActions.d.ts +1 -0
  49. package/dist/features/layout/BottomActions.js +330 -0
  50. package/dist/features/layout/BottomChrome.d.ts +45 -0
  51. package/dist/features/layout/BottomChrome.js +5 -17
  52. package/dist/features/layout/SideActionDock.js +36 -13
  53. package/dist/features/layout/index.d.ts +2 -7
  54. package/dist/features/layout/index.js +7 -7
  55. package/dist/features/layout/useIsDesktop.d.ts +36 -0
  56. package/dist/features/layout/useIsDesktop.js +48 -0
  57. package/dist/features/media/messages/en.json +67 -67
  58. package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
  59. package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
  60. package/dist/features/orders/components/index.d.ts +2 -1
  61. package/dist/features/orders/components/index.js +1 -1
  62. package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
  63. package/dist/features/orders/utils/order-announcement-message.js +16 -0
  64. package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
  65. package/dist/features/orders/utils/payment-proof-message.js +17 -0
  66. package/dist/features/parts/schemas/firestore.d.ts +227 -0
  67. package/dist/features/parts/schemas/firestore.js +6 -6
  68. package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
  69. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
  70. package/dist/features/products/api/[id]/route.js +8 -1
  71. package/dist/features/products/api/route.js +16 -4
  72. package/dist/features/products/components/AvailabilityScopeHint.js +1 -1
  73. package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
  74. package/dist/features/products/components/InteractiveProductCard.js +16 -4
  75. package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
  76. package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
  77. package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
  78. package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
  79. package/dist/features/products/components/ProductDetailPageView.js +27 -2
  80. package/dist/features/products/components/ProductGrid.d.ts +14 -1
  81. package/dist/features/products/components/ProductGrid.js +22 -5
  82. package/dist/features/products/components/ProductsIndexListing.js +2 -2
  83. package/dist/features/reviews/api/[id]/route.js +83 -23
  84. package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
  85. package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
  86. package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
  87. package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
  88. package/dist/features/reviews/components/index.d.ts +4 -0
  89. package/dist/features/reviews/components/index.js +2 -0
  90. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  91. package/dist/features/seller/actions/seller-actions.js +49 -3
  92. package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
  93. package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
  94. package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
  95. package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
  96. package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
  97. package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
  98. package/dist/features/stores/components/index.d.ts +0 -2
  99. package/dist/features/stores/components/index.js +0 -1
  100. package/dist/features/stores/schemas/index.d.ts +1 -1
  101. package/dist/features/stores/schemas/index.js +10 -1
  102. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +403 -0
  103. package/dist/index.d.ts +8 -27
  104. package/dist/index.js +14 -35
  105. package/dist/next/routing/route-map.d.ts +2 -0
  106. package/dist/next/routing/route-map.js +9 -0
  107. package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
  108. package/dist/providers/shipping-shiprocket/index.js +353 -0
  109. package/dist/schemas/registry.d.ts +2 -2
  110. package/dist/security/csp.d.ts +16 -1
  111. package/dist/security/csp.js +5 -3
  112. package/dist/seed/_helpers/category-forest.js +27 -0
  113. package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
  114. package/dist/seed/_helpers/category-parts.d.ts +1 -1
  115. package/dist/seed/_helpers/category-parts.js +13412 -3020
  116. package/dist/seed/categories-seed-data.js +48 -6
  117. package/dist/seed/homepage-sections-seed-data.js +41 -28
  118. package/dist/seed/model-composition-seed-data.js +6 -8
  119. package/dist/seed/part-glossary/chunk0.d.ts +9 -0
  120. package/dist/seed/part-glossary/chunk0.js +35044 -0
  121. package/dist/seed/part-glossary/chunk1.d.ts +9 -0
  122. package/dist/seed/part-glossary/chunk1.js +39955 -0
  123. package/dist/seed/part-glossary/chunk2.d.ts +9 -0
  124. package/dist/seed/part-glossary/chunk2.js +50335 -0
  125. package/dist/seed/part-glossary/chunk3.d.ts +9 -0
  126. package/dist/seed/part-glossary/chunk3.js +52386 -0
  127. package/dist/seed/part-glossary/chunk4.d.ts +9 -0
  128. package/dist/seed/part-glossary/chunk4.js +50549 -0
  129. package/dist/seed/part-glossary-seed-data.d.ts +1 -1
  130. package/dist/seed/part-glossary-seed-data.js +23 -47483
  131. package/dist/server.d.ts +4 -8
  132. package/dist/server.js +8 -20
  133. package/dist/styles.css +516 -358
  134. package/dist/tailwind-utilities.css +1 -1
  135. package/dist/tokens/themes/index.d.ts +3 -1
  136. package/dist/tokens/themes/index.js +11 -1
  137. package/dist/tokens/themes/technical-dark.d.ts +30 -0
  138. package/dist/tokens/themes/technical-dark.js +103 -0
  139. package/dist/tokens/themes/technical-light.d.ts +22 -0
  140. package/dist/tokens/themes/technical-light.js +89 -0
  141. package/dist/tokens/tokens.css +155 -0
  142. package/dist/ui/components/GatedPrice.js +30 -3
  143. package/dist/ui/components/GatedPrice.style.css +22 -19
  144. package/dist/ui/components/QuickCreateModal.style.css +78 -78
  145. package/dist/ui/forms/FormShell.style.css +260 -260
  146. package/package.json +1 -1
  147. package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
  148. package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
  149. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
  150. package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
  151. package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
  152. package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
  153. package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
  154. package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
  155. package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
  156. package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
  157. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
  158. package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
  159. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
  160. package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
  161. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
  162. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
  163. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
  164. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
  165. package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
  166. package/dist/features/whatsapp-bot/components/index.js +0 -2
  167. package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
  168. package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
  169. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
  170. package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
  171. package/dist/features/whatsapp-bot/index.d.ts +0 -3
  172. package/dist/features/whatsapp-bot/index.js +0 -3
  173. package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
  174. package/dist/features/whatsapp-bot/manifest.js +0 -13
  175. package/dist/features/whatsapp-bot/messages/en.json +0 -7
  176. package/dist/features/whatsapp-bot/server.d.ts +0 -7
  177. package/dist/features/whatsapp-bot/server.js +0 -7
  178. package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
  179. package/dist/features/whatsapp-bot/types/index.js +0 -1
@@ -0,0 +1,330 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * BottomActions — the PHONE half of the page-action control.
5
+ *
6
+ * 🛑 Mobile only, unconditionally. `SideActionDock` is the desktop half, and
7
+ * `useIsDesktop` is the single line that chooses between them; both read the
8
+ * same `BottomActionsContext`, so every publisher (`useBottomActions`,
9
+ * `useFormBottomActions`, every `DataListingView` bulk bar) feeds whichever
10
+ * one is mounted without knowing which that is.
11
+ *
12
+ * Why a bar here and a dock there: a phone has a thumb zone and a bottom edge
13
+ * that already means something, and this bar stacks correctly above the tab
14
+ * bar inside `BottomChrome`. A floating puck on a phone takes content space on
15
+ * the screen with the least of it. At 1440px the argument reverses — see
16
+ * `SideActionDock`.
17
+ *
18
+ * 🛑 THE `desktop` OPTION IS GONE FROM THIS COMPONENT. It used to carry an
19
+ * `"always" | "after-scroll"` opt-in with its own `scrollY > 400` gate. That
20
+ * threshold is a guess about where a page's CTA sits — wrong on a short page
21
+ * and wrong on a long gallery — and the dock now owns desktop outright, so a
22
+ * page opting the BAR onto desktop would put two toolbars on one screen.
23
+ * `BottomActionsState.desktop` still exists for callers; this component no
24
+ * longer reads it.
25
+ *
26
+ * A fixed-bottom mobile action bar rendered **above** BottomNavbar (bottom-14).
27
+ * Reads from `BottomActionsContext` — features register their actions via the
28
+ * `useBottomActions` hook; this component just renders whatever is registered.
29
+ *
30
+ * Two modes:
31
+ * - **Page mode** — shows registered page-level actions inline (Add to Cart,
32
+ * Buy Now, Place Bid, Proceed to Checkout, etc.) with an optional info label.
33
+ * - **Bulk mode** — activates when sieveFilter("bulk.selectedCount", SIEVE_OP.GT, "0"); shows:
34
+ * • Selection count pill on the left (tap to deselect all)
35
+ * • An upward-opening type-picker dropdown (middle, flex-1) — tap to
36
+ * choose WHICH action to run; the chosen label is always visible.
37
+ * • An "Apply" submit button on the right — executes the selected action,
38
+ * styled with the selected action's variant (danger = red, etc.).
39
+ *
40
+ * Layout rules:
41
+ * - Not mounted at all on lg+ screens.
42
+ * - It reveals only once the page's own inline CTA has been SEEN and scrolled
43
+ * past (`useCtaReveal`), and hides again when that CTA comes back into view.
44
+ * A bar duplicating a button the user is already looking at is noise.
45
+ * - The bar collapses rather than translating when hidden, so the height it
46
+ * publishes through `BottomChrome` is honest at every frame.
47
+ *
48
+ * @component
49
+ * @example
50
+ * // Automatically rendered by LayoutClient — no manual usage required.
51
+ * // Features use `useBottomActions` to register their actions.
52
+ */
53
+ import { useState, useRef, useEffect, useId } from "react";
54
+ import { X, ChevronUp, ChevronDown, Check } from "lucide-react";
55
+ import { useBottomActionsContext } from "./BottomActionsContext.js";
56
+ import { useClickOutside } from "../../react/index.js";
57
+ import { useCtaReveal } from "./useCtaReveal.js";
58
+ import { useIsDesktop } from "./useIsDesktop.js";
59
+ import { ActionRow, Div, Row, Span, Text, Button } from "../../ui/index.js";
60
+ // Token values inlined from @mohasinac/appkit/tokens
61
+ const BOTTOM_NAV_BG = "bg-[color-mix(in_srgb,var(--appkit-color-bg)_90%,transparent)] backdrop-blur-md border-t border-[var(--appkit-color-border)]";
62
+ const BOTTOM_NAV_HEIGHT = "h-14";
63
+ const FLEX_CENTER = "flex items-center justify-center";
64
+ const CLS_COUNT_BADGE = "bg-error-solid text-error-on-solid";
65
+ // ─── Sub-components ───────────────────────────────────────────────────────────
66
+ function BulkPickerPanel({ bulkActions, selectedActionId, pickerOpen, onSelect, }) {
67
+ return (_jsx(Div, { role: "listbox", "aria-label": "Bulk actions", className: [
68
+ // Flips with the tier — see InfoPanel for why.
69
+ "absolute left-0 right-0 overflow-hidden",
70
+ "bottom-full lg:bottom-auto lg:top-full",
71
+ BOTTOM_NAV_BG,
72
+ "border-t lg:border-t-0 lg:border-b border-[var(--appkit-color-border)]/80",
73
+ "shadow-[0_-8px_24px_rgba(0,0,0,0.10)] dark:shadow-[0_-8px_24px_rgba(0,0,0,0.35)]",
74
+ "lg:shadow-[0_8px_24px_rgba(0,0,0,0.10)] dark:lg:shadow-[0_8px_24px_rgba(0,0,0,0.35)]",
75
+ "transition-[max-height,opacity] duration-200 ease-out",
76
+ pickerOpen ? "max-h-64 opacity-100" : "max-h-0 opacity-0 pointer-events-none",
77
+ ]
78
+ .filter(Boolean)
79
+ .join(" "), children: bulkActions.map((action, i) => {
80
+ const isSelected = action.id === selectedActionId;
81
+ return (_jsxs(Button, { role: "option", "aria-selected": isSelected, type: "button", variant: "ghost", disabled: action.disabled || action.loading, onClick: () => onSelect(action.id), className: [
82
+ "w-full flex items-center gap-[var(--appkit-space-3)] px-[var(--appkit-space-5)] py-[var(--appkit-space-3-5)] text-left text-[length:var(--appkit-text-sm)] font-medium transition-colors rounded-none",
83
+ i > 0 ? "border-t border-zinc-100/80 border-[var(--appkit-color-border-subtle)]" : "",
84
+ isSelected ? " bg-[var(--appkit-color-surface-input)]" : "",
85
+ action.variant === "danger"
86
+ ? "text-error hover:bg-error-surface"
87
+ : "text-[var(--appkit-color-text)] hover:bg-surface-hover",
88
+ action.disabled || action.loading ? "opacity-50 cursor-not-allowed" : "",
89
+ ]
90
+ .filter(Boolean)
91
+ .join(" "), children: [action.icon && (_jsx(Span, { className: `flex-shrink-0 w-5 h-5 ${FLEX_CENTER}`, "aria-hidden": "true", children: action.icon })), _jsx(Span, { className: "flex-1 truncate", children: action.label }), isSelected && (_jsx(Check, { className: "w-4 h-4 flex-shrink-0 text-primary-600 dark:text-primary-400", "aria-hidden": "true" }))] }, action.id));
92
+ }) }));
93
+ }
94
+ /**
95
+ * Expandable detail that opens UPWARD from the bar.
96
+ *
97
+ * Same recipe as BulkPickerPanel above — absolutely positioned at `bottom-full`
98
+ * so it grows into the space above the bar rather than pushing the page, and
99
+ * animated on max-height/opacity so it can't be tabbed into while closed.
100
+ * Scrolls internally, since a many-store cart's breakdown can outgrow the
101
+ * screen.
102
+ */
103
+ function InfoPanel({ id, open, children, }) {
104
+ return (_jsx(Div, { id: id, className: [
105
+ // Opens UPWARD from a bottom-edge bar, DOWNWARD from a desktop bar
106
+ // hanging under the header — a panel must always expand into the
107
+ // screen, never off it. Border and shadow flip with it, or the sheet
108
+ // is lit from the wrong side and reads as detached from its bar.
109
+ "absolute left-0 right-0 overflow-hidden",
110
+ "bottom-full lg:bottom-auto lg:top-full",
111
+ BOTTOM_NAV_BG,
112
+ "border-t lg:border-t-0 lg:border-b border-[var(--appkit-color-border)]/80",
113
+ "shadow-[0_-8px_24px_rgba(0,0,0,0.10)] dark:shadow-[0_-8px_24px_rgba(0,0,0,0.35)]",
114
+ "lg:shadow-[0_8px_24px_rgba(0,0,0,0.10)] dark:lg:shadow-[0_8px_24px_rgba(0,0,0,0.35)]",
115
+ "transition-[max-height,opacity] duration-200 ease-out",
116
+ open ? "max-h-[60vh] opacity-100" : "max-h-0 opacity-0 pointer-events-none",
117
+ ]
118
+ .filter(Boolean)
119
+ .join(" "), "aria-hidden": !open, children: _jsx(Div, { className: "max-h-[60vh]", overflow: "y-auto", padding: "md", children: children }) }));
120
+ }
121
+ /**
122
+ * How many actions fit on one line before the row splits in two.
123
+ *
124
+ * 🛑 ROW COUNT is a function of `pageActions.length` and NOTHING else — never
125
+ * of validation state, and never of measured width. `BottomChrome`'s
126
+ * ResizeObserver faithfully republishes `--bottom-chrome-height` on any height
127
+ * change, and every consumer (BackToTop, the pagination bar, the footer's
128
+ * clearance) reflows with it. A bar that grew a row when an error appeared
129
+ * would shift the whole page while the user was typing. The error count lives
130
+ * in `infoLabel`, which sits in the absolutely-positioned panel and is excluded
131
+ * from the published height by construction.
132
+ *
133
+ * The invariant is about ROW COUNT, not pixel height. `ActionRow` lets a long
134
+ * label wrap to a second line, which changes the bar's height by ~16px,
135
+ * monotonically, and the observer handles that correctly. What must never
136
+ * happen is a row count that FLIPS: that is a ~48px step and it is reversible
137
+ * mid-interaction.
138
+ *
139
+ * Which is exactly why the count split stays primary and `flex-wrap` is only an
140
+ * overflow net. In this app LABELS ARE STATE — "Add to Cart" becomes "Adding…",
141
+ * "Buy Now" becomes "Out of Stock", "Apply" becomes "Apply (3)". If width alone
142
+ * decided rows, tapping Add to Cart would shorten the label, free ~40px,
143
+ * un-wrap a row and shift the page under the user's thumb at the exact moment
144
+ * they committed to the tap.
145
+ *
146
+ * 🛑 This constant is COUPLED to `--appkit-size-action-min` (tokens.css).
147
+ * Two actions per line is safe only while `2 × action-min + gap` fits the
148
+ * narrowest supported viewport (280px). Raising either without re-checking that
149
+ * sum reintroduces device-dependent row flipping.
150
+ */
151
+ const SINGLE_ROW_MAX_ACTIONS = 2;
152
+ function ActionButtons({ actions, dispatchAction, }) {
153
+ return (_jsx(_Fragment, { children: actions.map((action) => {
154
+ const isIconOnly = !action.label;
155
+ /*
156
+ * Width is ActionRow's job now, not this component's — every action is
157
+ * sized from its own label by `.appkit-action-row__group > *`, and only
158
+ * the leftover is shared out. There is deliberately NO per-action width
159
+ * class here: the old `grow` ternary is what produced a 44px stub
160
+ * reading "Ba" next to a full-width "Continue to payment".
161
+ *
162
+ * The one thing a stylesheet cannot infer is that a child has no label,
163
+ * so icon-only opts into its square tap target explicitly.
164
+ *
165
+ * 🛑 Never add `flex-1`, `flex-shrink-0`, `basis-*`, `w-*`, `h-*`,
166
+ * `truncate` or `leading-none` below. Both tailwind configs set
167
+ * `important: true`, so any of them beats ActionRow's CSS and silently
168
+ * reverts this bar to the squeezed behaviour with no visible error.
169
+ */
170
+ return (_jsxs(Button, { type: "button", variant: action.variant ?? "primary", size: "sm", isLoading: action.loading, disabled: action.disabled, onClick: () => dispatchAction(action.id), className: [
171
+ "relative",
172
+ isIconOnly ? "appkit-action-row__icon-only justify-center" : "",
173
+ ]
174
+ .filter(Boolean)
175
+ .join(" "), children: [action.icon && (_jsx(Span, { className: ["flex-shrink-0", action.label ? "mr-1.5" : ""].join(" "), "aria-hidden": "true", children: action.icon })), action.label && _jsx(Span, { children: action.label }), action.badge !== undefined && (_jsx(Span, { className: `absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] ${CLS_COUNT_BADGE} text-[10px] ${FLEX_CENTER} px-[var(--appkit-space-1)] pointer-events-none select-none`, rounded: "full", weight: "bold", "aria-hidden": "true", children: action.badge }))] }, action.id));
176
+ }) }));
177
+ }
178
+ /**
179
+ * Lays a form's or a page's actions out across one or two lines.
180
+ *
181
+ * | n | layout |
182
+ * |------|-----------------------------------------------------------------|
183
+ * | 1 | one row, full-width primary |
184
+ * | 2 | one row, each sized from its own label |
185
+ * | 3+ | two rows — secondaries above, primary full-width below |
186
+ *
187
+ * The LAST action is treated as the primary. That holds for every producer:
188
+ * `useFormBottomActions` appends submit last, and the checkout/cart bars build
189
+ * `[back, primary]`. Putting it on its own line keeps the thumb target the same
190
+ * size no matter how many secondary actions a screen has, which is the whole
191
+ * reason a 5-action form (3 forms + soft-ban + hard-ban on the admin user
192
+ * editor) was unrepresentable before.
193
+ *
194
+ * Secondaries WRAP rather than overflowing into a menu. Wrapping keeps the
195
+ * height a pure function of the action count — the invariant above — whereas a
196
+ * popover would add a second interaction to reach an action that is already
197
+ * only one line away. No current caller exceeds five.
198
+ */
199
+ function PageActionsRow({ pageActions, dispatchAction, }) {
200
+ if (pageActions.length === 0)
201
+ return null;
202
+ if (pageActions.length <= SINGLE_ROW_MAX_ACTIONS) {
203
+ return (_jsx(ActionRow, { children: _jsx(ActionButtons, { actions: pageActions, dispatchAction: dispatchAction }) }));
204
+ }
205
+ const secondaries = pageActions.slice(0, -1);
206
+ const primary = pageActions[pageActions.length - 1];
207
+ return (_jsxs(Div, { className: "flex w-full flex-col gap-[var(--appkit-space-2)]", children: [_jsx(ActionRow, { children: _jsx(ActionButtons, { actions: secondaries, dispatchAction: dispatchAction }) }), _jsx(ActionRow, { children: _jsx(ActionButtons, { actions: [primary], dispatchAction: dispatchAction }) })] }));
208
+ }
209
+ export default function BottomActions() {
210
+ const { state, actionCallbacksRef, bulkCallbacksRef, bulkClearRef } = useBottomActionsContext();
211
+ const isDesktop = useIsDesktop();
212
+ const [pickerOpen, setPickerOpen] = useState(false);
213
+ const [infoOpen, setInfoOpen] = useState(false);
214
+ const [selectedActionId, setSelectedActionId] = useState(null);
215
+ const containerRef = useRef(null);
216
+ const infoPanelId = useId();
217
+ useClickOutside(containerRef, () => {
218
+ setPickerOpen(false);
219
+ setInfoOpen(false);
220
+ });
221
+ const { actions, bulk, infoLabel, secondaryLabel, infoPanel, infoOpenSignal, ctaAnchorId } = state;
222
+ const isBulkMode = !!(bulk && bulk.selectedCount > 0);
223
+ const hasInfoPanel = infoPanel != null && !isBulkMode;
224
+ const bulkActions = bulk?.actions ?? [];
225
+ const pageActions = actions;
226
+ const hasContent = (isBulkMode ? bulkActions.length > 0 || !!bulk : pageActions.length > 0) ||
227
+ !!infoLabel ||
228
+ !!secondaryLabel;
229
+ /*
230
+ * The same reveal rule the dock uses, for the same reason: while the page's
231
+ * own Add-to-Cart is on screen, a second copy of it welded to the bottom
232
+ * edge is a duplicate covering the last row of the page. A selection is its
233
+ * own reveal — bulk mode is a state the user just entered deliberately, and
234
+ * making them scroll to act on it would be a puzzle.
235
+ */
236
+ const revealed = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId) || isBulkMode;
237
+ const isVisible = hasContent && revealed;
238
+ // A panel left open after its content is withdrawn (bulk mode taking over, or
239
+ // the page clearing it) would sit there as an empty sheet.
240
+ useEffect(() => {
241
+ if (!hasInfoPanel)
242
+ setInfoOpen(false);
243
+ }, [hasInfoPanel]);
244
+ /*
245
+ * Force-open on a bumped signal — a failed form submit, today. Depends on
246
+ * the counter alone, so a re-render with the same value never re-opens what
247
+ * the user has since collapsed. `hasInfoPanel` is read but deliberately NOT
248
+ * a dependency: publishing the panel and bumping the signal are two separate
249
+ * setState calls, so re-running on `hasInfoPanel` would open the panel again
250
+ * one render after the user closed it.
251
+ */
252
+ const lastSignalRef = useRef(undefined);
253
+ useEffect(() => {
254
+ if (infoOpenSignal == null)
255
+ return;
256
+ if (lastSignalRef.current === infoOpenSignal)
257
+ return;
258
+ lastSignalRef.current = infoOpenSignal;
259
+ setInfoOpen(true);
260
+ }, [infoOpenSignal]);
261
+ // Keep selectedActionId in sync with available bulk actions
262
+ useEffect(() => {
263
+ if (!isBulkMode) {
264
+ setPickerOpen(false);
265
+ setSelectedActionId(null);
266
+ return;
267
+ }
268
+ setSelectedActionId((prev) => {
269
+ const ids = bulkActions.map((a) => a.id);
270
+ if (prev && ids.includes(prev))
271
+ return prev;
272
+ return ids[0] ?? null;
273
+ });
274
+ // eslint-disable-next-line react-hooks/exhaustive-deps
275
+ }, [isBulkMode, bulkActions.map((a) => a.id).join(",")]);
276
+ const selectedAction = bulkActions.find((a) => a.id === selectedActionId);
277
+ const dispatchAction = (id) => actionCallbacksRef.current.get(id)?.();
278
+ const dispatchBulkClear = () => {
279
+ bulkClearRef.current?.();
280
+ setPickerOpen(false);
281
+ };
282
+ const handleApply = () => {
283
+ if (!selectedActionId)
284
+ return;
285
+ bulkCallbacksRef.current.get(selectedActionId)?.();
286
+ setPickerOpen(false);
287
+ };
288
+ // 🛑 AFTER every hook, never before — this flips on resize, and an early
289
+ // return above a `useState` would change the hook order mid-session.
290
+ // `return null`, not `lg:hidden`: see `useIsDesktop` for why the class form
291
+ // cannot be trusted here, and because a display:none bar still mounts,
292
+ // still observes, and can still take focus.
293
+ if (isDesktop)
294
+ return null;
295
+ return (_jsxs(Div, { ref: containerRef, role: "toolbar", "aria-label": isBulkMode ? "Bulk actions" : "Page actions", "aria-hidden": !isVisible, className: [
296
+ // Position, z-index and the offset above the nav all belong to
297
+ // <BottomChrome>, the tier container this renders inside. `relative` is
298
+ // kept because the two panels below are `absolute bottom-full` against it.
299
+ "relative pointer-events-auto",
300
+ ]
301
+ .filter(Boolean)
302
+ .join(" "), "data-section": "bottomactions-div-401", children: [isBulkMode && (_jsx(BulkPickerPanel, { bulkActions: bulkActions, selectedActionId: selectedActionId, pickerOpen: pickerOpen, onSelect: (id) => { setSelectedActionId(id); setPickerOpen(false); } })), hasInfoPanel && !isBulkMode && (_jsx(InfoPanel, { id: infoPanelId, open: infoOpen, children: infoPanel })), _jsx(Div, { className: `grid transition-[grid-template-rows] duration-300 ease-out ${isVisible ? "grid-rows-[1fr]" : "grid-rows-[0fr]"}`, children: _jsx(Div, { overflow: "hidden", className: "min-h-0", children: _jsxs(Div, { className: [
303
+ BOTTOM_NAV_BG,
304
+ // Cast away from the edge the bar is welded to: upward from the
305
+ // bottom edge, downward from under the header.
306
+ "shadow-[0_-4px_20px_rgba(0,0,0,0.08)] dark:shadow-[0_-4px_20px_rgba(0,0,0,0.30)]",
307
+ "lg:shadow-[0_4px_20px_rgba(0,0,0,0.08)] dark:lg:shadow-[0_4px_20px_rgba(0,0,0,0.30)]",
308
+ // A hairline on the side facing the page, so the bar reads as
309
+ // chrome rather than as a floating slab.
310
+ "lg:border-b lg:border-[var(--appkit-color-border)]/80",
311
+ isVisible ? "" : "pointer-events-none",
312
+ ]
313
+ .filter(Boolean)
314
+ .join(" "), children: [isBulkMode && (_jsx(Div, { className: "h-[3px] w-full bg-[image:var(--appkit-gradient-brand-tri)]" })), secondaryLabel && !isBulkMode && (_jsx(Div, { className: "pt-[var(--appkit-space-2)] pb-0 border-b border-[var(--appkit-color-border-subtle)]", padding: "x-md", children: _jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: secondaryLabel }) })), infoLabel && !isBulkMode && !hasInfoPanel && (_jsx(Div, { className: "pt-[var(--appkit-space-2)] pb-0 border-b border-[var(--appkit-color-border-subtle)]", padding: "x-md", children: _jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: infoLabel }) })), infoLabel && !isBulkMode && hasInfoPanel && (_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setInfoOpen((o) => !o), "aria-expanded": infoOpen, "aria-controls": infoPanelId, className: "w-full flex items-center justify-between gap-[var(--appkit-space-2)] rounded-none border-b border-[var(--appkit-color-border)]/80 px-[var(--appkit-space-4)] pt-[var(--appkit-space-2)] pb-[var(--appkit-space-1)] min-h-0", children: [_jsx(Text, { className: "leading-5 truncate", color: "muted", size: "xs", weight: "semibold", children: infoLabel }), _jsxs(Span, { className: "flex-shrink-0 flex items-center gap-[var(--appkit-space-1)]", children: [_jsx(Text, { as: "span", size: "xs", color: "muted", children: infoOpen ? "Hide" : "Details" }), infoOpen ? (_jsx(ChevronDown, { className: "w-4 h-4 text-zinc-400", "aria-hidden": "true" })) : (_jsx(ChevronUp, { className: "w-4 h-4 text-zinc-400", "aria-hidden": "true" }))] })] })), _jsx(Row, { className: `min-${BOTTOM_NAV_HEIGHT} py-[var(--appkit-space-2)]`, gap: "sm", padding: "x-sm", children: isBulkMode && bulk ? (_jsxs(_Fragment, { children: [_jsxs(Button, { rounded: "full", gap: "xs", type: "button", variant: "ghost", onClick: dispatchBulkClear, className: "flex-shrink-0 bg-primary-50 hover:bg-primary-surface active:bg-primary-200 dark:bg-primary-950/30 text-primary-700 dark:text-primary-300 pl-2 pr-3 h-8 border border-primary-200/70 dark:border-primary-800/50 transition-colors min-h-0", "aria-label": "Clear selection", children: [_jsx(X, { className: "w-3.5 h-3.5 flex-shrink-0", "aria-hidden": "true" }), _jsx(Span, { size: "xs", weight: "semibold", className: "tabular-nums whitespace-nowrap leading-none", children: bulk.noun
315
+ ? `${bulk.selectedCount} ${bulk.noun}`
316
+ : `${bulk.selectedCount} selected` })] }), bulkActions.length > 0 && (_jsxs(Button, { type: "button", variant: "ghost", onClick: () => setPickerOpen((o) => !o), "aria-haspopup": "listbox", "aria-expanded": pickerOpen, className: [
317
+ "flex-1 min-w-0 h-10 flex items-center gap-[var(--appkit-space-2)] px-[var(--appkit-space-3)] rounded-lg border text-[length:var(--appkit-text-sm)] font-medium transition-colors",
318
+ " hover:bg-[var(--appkit-color-surface)] active:bg-zinc-200 bg-[var(--appkit-color-surface-input)] dark:hover:bg-slate-700/60",
319
+ "border-[var(--appkit-color-border)]",
320
+ selectedAction?.variant === "danger"
321
+ ? "text-error"
322
+ : "text-[var(--appkit-color-text)]",
323
+ ]
324
+ .filter(Boolean)
325
+ .join(" "), children: [selectedAction?.icon && (_jsx(Span, { className: `flex-shrink-0 w-4 h-4 ${FLEX_CENTER}`, "aria-hidden": "true", children: selectedAction.icon })), _jsx(Span, { className: "flex-1 truncate leading-none", align: "start", children: selectedAction?.label ?? "Bulk actions" }), pickerOpen ? (_jsx(ChevronDown, { className: "w-4 h-4 flex-shrink-0 text-zinc-400", "aria-hidden": "true" })) : (_jsx(ChevronUp, { className: "w-4 h-4 flex-shrink-0 text-zinc-400", "aria-hidden": "true" }))] })), bulkActions.length > 0 && (_jsx(Button, { type: "button", variant: selectedAction?.variant ?? "primary", size: "sm", isLoading: selectedAction?.loading, disabled: !selectedActionId ||
326
+ selectedAction?.disabled ||
327
+ selectedAction?.loading, onClick: handleApply, className: "h-10 flex-shrink-0", children: _jsx(Span, { className: "leading-none", children: "Apply" }) }))] })) : (
328
+ /* Page mode — action buttons inline ------------------------------ */
329
+ _jsx(PageActionsRow, { pageActions: pageActions, dispatchAction: dispatchAction })) })] }) }) })] }));
330
+ }
@@ -2,4 +2,49 @@ import React from "react";
2
2
  export interface BottomChromeProps {
3
3
  children: React.ReactNode;
4
4
  }
5
+ /**
6
+ * BottomChrome — the middle tier of the bottom edge.
7
+ *
8
+ * The bottom edge is three tiers: the nav bar is the floor, `BackToTop` is the
9
+ * ceiling, and *everything else* lives in here. This container is what makes
10
+ * that middle tier a single thing with a single height:
11
+ *
12
+ * --keyboard-inset-height useVisualViewportInset (the on-screen keyboard)
13
+ * --bottom-nav-height BottomNavLayout (the tab bar)
14
+ * --bottom-chrome-height THIS COMPONENT (CTA row, pagination, …)
15
+ * ↑ BackToTop floats above the sum of all three
16
+ *
17
+ * Because it is a flex column, its own `offsetHeight` IS the sum of whatever is
18
+ * inside it — a bar changing height, revealing on scroll, entering bulk mode,
19
+ * or being added years from now all just resize this box, and the one
20
+ * ResizeObserver below picks every case up. Consumers read one variable and
21
+ * never have to learn about individual bars. Publishing a separate var per bar
22
+ * was the alternative, and it fails the moment someone adds bar number three
23
+ * without also editing every consumer's `calc()`.
24
+ *
25
+ * Two constraints this container places on its children:
26
+ * - A child must hide by COLLAPSING, not by transforming. `translate-y-full`
27
+ * leaves layout height behind, so a hidden bar would still be reserved here
28
+ * on every page that has no CTA. See BottomActions' grid-rows collapse.
29
+ * - No `overflow: hidden` here, ever. BottomActions' bulk-picker and info
30
+ * panels are `absolute bottom-full` on mobile / `top-full` on desktop —
31
+ * deliberately outside the flow (they are transient tap-to-open overlays,
32
+ * not persistent bands, so they are excluded from the published height) —
33
+ * and clipping would erase them.
34
+ *
35
+ * ── The tier CHANGES ENDS at `lg` ──────────────────────────────────────────
36
+ *
37
+ * Below `lg` it is the bottom edge, as described above. At `lg` and up it hangs
38
+ * from under the sticky header instead, and publishes its height as
39
+ * `--top-chrome-height` rather than `--bottom-chrome-height`.
40
+ *
41
+ * A full-width action bar welded to the bottom of a desktop window is a mobile
42
+ * pattern on a screen that has neither a thumb zone nor a tab bar to stack
43
+ * above — it reads as a browser download bar, and it is the furthest point on
44
+ * screen from the content it acts on. Under the header it sits where the eye
45
+ * already is and where the page's own chrome lives.
46
+ *
47
+ * Consumers therefore read whichever end they care about, and the two are
48
+ * mutually exclusive by construction (see `write()`).
49
+ */
5
50
  export declare function BottomChrome({ children }: BottomChromeProps): React.JSX.Element;
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import React, { useEffect, useRef, useState } from "react";
3
+ import React, { useEffect, useRef } from "react";
4
4
  import { Div } from "../../ui/index.js";
5
5
  import { BOTTOM_CHROME_SLOT_ID } from "../../ui/components/bottom-chrome-slot.js";
6
+ import { useIsDesktop } from "./useIsDesktop.js";
6
7
  /**
7
8
  * BottomChrome — the middle tier of the bottom edge.
8
9
  *
@@ -48,26 +49,13 @@ import { BOTTOM_CHROME_SLOT_ID } from "../../ui/components/bottom-chrome-slot.js
48
49
  * Consumers therefore read whichever end they care about, and the two are
49
50
  * mutually exclusive by construction (see `write()`).
50
51
  */
51
- /**
52
- * The breakpoint at which the tier moves from the bottom edge to under the
53
- * header. 64rem is Tailwind's `lg` — the same line `BottomNavbar` disappears
54
- * at, so the tier changes ends exactly when the thing it was stacking above
55
- * stops existing.
56
- */
57
- const DESKTOP_QUERY = "(min-width: 64rem)";
58
52
  export function BottomChrome({ children }) {
59
53
  const ref = useRef(null);
60
- const [isDesktop, setIsDesktop] = useState(false);
61
54
  // Matched in JS as well as in CSS because the MEASUREMENT has to know which
62
55
  // variable to publish — a media query alone can move the box but cannot tell
63
- // the ResizeObserver which end of the screen it ended up at.
64
- useEffect(() => {
65
- const mq = window.matchMedia(DESKTOP_QUERY);
66
- const sync = () => setIsDesktop(mq.matches);
67
- sync();
68
- mq.addEventListener("change", sync);
69
- return () => mq.removeEventListener("change", sync);
70
- }, []);
56
+ // the ResizeObserver which end of the screen it ended up at. Shared with
57
+ // SideActionDock/BottomActions so one line decides the whole layout flip.
58
+ const isDesktop = useIsDesktop();
71
59
  useEffect(() => {
72
60
  const root = document.documentElement;
73
61
  const el = ref.current;
@@ -7,19 +7,31 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
7
7
  * (`useBottomActions`, `useFormBottomActions`, every `DataListingView` bulk
8
8
  * bar) are untouched: only the presentation changed.
9
9
  *
10
- * ── Why a side dock and not a bar ──────────────────────────────────────────
10
+ * ── 🛑 DESKTOP ONLY. The phone gets `BottomActions`. ───────────────────────
11
11
  *
12
- * A full-width bar welded to an edge covers content the whole time it is up —
13
- * the last row of a listing on mobile, and on desktop it either reads as a
14
- * browser download bar at the bottom or steals the header's space at the top.
15
- * A dock is a 56px button at rest and a panel only while it is being used.
12
+ * This used to render at every width, and on a phone that was the wrong trade
13
+ * twice over: a floating puck sits in the middle of the content on the screen
14
+ * with the least of it, and it replaced a bar that was already in the thumb
15
+ * zone and already stacked correctly above the tab bar. A phone has a thumb
16
+ * zone and a bottom edge that means something; a 1440px window has neither,
17
+ * which is the entire argument for each half.
18
+ *
19
+ * The two are mutually exclusive by construction — `useIsDesktop` decides, and
20
+ * both halves read the SAME `BottomActionsContext`, so all ~30 publishers are
21
+ * untouched and nothing can register with only one of them.
16
22
  *
17
23
  * ── Three rules this encodes ───────────────────────────────────────────────
18
24
  *
19
- * 1. **It docks OPPOSITE the sidebar.** `SidebarLayout` docks to the hand-mode
20
- * side (`hand === "left" ? left-0 : right-0`), so the dock takes the other
21
- * edge and the two can never overlap — including while the sidebar is open,
25
+ * 1. **It docks OPPOSITE the sidebar, at the vertical MIDDLE.** `SidebarLayout`
26
+ * docks to the hand-mode side (`hand === "left" ? left-0 : right-0`), so the
27
+ * dock takes the other edge — left, in the default right-hand configuration
28
+ * — and the two can never overlap, including while the sidebar is open,
22
29
  * which is exactly when a CTA hidden underneath it would be unreachable.
30
+ * Vertically it is centred rather than bottom-anchored: at the bottom of a
31
+ * desktop window it reads as a browser download bar and sits as far from
32
+ * the content as the viewport allows. Centred, it is beside the content,
33
+ * and it needs no bottom-tier clearance at all — there is no tab bar at
34
+ * this width and the pagination bar is at the other end of the screen.
23
35
  *
24
36
  * 2. **It reveals only after the inline CTA has been seen and passed** — see
25
37
  * `useCtaReveal`. A page with no CTA anchor shows it at once, which is what
@@ -41,6 +53,7 @@ import { useBottomActionsContext } from "./BottomActionsContext.js";
41
53
  import { useClickOutside } from "../../react/index.js";
42
54
  import { useHandMode } from "../../_internal/client/hand-mode/index.js";
43
55
  import { useCtaReveal } from "./useCtaReveal.js";
56
+ import { useIsDesktop } from "./useIsDesktop.js";
44
57
  import { ActionRow, Div, IconButton, Row, Span, Stack, Text, Button } from "../../ui/index.js";
45
58
  const PANEL_SURFACE = "bg-[color-mix(in_srgb,var(--appkit-color-bg)_94%,transparent)] backdrop-blur-md border border-[var(--appkit-color-border)]";
46
59
  const FLEX_CENTER = "flex items-center justify-center";
@@ -111,6 +124,7 @@ function BulkActionList({ bulkActions, selectedActionId, onSelect, }) {
111
124
  export default function SideActionDock() {
112
125
  const { state, actionCallbacksRef, bulkCallbacksRef, bulkClearRef } = useBottomActionsContext();
113
126
  const { hand } = useHandMode();
127
+ const isDesktop = useIsDesktop();
114
128
  const [expanded, setExpanded] = useState(false);
115
129
  const [infoOpen, setInfoOpen] = useState(false);
116
130
  const [selectedActionId, setSelectedActionId] = useState(null);
@@ -130,7 +144,7 @@ export default function SideActionDock() {
130
144
  !!secondaryLabel;
131
145
  // Rule 3 — a selection is its own reveal.
132
146
  const revealed = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId) || isBulkMode;
133
- const isVisible = hasContent && revealed;
147
+ const isVisible = isDesktop && hasContent && revealed;
134
148
  // Rule 1 — opposite the sidebar, which docks to the hand-mode side.
135
149
  const dockSideClass = hand === "left" ? "right-0" : "left-0";
136
150
  const panelAlignClass = hand === "left" ? "items-end" : "items-start";
@@ -206,10 +220,19 @@ export default function SideActionDock() {
206
220
  "fixed z-[var(--appkit-z-overlay)] pointer-events-auto flex flex-col",
207
221
  panelAlignClass,
208
222
  dockSideClass,
209
- // Clears the whole bottom tier, so it never lands on the mobile tab bar
210
- // or on a pagination bar — the same sum every other floating control
211
- // reads (see CSS Variable Reference → "The bottom edge is three tiers").
212
- "bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem)+var(--bottom-chrome-height,0px)+var(--appkit-space-4))]",
223
+ /*
224
+ * Vertically centred — see rule 1. This is a desktop-only control, so
225
+ * it deliberately reads NONE of the three bottom-tier variables: there
226
+ * is no tab bar at this width, the keyboard inset is a phone concern,
227
+ * and the pagination bar is at the opposite end of the viewport. An
228
+ * unused `calc()` over variables that are 0 here is just a claim about
229
+ * a relationship that no longer exists.
230
+ *
231
+ * `max-h` + `justify-center` so a tall expanded panel grows about the
232
+ * midpoint and is clipped by the viewport rather than running off both
233
+ * ends of it; the panel scrolls internally.
234
+ */
235
+ "top-1/2 -translate-y-1/2 max-h-[90vh] justify-center",
213
236
  "px-[var(--appkit-space-3)] gap-[var(--appkit-space-2)]",
214
237
  ].join(" "), "data-section": "side-action-dock", children: [expanded && (_jsx(Div, { surface: "elevated", rounded: "xl", shadow: "lg", padding: "sm", className: `${PANEL_SURFACE} w-[min(20rem,calc(100vw-2rem))] max-h-[70vh] overflow-y-auto`, children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { align: "center", justify: "between", gap: "sm", children: [_jsx(Text, { size: "xs", weight: "semibold", color: "muted", className: "truncate", children: isBulkMode
215
238
  ? bulk?.noun
@@ -33,13 +33,8 @@ export type { UseBottomActionsOptions } from "./hooks/useBottomActions";
33
33
  export { useFormBottomActions } from "./hooks/useFormBottomActions";
34
34
  export type { UseFormBottomActionsOptions } from "./hooks/useFormBottomActions";
35
35
  export { default as SideActionDock } from "./SideActionDock";
36
- /**
37
- * @deprecated The bottom action bar is now a floating side dock. Kept as an
38
- * alias so a consumer that mounts it by name keeps compiling and keeps
39
- * rendering the same actions — the component it resolves to is
40
- * `SideActionDock`.
41
- */
42
- export { default as BottomActions } from "./SideActionDock";
36
+ export { default as BottomActions } from "./BottomActions";
37
+ export { useIsDesktop, DESKTOP_QUERY } from "./useIsDesktop";
43
38
  export { ctaAnchorProps, CTA_ANCHOR_ATTR } from "./cta-anchor";
44
39
  export { BottomChrome } from "./BottomChrome";
45
40
  export type { BottomChromeProps } from "./BottomChrome";
@@ -17,14 +17,14 @@ export { AppLayoutShell } from "./AppLayoutShell.js";
17
17
  export { BottomActionsProvider, useBottomActionsContext, } from "./BottomActionsContext.js";
18
18
  export { useBottomActions } from "./hooks/useBottomActions.js";
19
19
  export { useFormBottomActions } from "./hooks/useFormBottomActions.js";
20
- export { default as SideActionDock } from "./SideActionDock.js";
21
- /**
22
- * @deprecated The bottom action bar is now a floating side dock. Kept as an
23
- * alias so a consumer that mounts it by name keeps compiling and keeps
24
- * rendering the same actions — the component it resolves to is
25
- * `SideActionDock`.
20
+ /*
21
+ * The two presentations of one control. `AppLayoutShell` mounts both and
22
+ * `useIsDesktop` lets exactly one render — do not mount either directly, or a
23
+ * page gets two toolbars publishing into the same context.
26
24
  */
27
- export { default as BottomActions } from "./SideActionDock.js";
25
+ export { default as SideActionDock } from "./SideActionDock.js";
26
+ export { default as BottomActions } from "./BottomActions.js";
27
+ export { useIsDesktop, DESKTOP_QUERY } from "./useIsDesktop.js";
28
28
  export { ctaAnchorProps, CTA_ANCHOR_ATTR } from "./cta-anchor.js";
29
29
  export { BottomChrome } from "./BottomChrome.js";
30
30
  export { BottomNavbar } from "./BottomNavbar.js";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The line at which the layout stops being a phone.
3
+ *
4
+ * 64rem is Tailwind's `lg` — deliberately the same breakpoint `BottomNavbar`
5
+ * disappears at, so every decision that depends on "is there a tab bar at the
6
+ * bottom of this screen" flips at one place.
7
+ */
8
+ export declare const DESKTOP_QUERY = "(min-width: 64rem)";
9
+ /**
10
+ * `true` at `lg` and up.
11
+ *
12
+ * ── 🛑 WHY A HOOK AND NOT A `lg:` CLASS ────────────────────────────────────
13
+ *
14
+ * Three callers need the breakpoint as a VALUE, not as styling:
15
+ *
16
+ * - `BottomChrome` must know which CSS variable to publish — a media query
17
+ * can move the box but cannot tell a `ResizeObserver` which end of the
18
+ * screen it landed at.
19
+ * - `SideActionDock` and `BottomActions` are the two halves of one control
20
+ * and must never both be mounted: two toolbars reading the same context,
21
+ * both `fixed`, both dispatching the same callbacks. Hiding one with
22
+ * `display:none` would still mount it, still run its observers, and still
23
+ * let a stray `:focus` land inside an invisible bar.
24
+ *
25
+ * And the class form is actively unsafe here. Both Tailwind configs set
26
+ * `important: true`, so a `lg:` override and its unprefixed base tie on
27
+ * specificity AND on `!important` — a media query adds no specificity — and
28
+ * the winner is decided by EMISSION ORDER in the built CSS, which is invisible
29
+ * from the call site. That is exactly how `BottomChrome` ended up anchored to
30
+ * both edges at once and published an 801px phantom bar (see its header).
31
+ *
32
+ * SSR renders the mobile branch, because that is the branch that is correct
33
+ * when nothing is known: a phone that briefly gets the dock has lost its only
34
+ * CTA, while a desktop that briefly gets the bar has a bar.
35
+ */
36
+ export declare function useIsDesktop(): boolean;