@mohasinac/appkit 4.62.1 → 4.63.1
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.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
- package/dist/_internal/server/features/checkout/actions.js +335 -80
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.js +26 -1
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +14 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +57 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +268 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
- package/dist/_internal/shared/checkout/order-math.js +22 -36
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +0 -12
- package/dist/_internal/shared/checkout/split.js +22 -2
- package/dist/_internal/shared/content/disclaimers.js +7 -7
- package/dist/_internal/shared/content/published-values.d.ts +6 -1
- package/dist/_internal/shared/content/published-values.js +8 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
- package/dist/_internal/shared/features/admin/integrations.js +19 -9
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
- package/dist/_internal/shared/features/points/accepts.js +12 -6
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +11 -1
- package/dist/client.js +13 -0
- package/dist/constants/api-endpoints.d.ts +72 -0
- package/dist/constants/api-endpoints.js +41 -0
- package/dist/constants/field-names.d.ts +41 -0
- package/dist/constants/field-names.js +47 -0
- package/dist/features/account/components/UserOrdersView.js +28 -2
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
- package/dist/features/admin/components/AdminOrdersView.js +20 -1
- package/dist/features/admin/schemas/firestore.d.ts +32 -0
- package/dist/features/ads/schemas/booking-write.d.ts +157 -0
- package/dist/features/ads/schemas/booking-write.js +88 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
- package/dist/features/audit-log/schemas/firestore.js +74 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
- package/dist/features/cart/repository/cart.repository.d.ts +14 -0
- package/dist/features/cart/repository/cart.repository.js +20 -0
- package/dist/features/cart/schemas/firestore.d.ts +24 -0
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
- package/dist/features/orders/repository/orders.repository.d.ts +28 -1
- package/dist/features/orders/repository/orders.repository.js +60 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +202 -0
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +21 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +4 -0
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +94 -2
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +41 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/next/routing/route-map.d.ts +92 -0
- package/dist/next/routing/route-map.js +53 -0
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +22 -16
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +10 -1
- package/dist/server.d.ts +29 -1
- package/dist/server.js +158 -0
- package/dist/styles.css +349 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.js +10 -1
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +35 -0
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +3 -0
|
@@ -76,6 +76,15 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
|
|
76
76
|
* Children and aria-label props override the action defaults when explicitly provided.
|
|
77
77
|
*/
|
|
78
78
|
action?: ActionDef;
|
|
79
|
+
/**
|
|
80
|
+
* Replace the confirmation dialog's BODY for this one render.
|
|
81
|
+
*
|
|
82
|
+
* Does not create a dialog: ignored unless `action.confirmation` exists, so
|
|
83
|
+
* the registry still decides whether an action is confirmed at all. This
|
|
84
|
+
* only substitutes the sentence, for the case where the consequence is data
|
|
85
|
+
* the registry should not hold a copy of. See the note at `confirmBody`.
|
|
86
|
+
*/
|
|
87
|
+
confirmationBody?: React.ReactNode;
|
|
79
88
|
}
|
|
80
|
-
export declare function Button({ variant, size, className, isLoading, disabled, children, gap, textSize, paddingX, paddingY, weight, textColor, border, rounded, shadow, justify, asChild, action, ...props }: ButtonProps): React.JSX.Element;
|
|
89
|
+
export declare function Button({ variant, size, className, isLoading, disabled, children, gap, textSize, paddingX, paddingY, weight, textColor, border, rounded, shadow, justify, asChild, action, confirmationBody, ...props }: ButtonProps): React.JSX.Element;
|
|
81
90
|
export {};
|
|
@@ -143,7 +143,7 @@ const ACTION_ICON_SIZE = {
|
|
|
143
143
|
md: "md",
|
|
144
144
|
lg: "lg",
|
|
145
145
|
};
|
|
146
|
-
function ActionIcon({ action, size, }) {
|
|
146
|
+
function ActionIcon({ action, confirmationBody, size, }) {
|
|
147
147
|
const Glyph = resolveIcon(action?.iconKey);
|
|
148
148
|
if (!Glyph)
|
|
149
149
|
return null;
|
|
@@ -158,7 +158,7 @@ const ACTION_KIND_VARIANT = {
|
|
|
158
158
|
link: "ghost",
|
|
159
159
|
chip: "outline",
|
|
160
160
|
};
|
|
161
|
-
export function Button({ variant, size = "md", className = "", isLoading = false, disabled, children, gap, textSize, paddingX, paddingY, weight, textColor, border, rounded, shadow, justify, asChild = false, action, ...props }) {
|
|
161
|
+
export function Button({ variant, size = "md", className = "", isLoading = false, disabled, children, gap, textSize, paddingX, paddingY, weight, textColor, border, rounded, shadow, justify, asChild = false, action, confirmationBody, ...props }) {
|
|
162
162
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
163
163
|
// React 17+ no longer pools SyntheticEvents, so it's safe to hold onto the
|
|
164
164
|
// original triggering event across the confirmation dialog's async gap and
|
|
@@ -242,6 +242,28 @@ export function Button({ variant, size = "md", className = "", isLoading = false
|
|
|
242
242
|
const confirmVariant = confirmDef?.confirmKind
|
|
243
243
|
? (ACTION_KIND_VARIANT[confirmDef.confirmKind] ?? "primary")
|
|
244
244
|
: "primary";
|
|
245
|
+
/*
|
|
246
|
+
* The body a caller supplied, when the consequence is DATA rather than copy.
|
|
247
|
+
*
|
|
248
|
+
* 🛑 It cannot CREATE a dialog — `confirmPortal` still gates on
|
|
249
|
+
* `action.confirmation` existing. So the registry keeps every decision that
|
|
250
|
+
* matters: whether this action is confirmed at all, its title, its confirm
|
|
251
|
+
* label, and its destructive kind. Only the sentence in the middle can be
|
|
252
|
+
* substituted, and only when the ActionDef has already declared that there
|
|
253
|
+
* IS one.
|
|
254
|
+
*
|
|
255
|
+
* Why the seam is needed: `/admin/integrations` renders one row per
|
|
256
|
+
* integration, and "what switching this off does" genuinely differs per row
|
|
257
|
+
* — payments abandons in-flight payments, support hides a contact route,
|
|
258
|
+
* shipping selects a working manual path and stops nothing. That text already
|
|
259
|
+
* lives on `INTEGRATIONS[id].offConsequence`, which is also what the row
|
|
260
|
+
* itself renders. The alternatives were worse in both directions: four
|
|
261
|
+
* near-duplicate ActionDefs with the consequence copied into the registry (a
|
|
262
|
+
* second place for one sentence to drift from — Root Cause #42), or a generic
|
|
263
|
+
* "are you sure?" that flattens exactly the distinction the model exists to
|
|
264
|
+
* preserve.
|
|
265
|
+
*/
|
|
266
|
+
const confirmBody = confirmationBody ?? confirmDef?.body;
|
|
245
267
|
// Built once and rendered alongside EITHER the asChild clone or the plain
|
|
246
268
|
// <button> below — this must never live inside an early return for only
|
|
247
269
|
// one of those two branches, or the other branch can never open a
|
|
@@ -258,7 +280,7 @@ export function Button({ variant, size = "md", className = "", isLoading = false
|
|
|
258
280
|
padding: "1.5rem",
|
|
259
281
|
maxWidth: "380px", width: "calc(100% - 2rem)",
|
|
260
282
|
boxShadow: "var(--appkit-shadow-lg, 0 20px 60px rgba(0,0,0,0.2))",
|
|
261
|
-
}, children: [_jsx("p", { id: "appkit-action-confirm-title", style: { fontWeight: 600, marginBottom: "0.5rem", fontSize: "1rem" }, children: confirmDef.title }), _jsx("p", { style: { fontSize: "0.875rem", color: "var(--appkit-color-text-muted)", marginBottom: "1.25rem" }, children:
|
|
283
|
+
}, children: [_jsx("p", { id: "appkit-action-confirm-title", style: { fontWeight: 600, marginBottom: "0.5rem", fontSize: "1rem" }, children: confirmDef.title }), _jsx("p", { style: { fontSize: "0.875rem", color: "var(--appkit-color-text-muted)", marginBottom: "1.25rem" }, children: confirmBody }), _jsxs("div", { style: { display: "flex", gap: "0.5rem", justifyContent: "flex-end" }, children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setConfirmOpen(false), children: confirmDef.cancelLabel ?? "Cancel" }), _jsx(Button, { variant: confirmVariant, size: "sm", onClick: handleConfirm, children: confirmDef.confirmLabel })] })] }) }), document.body);
|
|
262
284
|
if (asChild && React.isValidElement(resolvedChildren)) {
|
|
263
285
|
const child = resolvedChildren;
|
|
264
286
|
// Never spread the caller's raw onClick here — it must go through
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one chart primitive — C-chart.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Three features had already hand-rolled the same thing:
|
|
7
|
+
* `AdminAnalyticsCharts`, `SellerRevenueChart` and `TesterFeedbackChart` each
|
|
8
|
+
* re-declare the same eight `dynamic(() => import("recharts"))` wrappers with
|
|
9
|
+
* the same `as React.ComponentType<any>` cast and their own hardcoded fills.
|
|
10
|
+
* Three copies is the Duplication Framework's Rule-of-Three trigger, so this is
|
|
11
|
+
* consolidation of something that exists, not speculative infrastructure.
|
|
12
|
+
*
|
|
13
|
+
* It is also what lets a chart live in CMS content: `ChartSpec` is a plain
|
|
14
|
+
* JSON-serialisable object, so a rich-text author can embed one as
|
|
15
|
+
* `<figure data-chart='{...}'>` and the renderer hydrates it here.
|
|
16
|
+
*
|
|
17
|
+
* The data contract and its validator live in `./chart-spec`, which is pure —
|
|
18
|
+
* see that file's header for why the split matters.
|
|
19
|
+
*
|
|
20
|
+
* ## 🛑 recharts is client-only, and that is not incidental
|
|
21
|
+
*
|
|
22
|
+
* Every recharts component is behind `dynamic(..., { ssr: false })`, so this
|
|
23
|
+
* module renders its chrome on the server and the plot only in the browser.
|
|
24
|
+
* `RichTextRenderer` is a Server Component; it can mount this because this is a
|
|
25
|
+
* client component, but nothing here may be called during SSR.
|
|
26
|
+
*
|
|
27
|
+
* ## 🛑 The palette lives in CSS and NOWHERE ELSE
|
|
28
|
+
*
|
|
29
|
+
* There is deliberately no colour literal in this file. An earlier draft kept a
|
|
30
|
+
* `FALLBACK_SERIES_COLORS` array of eight hexes beside the identical eight in
|
|
31
|
+
* `Chart.style.css` — which is a denormalised mirror of exactly the kind Root
|
|
32
|
+
* Cause #42 is about: the day someone re-steps a hue for contrast, they fix one
|
|
33
|
+
* copy, and the chart renders one colour in CSS and a different one in JS with
|
|
34
|
+
* nothing reporting the disagreement.
|
|
35
|
+
*
|
|
36
|
+
* So the colours are read from the cascade at runtime, and if the stylesheet is
|
|
37
|
+
* absent the chart renders with recharts' own defaults plus a one-time console
|
|
38
|
+
* warning naming the likely cause. A wrong-but-plausible colour is worse than
|
|
39
|
+
* an obviously unstyled one.
|
|
40
|
+
*
|
|
41
|
+
* Colour is read off THIS element rather than from `useTheme()`, which throws
|
|
42
|
+
* without a `<ThemeProvider>` — a chart has to survive a preview iframe, an
|
|
43
|
+
* email preview or a test harness, the same reason `useCanSeePrices` uses
|
|
44
|
+
* `useOptionalSession` (Root Cause #20).
|
|
45
|
+
*/
|
|
46
|
+
import React from "react";
|
|
47
|
+
import { type ChartSpec } from "./chart-spec";
|
|
48
|
+
export interface ChartProps {
|
|
49
|
+
spec: ChartSpec;
|
|
50
|
+
/** Plot height in px. Default 300. */
|
|
51
|
+
height?: number;
|
|
52
|
+
className?: string;
|
|
53
|
+
/** Hide the table-view toggle. The legend is never optional for ≥2 series. */
|
|
54
|
+
hideTableToggle?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export declare function Chart({ spec, height, className, hideTableToggle, }: ChartProps): React.JSX.Element;
|
|
57
|
+
/**
|
|
58
|
+
* Render a `ChartSpec` from untrusted JSON, showing the validation reason when
|
|
59
|
+
* it does not parse — never rendering nothing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function ChartFromJson({ json, ...rest }: {
|
|
62
|
+
json: string;
|
|
63
|
+
} & Omit<ChartProps, "spec">): React.JSX.Element;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The one chart primitive — C-chart.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this exists
|
|
7
|
+
*
|
|
8
|
+
* Three features had already hand-rolled the same thing:
|
|
9
|
+
* `AdminAnalyticsCharts`, `SellerRevenueChart` and `TesterFeedbackChart` each
|
|
10
|
+
* re-declare the same eight `dynamic(() => import("recharts"))` wrappers with
|
|
11
|
+
* the same `as React.ComponentType<any>` cast and their own hardcoded fills.
|
|
12
|
+
* Three copies is the Duplication Framework's Rule-of-Three trigger, so this is
|
|
13
|
+
* consolidation of something that exists, not speculative infrastructure.
|
|
14
|
+
*
|
|
15
|
+
* It is also what lets a chart live in CMS content: `ChartSpec` is a plain
|
|
16
|
+
* JSON-serialisable object, so a rich-text author can embed one as
|
|
17
|
+
* `<figure data-chart='{...}'>` and the renderer hydrates it here.
|
|
18
|
+
*
|
|
19
|
+
* The data contract and its validator live in `./chart-spec`, which is pure —
|
|
20
|
+
* see that file's header for why the split matters.
|
|
21
|
+
*
|
|
22
|
+
* ## 🛑 recharts is client-only, and that is not incidental
|
|
23
|
+
*
|
|
24
|
+
* Every recharts component is behind `dynamic(..., { ssr: false })`, so this
|
|
25
|
+
* module renders its chrome on the server and the plot only in the browser.
|
|
26
|
+
* `RichTextRenderer` is a Server Component; it can mount this because this is a
|
|
27
|
+
* client component, but nothing here may be called during SSR.
|
|
28
|
+
*
|
|
29
|
+
* ## 🛑 The palette lives in CSS and NOWHERE ELSE
|
|
30
|
+
*
|
|
31
|
+
* There is deliberately no colour literal in this file. An earlier draft kept a
|
|
32
|
+
* `FALLBACK_SERIES_COLORS` array of eight hexes beside the identical eight in
|
|
33
|
+
* `Chart.style.css` — which is a denormalised mirror of exactly the kind Root
|
|
34
|
+
* Cause #42 is about: the day someone re-steps a hue for contrast, they fix one
|
|
35
|
+
* copy, and the chart renders one colour in CSS and a different one in JS with
|
|
36
|
+
* nothing reporting the disagreement.
|
|
37
|
+
*
|
|
38
|
+
* So the colours are read from the cascade at runtime, and if the stylesheet is
|
|
39
|
+
* absent the chart renders with recharts' own defaults plus a one-time console
|
|
40
|
+
* warning naming the likely cause. A wrong-but-plausible colour is worse than
|
|
41
|
+
* an obviously unstyled one.
|
|
42
|
+
*
|
|
43
|
+
* Colour is read off THIS element rather than from `useTheme()`, which throws
|
|
44
|
+
* without a `<ThemeProvider>` — a chart has to survive a preview iframe, an
|
|
45
|
+
* email preview or a test harness, the same reason `useCanSeePrices` uses
|
|
46
|
+
* `useOptionalSession` (Root Cause #20).
|
|
47
|
+
*/
|
|
48
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
49
|
+
import dynamic from "next/dynamic.js";
|
|
50
|
+
import { CHART_MAX_SERIES, parseChartAttr, } from "./chart-spec.js";
|
|
51
|
+
/*
|
|
52
|
+
* recharts exports generic components (<DataPointType>) whose inferred props do
|
|
53
|
+
* not satisfy `dynamic()`'s ComponentType constraint, so each needs a cast.
|
|
54
|
+
* This is the SINGLE copy of that cast in the codebase — the three feature
|
|
55
|
+
* charts above should migrate onto this module rather than repeating it.
|
|
56
|
+
*/
|
|
57
|
+
const ResponsiveContainer = dynamic(() => import("recharts").then((m) => m.ResponsiveContainer), { ssr: false });
|
|
58
|
+
const BarChart = dynamic(() => import("recharts").then((m) => m.BarChart), { ssr: false });
|
|
59
|
+
const LineChart = dynamic(() => import("recharts").then((m) => m.LineChart), { ssr: false });
|
|
60
|
+
const AreaChart = dynamic(() => import("recharts").then((m) => m.AreaChart), { ssr: false });
|
|
61
|
+
const Bar = dynamic(() => import("recharts").then((m) => m.Bar), { ssr: false });
|
|
62
|
+
const Line = dynamic(() => import("recharts").then((m) => m.Line), { ssr: false });
|
|
63
|
+
const Area = dynamic(() => import("recharts").then((m) => m.Area), { ssr: false });
|
|
64
|
+
const XAxis = dynamic(() => import("recharts").then((m) => m.XAxis), { ssr: false });
|
|
65
|
+
const YAxis = dynamic(() => import("recharts").then((m) => m.YAxis), { ssr: false });
|
|
66
|
+
const CartesianGrid = dynamic(() => import("recharts").then((m) => m.CartesianGrid), { ssr: false });
|
|
67
|
+
const Tooltip = dynamic(() => import("recharts").then((m) => m.Tooltip), { ssr: false });
|
|
68
|
+
/** `""` → `undefined`, so recharts falls back to its own default. */
|
|
69
|
+
const orUndef = (s) => (s.length > 0 ? s : undefined);
|
|
70
|
+
let warnedMissingStylesheet = false;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the palette from the element's own computed style, and re-resolve
|
|
73
|
+
* whenever the active theme changes.
|
|
74
|
+
*
|
|
75
|
+
* Two triggers, because there are two ways the theme can move: the viewer's
|
|
76
|
+
* toggle (which stamps `data-theme` on `<html>`) and the OS preference (which
|
|
77
|
+
* only the media query sees). Watching one and not the other leaves a chart
|
|
78
|
+
* stranded in the previous palette.
|
|
79
|
+
*
|
|
80
|
+
* Returns `null` until the first read completes — one frame after mount, which
|
|
81
|
+
* is invisible given the plot itself is behind `ssr: false` anyway.
|
|
82
|
+
*/
|
|
83
|
+
function useChartPalette(ref) {
|
|
84
|
+
const [palette, setPalette] = useState(null);
|
|
85
|
+
const read = useCallback(() => {
|
|
86
|
+
const el = ref.current;
|
|
87
|
+
if (!el || typeof window === "undefined")
|
|
88
|
+
return;
|
|
89
|
+
const cs = window.getComputedStyle(el);
|
|
90
|
+
const v = (name) => cs.getPropertyValue(name).trim();
|
|
91
|
+
const series = Array.from({ length: CHART_MAX_SERIES }, (_, i) => v(`--chart-${i + 1}`));
|
|
92
|
+
if (!series[0] && !warnedMissingStylesheet) {
|
|
93
|
+
warnedMissingStylesheet = true;
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.warn("[appkit] <Chart> found no --chart-* custom properties. " +
|
|
96
|
+
'Import the stylesheet once at your app root: import "@mohasinac/appkit/styles". ' +
|
|
97
|
+
"Rendering with recharts' default colours until then.");
|
|
98
|
+
}
|
|
99
|
+
setPalette({
|
|
100
|
+
series,
|
|
101
|
+
grid: v("--chart-grid"),
|
|
102
|
+
axis: v("--chart-axis"),
|
|
103
|
+
inkMuted: v("--chart-ink-muted"),
|
|
104
|
+
});
|
|
105
|
+
}, [ref]);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
read();
|
|
108
|
+
if (typeof window === "undefined")
|
|
109
|
+
return;
|
|
110
|
+
const observer = new MutationObserver(read);
|
|
111
|
+
observer.observe(document.documentElement, {
|
|
112
|
+
attributes: true,
|
|
113
|
+
attributeFilter: ["data-theme", "style", "class"],
|
|
114
|
+
});
|
|
115
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
116
|
+
mq.addEventListener("change", read);
|
|
117
|
+
return () => {
|
|
118
|
+
observer.disconnect();
|
|
119
|
+
mq.removeEventListener("change", read);
|
|
120
|
+
};
|
|
121
|
+
}, [read]);
|
|
122
|
+
return palette;
|
|
123
|
+
}
|
|
124
|
+
const fmt = (n, unit) => `${Number.isInteger(n) ? n : n.toFixed(2)}${unit ? ` ${unit}` : ""}`;
|
|
125
|
+
export function Chart({ spec, height = 300, className = "", hideTableToggle = false, }) {
|
|
126
|
+
const rootRef = useRef(null);
|
|
127
|
+
const palette = useChartPalette(rootRef);
|
|
128
|
+
const [showTable, setShowTable] = useState(false);
|
|
129
|
+
/** recharts wants row-per-category; the spec is column-per-series. */
|
|
130
|
+
const rows = useMemo(() => spec.categories.map((cat, i) => {
|
|
131
|
+
const row = { category: cat };
|
|
132
|
+
for (const s of spec.series)
|
|
133
|
+
row[s.label] = s.values[i] ?? 0;
|
|
134
|
+
return row;
|
|
135
|
+
}), [spec]);
|
|
136
|
+
const multi = spec.series.length >= 2;
|
|
137
|
+
const accessibleName = spec.title ?? spec.caption ?? "Chart";
|
|
138
|
+
const colorAt = (i) => orUndef(palette?.series[i % CHART_MAX_SERIES] ?? "");
|
|
139
|
+
const renderTable = () => (_jsx("div", { className: "appkit-chart__scroll", children: _jsxs("table", { className: "appkit-chart__table", children: [_jsx("caption", { className: "appkit-chart__caption", children: accessibleName }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Category" }), spec.series.map((s) => (_jsxs("th", { scope: "col", children: [s.label, spec.unit ? ` (${spec.unit})` : ""] }, s.label)))] }) }), _jsx("tbody", { children: spec.categories.map((cat, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: cat }), spec.series.map((s) => (_jsx("td", { children: fmt(s.values[i] ?? 0, spec.unit) }, s.label)))] }, cat))) })] }) }));
|
|
140
|
+
const tooltip = (_jsx(Tooltip, { content: (props) => {
|
|
141
|
+
if (!props?.active || !Array.isArray(props.payload) || !props.payload.length) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return (_jsxs("div", { className: "appkit-chart__tooltip", children: [_jsx("div", { className: "appkit-chart__tooltip-label", children: props.label }), props.payload.map((p) => (_jsxs("div", { className: "appkit-chart__tooltip-row", children: [_jsx("span", { style: { color: p.color }, children: String(p.dataKey) }), _jsx("span", { children: fmt(Number(p.value), spec.unit) })] }, String(p.dataKey))))] }));
|
|
145
|
+
} }));
|
|
146
|
+
const axes = (_jsxs(_Fragment, { children: [_jsx(CartesianGrid, { stroke: orUndef(palette?.grid ?? ""), vertical: false }), _jsx(XAxis, { dataKey: "category", stroke: orUndef(palette?.axis ?? ""), tick: { fill: orUndef(palette?.inkMuted ?? ""), fontSize: 12 }, tickLine: false }), _jsx(YAxis, { stroke: orUndef(palette?.axis ?? ""), tick: { fill: orUndef(palette?.inkMuted ?? ""), fontSize: 12 }, tickLine: false, width: 48 }), tooltip] }));
|
|
147
|
+
let plot;
|
|
148
|
+
if (spec.kind === "bar" || spec.kind === "groupedBar") {
|
|
149
|
+
plot = (_jsxs(BarChart, { data: rows, barGap: 2, barCategoryGap: "20%", children: [axes, spec.series.map((s, i) => (_jsx(Bar, { dataKey: s.label, fill: colorAt(i), radius: [4, 4, 0, 0] }, s.label)))] }));
|
|
150
|
+
}
|
|
151
|
+
else if (spec.kind === "line") {
|
|
152
|
+
plot = (_jsxs(LineChart, { data: rows, children: [axes, spec.series.map((s, i) => (_jsx(Line, { type: "monotone", dataKey: s.label, stroke: colorAt(i), strokeWidth: 2, dot: { r: 3 }, activeDot: { r: 5 } }, s.label)))] }));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
plot = (_jsxs(AreaChart, { data: rows, children: [axes, spec.series.map((s, i) => (_jsx(Area, { type: "monotone", dataKey: s.label, stroke: colorAt(i), strokeWidth: 2, fill: colorAt(i), fillOpacity: 0.18 }, s.label)))] }));
|
|
156
|
+
}
|
|
157
|
+
return (_jsxs("div", { ref: rootRef, className: `appkit-chart ${className}`.trim(), role: "group", "aria-label": accessibleName, children: [multi && palette ? (_jsx("div", { className: "appkit-chart__legend", children: spec.series.map((s, i) => (_jsxs("span", { children: [_jsx("span", { className: "appkit-chart__legend-swatch", style: { background: colorAt(i) }, "aria-hidden": "true" }), s.label] }, s.label))) })) : null, showTable ? (renderTable()) : (_jsx("div", { className: "appkit-chart__plot", style: { height }, children: palette ? (_jsx(ResponsiveContainer, { width: "100%", height: "100%", children: plot })) : null })), spec.caption ? _jsx("p", { className: "appkit-chart__caption", children: spec.caption }) : null, hideTableToggle ? null : (_jsx("div", { className: "appkit-chart__toolbar", children: _jsx("button", { type: "button", className: "appkit-chart__toggle", onClick: () => setShowTable((v) => !v), "aria-pressed": showTable, children: showTable ? "Show chart" : "Show as table" }) }))] }));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Render a `ChartSpec` from untrusted JSON, showing the validation reason when
|
|
161
|
+
* it does not parse — never rendering nothing.
|
|
162
|
+
*/
|
|
163
|
+
export function ChartFromJson({ json, ...rest }) {
|
|
164
|
+
const result = useMemo(() => parseChartAttr(json), [json]);
|
|
165
|
+
if (!result.ok) {
|
|
166
|
+
return (_jsx("div", { className: "appkit-chart", children: _jsxs("p", { className: "appkit-chart__error", children: ["Chart could not be rendered: ", result.reason] }) }));
|
|
167
|
+
}
|
|
168
|
+
return _jsx(Chart, { spec: result.spec, ...rest });
|
|
169
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Chart palette + chrome.
|
|
3
|
+
*
|
|
4
|
+
* ── 🛑 The custom properties are scoped to `.appkit-chart`, NOT `:root` ────
|
|
5
|
+
*
|
|
6
|
+
* Two reasons, and the second is the load-bearing one:
|
|
7
|
+
*
|
|
8
|
+
* 1. A chart palette is not a theme token. Nothing outside a chart should be
|
|
9
|
+
* able to consume `--chart-3`, and putting it on `:root` invites exactly
|
|
10
|
+
* that.
|
|
11
|
+
* 2. `audit-theme-drift` matches `:root { ... }` blocks in the token layer
|
|
12
|
+
* against the TypeScript theme presets, so a NEW `:root` custom property
|
|
13
|
+
* declared here is a drift finding waiting to happen. Scoping to the
|
|
14
|
+
* component sidesteps the question entirely.
|
|
15
|
+
*
|
|
16
|
+
* Reading them off the chart's OWN element (rather than `documentElement`) is
|
|
17
|
+
* also what makes the cascade do the work: a `[data-theme]` override below
|
|
18
|
+
* resolves correctly with no JS mode detection.
|
|
19
|
+
*
|
|
20
|
+
* ── 🛑 The dark steps are SELECTED, not flipped ───────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* Both palettes were run through the dataviz validator against the real app
|
|
23
|
+
* surfaces (#ffffff and #0f172a). The light set is the reference order. The
|
|
24
|
+
* dark set keeps that hue ORDER — because the order is what the adjacent-pair
|
|
25
|
+
* CVD check passes on — and re-steps only the four hues that fell outside the
|
|
26
|
+
* dark lightness band (L 0.48–0.67).
|
|
27
|
+
*
|
|
28
|
+
* Re-ordering instead was tried and was much worse: moving pink next to green
|
|
29
|
+
* produced an adjacent deuteranopia ΔE of 1.7, i.e. two series a red-green
|
|
30
|
+
* colourblind reader cannot tell apart at all.
|
|
31
|
+
*
|
|
32
|
+
* Light set carries a sub-3:1 contrast WARN on --chart-3/4/5. That is not
|
|
33
|
+
* dismissable: the component always renders a legend for ≥2 series and offers
|
|
34
|
+
* a table view, which is the required relief.
|
|
35
|
+
*
|
|
36
|
+
* If you change ANY value here, re-run:
|
|
37
|
+
* node scripts/validate_palette.js "<the 8 hexes>" --mode light --surface "#ffffff"
|
|
38
|
+
* node scripts/validate_palette.js "<the 8 hexes>" --mode dark --surface "#0f172a"
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
.appkit-chart {
|
|
42
|
+
--chart-1: #2a78d6;
|
|
43
|
+
--chart-2: #eb6834;
|
|
44
|
+
--chart-3: #1baf7a;
|
|
45
|
+
--chart-4: #eda100;
|
|
46
|
+
--chart-5: #e87ba4;
|
|
47
|
+
--chart-6: #008300;
|
|
48
|
+
--chart-7: #4a3aa7;
|
|
49
|
+
--chart-8: #e34948;
|
|
50
|
+
|
|
51
|
+
--chart-grid: rgba(113, 113, 122, 0.22);
|
|
52
|
+
--chart-axis: rgba(113, 113, 122, 0.55);
|
|
53
|
+
--chart-ink: var(--appkit-color-text, #18181b);
|
|
54
|
+
--chart-ink-muted: var(--appkit-color-text-muted, #52525b);
|
|
55
|
+
--chart-tooltip-bg: var(--appkit-color-surface-elevated, #ffffff);
|
|
56
|
+
--chart-tooltip-border: var(--appkit-color-border, rgba(113, 113, 122, 0.3));
|
|
57
|
+
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
gap: 0.75rem;
|
|
61
|
+
margin: 1.5rem 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@media (prefers-color-scheme: dark) {
|
|
65
|
+
.appkit-chart {
|
|
66
|
+
--chart-2: #E36430;
|
|
67
|
+
--chart-4: #C38600;
|
|
68
|
+
--chart-5: #D76691;
|
|
69
|
+
--chart-7: #6655CE;
|
|
70
|
+
--chart-grid: rgba(161, 161, 170, 0.2);
|
|
71
|
+
--chart-axis: rgba(161, 161, 170, 0.5);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* The viewer's explicit toggle must win over the media query in BOTH
|
|
77
|
+
* directions — `applyTheme` stamps `data-theme="<theme id>"` on <html>, and a
|
|
78
|
+
* light theme chosen on a dark-OS machine has to restore the light steps.
|
|
79
|
+
* `:root[data-theme]` is deliberately broad here: the attribute carries a theme
|
|
80
|
+
* ID (e.g. "default-light"), not the literal mode, so the mode-specific
|
|
81
|
+
* selectors below key off the id prefix the built-in themes use.
|
|
82
|
+
*/
|
|
83
|
+
:root[data-theme*="dark"] .appkit-chart {
|
|
84
|
+
--chart-2: #E36430;
|
|
85
|
+
--chart-4: #C38600;
|
|
86
|
+
--chart-5: #D76691;
|
|
87
|
+
--chart-7: #6655CE;
|
|
88
|
+
--chart-grid: rgba(161, 161, 170, 0.2);
|
|
89
|
+
--chart-axis: rgba(161, 161, 170, 0.5);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
:root[data-theme*="light"] .appkit-chart {
|
|
93
|
+
--chart-2: #eb6834;
|
|
94
|
+
--chart-4: #eda100;
|
|
95
|
+
--chart-5: #e87ba4;
|
|
96
|
+
--chart-7: #4a3aa7;
|
|
97
|
+
--chart-grid: rgba(113, 113, 122, 0.22);
|
|
98
|
+
--chart-axis: rgba(113, 113, 122, 0.55);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.appkit-chart__caption {
|
|
102
|
+
font-size: 0.8125rem;
|
|
103
|
+
line-height: 1.5;
|
|
104
|
+
color: var(--chart-ink-muted);
|
|
105
|
+
margin: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.appkit-chart__legend {
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-wrap: wrap;
|
|
111
|
+
gap: 0.5rem 1.25rem;
|
|
112
|
+
align-items: center;
|
|
113
|
+
font-size: 0.75rem;
|
|
114
|
+
color: var(--chart-ink-muted);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.appkit-chart__legend-swatch {
|
|
118
|
+
display: inline-block;
|
|
119
|
+
width: 11px;
|
|
120
|
+
height: 11px;
|
|
121
|
+
border-radius: 2px;
|
|
122
|
+
margin-right: 0.4rem;
|
|
123
|
+
vertical-align: -1px;
|
|
124
|
+
flex: 0 0 auto;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.appkit-chart__plot {
|
|
128
|
+
width: 100%;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Wide charts scroll in their own container so the page body never does. */
|
|
132
|
+
.appkit-chart__scroll {
|
|
133
|
+
overflow-x: auto;
|
|
134
|
+
width: 100%;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.appkit-chart__toolbar {
|
|
138
|
+
display: flex;
|
|
139
|
+
justify-content: flex-end;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.appkit-chart__toggle {
|
|
143
|
+
font: inherit;
|
|
144
|
+
font-size: 0.6875rem;
|
|
145
|
+
letter-spacing: 0.04em;
|
|
146
|
+
text-transform: uppercase;
|
|
147
|
+
color: var(--chart-ink-muted);
|
|
148
|
+
background: none;
|
|
149
|
+
border: 1px solid var(--chart-tooltip-border);
|
|
150
|
+
border-radius: 3px;
|
|
151
|
+
padding: 0.2rem 0.5rem;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.appkit-chart__toggle:hover {
|
|
156
|
+
color: var(--chart-ink);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.appkit-chart__toggle:focus-visible {
|
|
160
|
+
outline: 2px solid var(--appkit-color-focus-ring, #2a78d6);
|
|
161
|
+
outline-offset: 2px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.appkit-chart__table {
|
|
165
|
+
border-collapse: collapse;
|
|
166
|
+
width: 100%;
|
|
167
|
+
font-size: 0.8125rem;
|
|
168
|
+
font-variant-numeric: tabular-nums;
|
|
169
|
+
color: var(--chart-ink);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.appkit-chart__table th,
|
|
173
|
+
.appkit-chart__table td {
|
|
174
|
+
border-bottom: 1px solid var(--chart-grid);
|
|
175
|
+
padding: 0.4rem 0.6rem;
|
|
176
|
+
text-align: left;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.appkit-chart__table td + td,
|
|
180
|
+
.appkit-chart__table th + th {
|
|
181
|
+
text-align: right;
|
|
182
|
+
white-space: nowrap;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.appkit-chart__tooltip {
|
|
186
|
+
background: var(--chart-tooltip-bg);
|
|
187
|
+
border: 1px solid var(--chart-tooltip-border);
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
padding: 0.5rem 0.65rem;
|
|
190
|
+
font-size: 0.75rem;
|
|
191
|
+
color: var(--chart-ink);
|
|
192
|
+
font-variant-numeric: tabular-nums;
|
|
193
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.appkit-chart__tooltip-label {
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
margin-bottom: 0.25rem;
|
|
199
|
+
color: var(--chart-ink);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.appkit-chart__tooltip-row {
|
|
203
|
+
display: flex;
|
|
204
|
+
gap: 0.75rem;
|
|
205
|
+
justify-content: space-between;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.appkit-chart__error {
|
|
209
|
+
font-size: 0.8125rem;
|
|
210
|
+
color: var(--appkit-color-error, #e34948);
|
|
211
|
+
border: 1px solid var(--appkit-color-error, #e34948);
|
|
212
|
+
border-radius: 4px;
|
|
213
|
+
padding: 0.6rem 0.75rem;
|
|
214
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
|
|
3
|
+
/*
|
|
4
|
+
* Defining modules, not `../index` — Root Cause #18.
|
|
5
|
+
*
|
|
6
|
+
* The barrel re-exports THIS component, so importing three primitives back
|
|
7
|
+
* through it closed a two-module cycle (`audit-import-cycles` component 1).
|
|
8
|
+
* The deep paths below have one direction only.
|
|
9
|
+
*/
|
|
10
|
+
import { Div } from "./Div.js";
|
|
11
|
+
import { Stack } from "./Layout.js";
|
|
12
|
+
import { Text } from "./Typography.js";
|
|
4
13
|
import { DISCLAIMERS, } from "../../_internal/shared/content/disclaimers.js";
|
|
5
14
|
import { resolvePublishedCopy, resolvePublishedValues, } from "../../_internal/shared/content/published-values.js";
|
|
6
15
|
/**
|
|
@@ -26,14 +26,22 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
26
26
|
* @tag sideEffects:dom-measurement
|
|
27
27
|
*/
|
|
28
28
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
29
|
+
import { createPortal } from "react-dom";
|
|
29
30
|
import NextLink from "next/link.js";
|
|
30
31
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
31
32
|
import { Avatar } from "./Avatar.js";
|
|
32
33
|
import { Icon } from "./Icon.js";
|
|
33
34
|
import { Input } from "./Input.js";
|
|
34
35
|
import { useRowCollapse } from "./row-collapse.js";
|
|
36
|
+
import { MOBILE_QUERY } from "./toolbar-collapse.js";
|
|
37
|
+
import { useBottomChromeSlot } from "./bottom-chrome-slot.js";
|
|
38
|
+
import { useMediaQuery } from "../../react/hooks/useMediaQuery.js";
|
|
35
39
|
const UI_OVERFLOW_ROW = {
|
|
36
40
|
shell: "appkit-overflow-row",
|
|
41
|
+
/** The in-flow measuring box, once the control itself has been portalled away. */
|
|
42
|
+
shellDocked: "appkit-overflow-row--docked",
|
|
43
|
+
/** The portalled control, as its own row inside `BottomChrome`. */
|
|
44
|
+
dock: "appkit-overflow-row__dock",
|
|
37
45
|
row: "appkit-overflow-row__row",
|
|
38
46
|
pill: "appkit-overflow-row__pill",
|
|
39
47
|
pillActive: "appkit-overflow-row__pill--active",
|
|
@@ -77,11 +85,22 @@ export function OverflowRow({ items, ariaLabel, activeId, variant = "tabs", plac
|
|
|
77
85
|
const shellRef = useRef(null);
|
|
78
86
|
const rowRef = useRef(null);
|
|
79
87
|
const triggerRef = useRef(null);
|
|
88
|
+
/*
|
|
89
|
+
* The portalled control, when docked into the bottom bar. Needed only by the
|
|
90
|
+
* outside-click handler: a portal moves the node out of `shellRef`'s subtree
|
|
91
|
+
* in the DOM, so `shell.contains(target)` is FALSE for a click on the
|
|
92
|
+
* trigger itself — the dropdown would close on the very click that opened
|
|
93
|
+
* it, which reads as the control being dead.
|
|
94
|
+
*/
|
|
95
|
+
const dockRef = useRef(null);
|
|
80
96
|
const searchRef = useRef(null);
|
|
81
97
|
const listboxId = useId();
|
|
82
98
|
const [open, setOpen] = useState(false);
|
|
83
99
|
const [query, setQuery] = useState("");
|
|
84
100
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
101
|
+
// Both before any conditional return — the collapsed branch is one.
|
|
102
|
+
const bottomChromeSlot = useBottomChromeSlot();
|
|
103
|
+
const isMobile = useMediaQuery(MOBILE_QUERY);
|
|
85
104
|
const signature = useMemo(() => signatureOf(items), [items]);
|
|
86
105
|
const collapsed = useRowCollapse(shellRef, rowRef, signature, {
|
|
87
106
|
disabled: alwaysCollapsed,
|
|
@@ -92,7 +111,11 @@ export function OverflowRow({ items, ariaLabel, activeId, variant = "tabs", plac
|
|
|
92
111
|
if (!open)
|
|
93
112
|
return;
|
|
94
113
|
const onDown = (event) => {
|
|
95
|
-
|
|
114
|
+
const target = event.target;
|
|
115
|
+
const inShell = shellRef.current?.contains(target) ?? false;
|
|
116
|
+
// See `dockRef` — when docked, the control lives outside the shell.
|
|
117
|
+
const inDock = dockRef.current?.contains(target) ?? false;
|
|
118
|
+
if (!inShell && !inDock)
|
|
96
119
|
setOpen(false);
|
|
97
120
|
};
|
|
98
121
|
document.addEventListener("mousedown", onDown);
|
|
@@ -166,7 +189,7 @@ export function OverflowRow({ items, ariaLabel, activeId, variant = "tabs", plac
|
|
|
166
189
|
const hasAnyVisual = items.some((item) => item.imageUrl || item.iconName || item.emoji || item.dotTone);
|
|
167
190
|
// ── Collapsed: one searchable dropdown ──────────────────────────────────
|
|
168
191
|
if (collapsed) {
|
|
169
|
-
|
|
192
|
+
const dropdown = (_jsxs(_Fragment, { children: [_jsxs("button", { ref: triggerRef, type: "button", className: UI_OVERFLOW_ROW.trigger, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-label": ariaLabel, onClick: () => setOpen((prev) => !prev), onKeyDown: (event) => {
|
|
170
193
|
if (!open && (event.key === "ArrowDown" || event.key === "Enter")) {
|
|
171
194
|
event.preventDefault();
|
|
172
195
|
setOpen(true);
|
|
@@ -186,6 +209,35 @@ export function OverflowRow({ items, ariaLabel, activeId, variant = "tabs", plac
|
|
|
186
209
|
const body = (_jsxs(_Fragment, { children: [hasAnyVisual ? visualOf(item, "option") : null, _jsx("span", { className: UI_OVERFLOW_ROW.optionLabel, children: item.label }), typeof item.count === "number" && (_jsx("span", { className: UI_OVERFLOW_ROW.optionCount, children: item.count > 99 ? "99+" : item.count })), isSelected && (_jsx(Check, { className: `w-4 h-4 ${UI_OVERFLOW_ROW.check}`, "aria-hidden": "true" }))] }));
|
|
187
210
|
return item.href ? (_jsx(Link, { ...linkProps, id: `${listboxId}-${item.id}`, role: "option", "aria-selected": isSelected, href: item.href, className: classes, onMouseEnter: () => setActiveIndex(index), children: body }, item.id)) : (_jsx("button", { id: `${listboxId}-${item.id}`, role: "option", type: "button", "aria-selected": isSelected, disabled: item.disabled, className: classes, onClick: () => commit(item), onMouseEnter: () => setActiveIndex(index), children: body }, item.id));
|
|
188
211
|
})] })] }))] }));
|
|
212
|
+
/*
|
|
213
|
+
* ── Mobile: the collapsed row docks into the bottom bar ────────────────
|
|
214
|
+
*
|
|
215
|
+
* A row that has collapsed to a dropdown is no longer a row — it is one
|
|
216
|
+
* control, and on a phone the place for one control is the thumb zone,
|
|
217
|
+
* not a band competing with the page heading. It joins `BottomChrome` as
|
|
218
|
+
* its OWN row, so it stacks above a CTA bar instead of overlapping it,
|
|
219
|
+
* and the tier's single `ResizeObserver` adds its height to
|
|
220
|
+
* `--bottom-chrome-height` — which is what keeps page content from being
|
|
221
|
+
* hidden behind it on scroll, with no consumer doing anything.
|
|
222
|
+
*
|
|
223
|
+
* 🛑 `shellRef` STAYS IN PLACE, always. It is what `useRowCollapse`
|
|
224
|
+
* measures the available width against; portal it into the bottom bar and
|
|
225
|
+
* it would measure the VIEWPORT instead, read as "there is room now",
|
|
226
|
+
* expand, return to the narrow container, collapse, and oscillate forever.
|
|
227
|
+
* So the shell remains an empty in-flow box at the original position and
|
|
228
|
+
* only the CONTROL is portalled. The expanded branch below is unchanged
|
|
229
|
+
* and still renders the real row there.
|
|
230
|
+
*/
|
|
231
|
+
const docked = isMobile && !!bottomChromeSlot;
|
|
232
|
+
return (_jsx("div", { ref: shellRef, className: [
|
|
233
|
+
UI_OVERFLOW_ROW.shell,
|
|
234
|
+
docked ? UI_OVERFLOW_ROW.shellDocked : "",
|
|
235
|
+
className,
|
|
236
|
+
]
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.join(" "), "data-collapsed": "true", "data-docked": docked ? "true" : "false", children: docked
|
|
239
|
+
? createPortal(_jsx("div", { ref: dockRef, className: UI_OVERFLOW_ROW.dock, children: dropdown }), bottomChromeSlot)
|
|
240
|
+
: dropdown }));
|
|
189
241
|
}
|
|
190
242
|
// ── Expanded: the pill row ──────────────────────────────────────────────
|
|
191
243
|
return (_jsx("div", { ref: shellRef, className: [UI_OVERFLOW_ROW.shell, className].filter(Boolean).join(" "), "data-collapsed": "false", children: _jsx("div", { ref: rowRef, className: UI_OVERFLOW_ROW.row, role: variant === "tabs" ? "tablist" : undefined, "aria-label": ariaLabel, children: items.map((item) => {
|