@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
|
@@ -233,3 +233,41 @@
|
|
|
233
233
|
font-size: var(--appkit-text-sm);
|
|
234
234
|
color: var(--appkit-color-text-muted);
|
|
235
235
|
}
|
|
236
|
+
|
|
237
|
+
/* ── Docked: the collapsed control lives in the bottom bar on mobile ────────
|
|
238
|
+
*
|
|
239
|
+
* The shell stays in flow as a zero-height measuring box — `useRowCollapse`
|
|
240
|
+
* compares the row's cached natural width against THIS element, so it must
|
|
241
|
+
* keep the original container's width. Portalling it instead would make it
|
|
242
|
+
* measure the viewport, read as "there is room", expand, and oscillate.
|
|
243
|
+
*/
|
|
244
|
+
.appkit-overflow-row--docked {
|
|
245
|
+
height: 0;
|
|
246
|
+
/* Not `display:none`: a hidden box has no width, and width is the entire
|
|
247
|
+
input to the collapse decision. */
|
|
248
|
+
overflow: hidden;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.appkit-overflow-row__dock {
|
|
252
|
+
position: relative;
|
|
253
|
+
display: flex;
|
|
254
|
+
width: 100%;
|
|
255
|
+
pointer-events: auto;
|
|
256
|
+
padding: var(--appkit-space-2) var(--appkit-space-4);
|
|
257
|
+
background: var(--appkit-color-surface-elevated);
|
|
258
|
+
border-top: 1px solid var(--appkit-color-border);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.appkit-overflow-row__dock > .appkit-overflow-row__trigger {
|
|
262
|
+
width: 100%;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* 🛑 Opens UPWARD when docked. The default popover is `top: 100%`, which from
|
|
266
|
+
a bar at the bottom edge renders off-screen — the control would look dead.
|
|
267
|
+
`BottomChrome` deliberately sets no `overflow`, so the panel may escape the
|
|
268
|
+
tier; that is the documented contract its own header relies on. */
|
|
269
|
+
.appkit-overflow-row__dock .appkit-overflow-row__popover {
|
|
270
|
+
top: auto;
|
|
271
|
+
bottom: calc(100% + var(--appkit-space-1));
|
|
272
|
+
width: 100%;
|
|
273
|
+
}
|
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { parseChartAttr, CHART_KINDS } from "./chart-spec.js";
|
|
5
|
+
import { chartFigureHtml } from "../rich-text/chart-blocks.js";
|
|
6
|
+
/**
|
|
7
|
+
* What the Chart button drops into the editor to be edited.
|
|
8
|
+
*
|
|
9
|
+
* A filled, working example rather than an empty skeleton: an author editing
|
|
10
|
+
* real numbers into a chart that already renders can see what each field does,
|
|
11
|
+
* where one facing `{"kind":"","categories":[],"series":[]}` has to guess the
|
|
12
|
+
* shape before anything appears at all.
|
|
13
|
+
*/
|
|
14
|
+
const STARTER_CHART = JSON.stringify({
|
|
15
|
+
kind: "groupedBar",
|
|
16
|
+
title: "Untitled chart",
|
|
17
|
+
categories: ["First", "Second", "Third"],
|
|
18
|
+
series: [
|
|
19
|
+
{ label: "Series A", values: [3, 5, 4] },
|
|
20
|
+
{ label: "Series B", values: [4, 2, 6] },
|
|
21
|
+
],
|
|
22
|
+
unit: "",
|
|
23
|
+
caption: "",
|
|
24
|
+
}, null, 2);
|
|
25
|
+
/** The chart JSON editor shown under the toolbar, with live validation. */
|
|
26
|
+
function ChartDraftPanel({ draft, onDraftChange, onInsert, onCancel, }) {
|
|
27
|
+
const result = useMemo(() => parseChartAttr(draft), [draft]);
|
|
28
|
+
/*
|
|
29
|
+
* 🛑 Generated, not a literal. Two RichTextEditors on one page — a category
|
|
30
|
+
* form with a description and an SEO blurb, say — would otherwise emit the
|
|
31
|
+
* SAME hardcoded id twice, so the second panel's `htmlFor` and
|
|
32
|
+
* `aria-describedby` resolve to the FIRST one's textarea and error. A screen
|
|
33
|
+
* reader then reads the wrong field's error and clicking the label focuses
|
|
34
|
+
* the wrong box, which is exactly the class of bug the `ariaLabelledBy` note
|
|
35
|
+
* at the top of this file exists about.
|
|
36
|
+
*/
|
|
37
|
+
const panelId = useId();
|
|
38
|
+
const fieldId = `${panelId}-draft`;
|
|
39
|
+
const errorId = `${panelId}-error`;
|
|
40
|
+
return (_jsxs("div", { className: "appkit-rich-text-editor__chart border-b p-2 border-[var(--appkit-color-border)]", children: [_jsxs("label", { className: "mb-1 block text-xs font-medium text-[var(--appkit-color-text-muted)]", htmlFor: fieldId, children: ["Chart data \u2014 kinds: ", CHART_KINDS.join(", "), ". Each series needs one value per category."] }), _jsx("textarea", { id: fieldId, value: draft, onChange: (e) => onDraftChange(e.target.value), rows: 12, spellCheck: false, "aria-invalid": !result.ok, "aria-describedby": result.ok ? undefined : errorId, className: "w-full rounded border p-2 font-mono text-xs border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-input)] text-[var(--appkit-color-text)]" }), result.ok ? null : (_jsx("p", { id: errorId, role: "alert", className: "mt-1 text-xs text-[var(--appkit-color-error)]", children: result.reason })), _jsxs("div", { className: "mt-2 flex gap-2", children: [_jsx("button", { type: "button", onClick: onInsert, disabled: !result.ok, className: "rounded px-2 py-1 text-xs font-medium transition disabled:cursor-not-allowed disabled:opacity-50 bg-[var(--appkit-color-primary)] text-[var(--appkit-color-text-on-primary)]", children: "Insert chart" }), _jsx("button", { type: "button", onClick: onCancel, className: "rounded px-2 py-1 text-xs font-medium transition hover:bg-surface-hover text-[var(--appkit-color-text-muted)]", children: "Cancel" })] })] }));
|
|
41
|
+
}
|
|
4
42
|
// Matches exactly what this editor's own toolbar (bold/italic/underline/
|
|
5
43
|
// lists/link/clear-formatting) and typing can legitimately produce.
|
|
6
44
|
// Anything else — <script>, <img onerror=...>, <iframe>, <svg>, <style>,
|
|
@@ -10,6 +48,9 @@ import { useEffect, useMemo, useRef } from "react";
|
|
|
10
48
|
const ALLOWED_TAGS = new Set([
|
|
11
49
|
"B", "STRONG", "I", "EM", "U", "S", "STRIKE",
|
|
12
50
|
"UL", "OL", "LI", "A", "BR", "DIV", "SPAN", "P",
|
|
51
|
+
// FIGURE is here only because the toolbar's "Chart" button produces it.
|
|
52
|
+
// It carries no text content — see sanitizeAttribute for the data-chart rule.
|
|
53
|
+
"FIGURE",
|
|
13
54
|
]);
|
|
14
55
|
function isSafeHref(href) {
|
|
15
56
|
const trimmed = href.trim();
|
|
@@ -18,14 +59,33 @@ function isSafeHref(href) {
|
|
|
18
59
|
// relative), or "#" — reject everything else including javascript:.
|
|
19
60
|
return /^(https?:\/\/|mailto:|\/(?!\/)|#)/i.test(trimmed);
|
|
20
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* 🛑 This is the SAVE-time sanitiser, and it is a different, stricter one from
|
|
64
|
+
* `RichText.tsx`'s render-time `sanitiseHtml`. Both had to learn about
|
|
65
|
+
* `data-chart` — teaching only the renderer, as a first pass here did, means a
|
|
66
|
+
* chart inserted in the editor is silently deleted the moment it is saved,
|
|
67
|
+
* with the toolbar button appearing to work perfectly.
|
|
68
|
+
*
|
|
69
|
+
* The rule is stricter than the renderer's on purpose: the value is not merely
|
|
70
|
+
* allowed through, it must PARSE as a valid ChartSpec. Invalid JSON or a spec
|
|
71
|
+
* that fails validation loses the attribute, and the empty `<figure>` that
|
|
72
|
+
* remains is then dropped by `sanitizeElement`. So malformed chart data cannot
|
|
73
|
+
* be persisted at all, which is the right place to stop it — once stored, it
|
|
74
|
+
* is every future reader's problem.
|
|
75
|
+
*/
|
|
21
76
|
function sanitizeAttribute(el, attr) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
|
|
77
|
+
const name = attr.name.toLowerCase();
|
|
78
|
+
if (el.tagName === "A" && name === "href") {
|
|
79
|
+
if (!isSafeHref(attr.value))
|
|
80
|
+
el.setAttribute("href", "#");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (el.tagName === "FIGURE" && name === "data-chart") {
|
|
84
|
+
if (!parseChartAttr(attr.value).ok)
|
|
85
|
+
el.removeAttribute(attr.name);
|
|
25
86
|
return;
|
|
26
87
|
}
|
|
27
|
-
|
|
28
|
-
el.setAttribute("href", "#");
|
|
88
|
+
el.removeAttribute(attr.name);
|
|
29
89
|
}
|
|
30
90
|
function sanitizeElement(root, el) {
|
|
31
91
|
if (!ALLOWED_TAGS.has(el.tagName)) {
|
|
@@ -33,6 +93,14 @@ function sanitizeElement(root, el) {
|
|
|
33
93
|
return;
|
|
34
94
|
}
|
|
35
95
|
Array.from(el.attributes).forEach((attr) => sanitizeAttribute(el, attr));
|
|
96
|
+
// A <figure> is only legal here as a chart carrier. If the attribute did not
|
|
97
|
+
// survive validation above, what is left is an empty element with no text and
|
|
98
|
+
// no meaning — drop it rather than persist a blank block the author cannot
|
|
99
|
+
// see, select, or delete.
|
|
100
|
+
if (el.tagName === "FIGURE" && !el.hasAttribute("data-chart")) {
|
|
101
|
+
root.removeChild(el);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
36
104
|
sanitizeChildren(el);
|
|
37
105
|
}
|
|
38
106
|
function sanitizeChildren(root) {
|
|
@@ -55,6 +123,7 @@ function sanitizeRichTextHtml(html) {
|
|
|
55
123
|
}
|
|
56
124
|
export function RichTextEditor({ value, onChange, disabled = false, className = "", minHeightClassName = "min-h-[180px]", placeholder = "Write formatted content...", ariaLabelledBy, ariaLabel, }) {
|
|
57
125
|
const editorRef = useRef(null);
|
|
126
|
+
const [chartDraft, setChartDraft] = useState(null);
|
|
58
127
|
const emitChange = () => {
|
|
59
128
|
if (!editorRef.current)
|
|
60
129
|
return;
|
|
@@ -102,12 +171,34 @@ export function RichTextEditor({ value, onChange, disabled = false, className =
|
|
|
102
171
|
exec("createLink", href);
|
|
103
172
|
},
|
|
104
173
|
},
|
|
174
|
+
{
|
|
175
|
+
label: "Chart",
|
|
176
|
+
title: "Insert chart",
|
|
177
|
+
run: () => setChartDraft(STARTER_CHART),
|
|
178
|
+
},
|
|
105
179
|
{
|
|
106
180
|
label: "Clear",
|
|
107
181
|
title: "Clear formatting",
|
|
108
182
|
run: () => exec("removeFormat"),
|
|
109
183
|
},
|
|
110
184
|
], []);
|
|
185
|
+
/*
|
|
186
|
+
* `insertHTML` rather than building a node and splicing it in: the editor is
|
|
187
|
+
* a contenteditable and execCommand is already how every other toolbar action
|
|
188
|
+
* writes, so this keeps the caret and the undo stack behaving the same way.
|
|
189
|
+
* The inserted markup then goes straight through `emitChange` →
|
|
190
|
+
* `sanitizeRichTextHtml`, so the chart is validated on the way out exactly as
|
|
191
|
+
* a pasted one would be — the button gets no privileged path.
|
|
192
|
+
*/
|
|
193
|
+
const insertChart = () => {
|
|
194
|
+
if (chartDraft === null)
|
|
195
|
+
return;
|
|
196
|
+
const parsed = parseChartAttr(chartDraft);
|
|
197
|
+
if (!parsed.ok)
|
|
198
|
+
return;
|
|
199
|
+
exec("insertHTML", chartFigureHtml(parsed.spec));
|
|
200
|
+
setChartDraft(null);
|
|
201
|
+
};
|
|
111
202
|
useEffect(() => {
|
|
112
203
|
const editor = editorRef.current;
|
|
113
204
|
if (!editor)
|
|
@@ -120,7 +211,7 @@ export function RichTextEditor({ value, onChange, disabled = false, className =
|
|
|
120
211
|
editor.innerHTML = safeValue;
|
|
121
212
|
}
|
|
122
213
|
}, [value]);
|
|
123
|
-
return (_jsxs("div", { className: `appkit-rich-text-editor rounded-lg border border-zinc-200 bg-[var(--appkit-color-surface)] border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${className}`, "data-section": "richtexteditor-div-582", children: [_jsx("div", { className: "appkit-rich-text-editor__toolbar flex flex-wrap gap-1 border-b p-2 border-[var(--appkit-color-border)]", "data-section": "richtexteditor-div-583", children: toolbarActions.map((action) => (_jsx("button", { type: "button", title: action.title, onClick: action.run, disabled: disabled, className: "appkit-rich-text-editor__btn rounded px-2 py-1 text-xs font-medium transition hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50 text-[var(--appkit-color-text-muted)]", children: action.label }, action.title))) }), _jsx("div", { ref: editorRef,
|
|
214
|
+
return (_jsxs("div", { className: `appkit-rich-text-editor rounded-lg border border-zinc-200 bg-[var(--appkit-color-surface)] border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${className}`, "data-section": "richtexteditor-div-582", children: [_jsx("div", { className: "appkit-rich-text-editor__toolbar flex flex-wrap gap-1 border-b p-2 border-[var(--appkit-color-border)]", "data-section": "richtexteditor-div-583", children: toolbarActions.map((action) => (_jsx("button", { type: "button", title: action.title, onClick: action.run, disabled: disabled, className: "appkit-rich-text-editor__btn rounded px-2 py-1 text-xs font-medium transition hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50 text-[var(--appkit-color-text-muted)]", children: action.label }, action.title))) }), chartDraft !== null ? (_jsx(ChartDraftPanel, { draft: chartDraft, onDraftChange: setChartDraft, onInsert: insertChart, onCancel: () => setChartDraft(null) })) : null, _jsx("div", { ref: editorRef,
|
|
124
215
|
/*
|
|
125
216
|
* 🛑 `role` and `aria-label` USED TO BE DECLARED HERE TOO, hardcoded
|
|
126
217
|
* to "Rich text editor" — and when the labelling props were added
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export declare const CHART_KINDS: readonly ["bar", "groupedBar", "line", "area"];
|
|
2
|
+
export type ChartKind = (typeof CHART_KINDS)[number];
|
|
3
|
+
/**
|
|
4
|
+
* 🛑 Eight, and a ninth is never a generated hue.
|
|
5
|
+
*
|
|
6
|
+
* The categorical palette is a fixed, validated ORDER of eight steps, checked
|
|
7
|
+
* for colourblind separation between adjacent pairs. A 9th colour generated at
|
|
8
|
+
* runtime cannot be validated, so a 9-series chart is REFUSED and the author is
|
|
9
|
+
* told to fold the tail into "Other" or split into small multiples.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CHART_MAX_SERIES = 8;
|
|
12
|
+
export interface ChartSeries {
|
|
13
|
+
label: string;
|
|
14
|
+
values: number[];
|
|
15
|
+
}
|
|
16
|
+
export interface ChartSpec {
|
|
17
|
+
kind: ChartKind;
|
|
18
|
+
/** X-axis categories. `series[].values` must be exactly this long. */
|
|
19
|
+
categories: string[];
|
|
20
|
+
series: ChartSeries[];
|
|
21
|
+
/** Appended to every value in tooltips and the table view. */
|
|
22
|
+
unit?: string;
|
|
23
|
+
/** Rendered under the plot. */
|
|
24
|
+
caption?: string;
|
|
25
|
+
/** Accessible name. Falls back to the caption. */
|
|
26
|
+
title?: string;
|
|
27
|
+
}
|
|
28
|
+
export type ChartSpecResult = {
|
|
29
|
+
ok: true;
|
|
30
|
+
spec: ChartSpec;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Validate an untrusted object into a `ChartSpec`.
|
|
37
|
+
*
|
|
38
|
+
* 🛑 Returns a REASON rather than throwing or silently returning null. This
|
|
39
|
+
* parses author-supplied JSON out of a `data-chart` attribute, and an author
|
|
40
|
+
* who mistyped needs to be told which rule they broke — a chart that quietly
|
|
41
|
+
* renders nothing is the failure mode this codebase keeps paying for.
|
|
42
|
+
*
|
|
43
|
+
* 🛑 There is deliberately no second y-axis in the shape. A dual-axis chart
|
|
44
|
+
* invites comparing two incommensurable scales on one plot; two measures mean
|
|
45
|
+
* two charts, small multiples, or indexing to a common base.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseChartSpec(input: unknown): ChartSpecResult;
|
|
48
|
+
/**
|
|
49
|
+
* Parse the JSON string carried by a `data-chart` attribute.
|
|
50
|
+
*
|
|
51
|
+
* 🛑 The normalised error is deliberately NOT surfaced to the caller. The
|
|
52
|
+
* thrown value is a `SyntaxError` from `JSON.parse` carrying nothing an author
|
|
53
|
+
* can act on beyond "it is not JSON" — which the returned reason already says,
|
|
54
|
+
* in their language, at the place they will read it. Returning the engine's
|
|
55
|
+
* message instead is how server text reaches a user (Root Cause #86). Malformed
|
|
56
|
+
* authored content is an expected input here, not an incident to report.
|
|
57
|
+
*
|
|
58
|
+
* `normalizeError` is still called, because that is what `audit-catch-normalize`
|
|
59
|
+
* actually accepts: its header also lists "return immediately" as sufficient,
|
|
60
|
+
* but the implementation only tests for `normalizeError(var)` or `throw var`
|
|
61
|
+
* (audit-catch-normalize.mjs:210-213), so a bare early return is reported.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseChartAttr(json: string): ChartSpecResult;
|
|
64
|
+
/**
|
|
65
|
+
* How many series the palette can colour. Exposed so a caller can decide to
|
|
66
|
+
* fold a tail into "Other" BEFORE validation rejects the whole chart.
|
|
67
|
+
*/
|
|
68
|
+
export declare function exceedsSeriesLimit(count: number): boolean;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chart DATA contract — pure, and deliberately separate from `<Chart>`.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Why this is not in Chart.tsx
|
|
5
|
+
*
|
|
6
|
+
* `Chart.tsx` is `"use client"` and statically reaches recharts. Anything that
|
|
7
|
+
* imports it inherits both. But the validator needs to be callable from places
|
|
8
|
+
* that are neither:
|
|
9
|
+
*
|
|
10
|
+
* - `chart-blocks.ts`, which must stay SSR-safe because `RichTextRenderer`
|
|
11
|
+
* is a Server Component;
|
|
12
|
+
* - a route handler or a seed script validating authored content before it
|
|
13
|
+
* is stored, where pulling in recharts would be absurd.
|
|
14
|
+
*
|
|
15
|
+
* Keeping the types and the parser here means a server-side caller gets the
|
|
16
|
+
* contract without the renderer — the same split as Root Cause #24's lesson
|
|
17
|
+
* about a file that mixed Node-only crypto with client-safe display helpers.
|
|
18
|
+
*
|
|
19
|
+
* No React, no DOM, no recharts.
|
|
20
|
+
*/
|
|
21
|
+
import { normalizeError } from "../../errors/normalize.js";
|
|
22
|
+
export const CHART_KINDS = ["bar", "groupedBar", "line", "area"];
|
|
23
|
+
/**
|
|
24
|
+
* 🛑 Eight, and a ninth is never a generated hue.
|
|
25
|
+
*
|
|
26
|
+
* The categorical palette is a fixed, validated ORDER of eight steps, checked
|
|
27
|
+
* for colourblind separation between adjacent pairs. A 9th colour generated at
|
|
28
|
+
* runtime cannot be validated, so a 9-series chart is REFUSED and the author is
|
|
29
|
+
* told to fold the tail into "Other" or split into small multiples.
|
|
30
|
+
*/
|
|
31
|
+
export const CHART_MAX_SERIES = 8;
|
|
32
|
+
/**
|
|
33
|
+
* Validate an untrusted object into a `ChartSpec`.
|
|
34
|
+
*
|
|
35
|
+
* 🛑 Returns a REASON rather than throwing or silently returning null. This
|
|
36
|
+
* parses author-supplied JSON out of a `data-chart` attribute, and an author
|
|
37
|
+
* who mistyped needs to be told which rule they broke — a chart that quietly
|
|
38
|
+
* renders nothing is the failure mode this codebase keeps paying for.
|
|
39
|
+
*
|
|
40
|
+
* 🛑 There is deliberately no second y-axis in the shape. A dual-axis chart
|
|
41
|
+
* invites comparing two incommensurable scales on one plot; two measures mean
|
|
42
|
+
* two charts, small multiples, or indexing to a common base.
|
|
43
|
+
*/
|
|
44
|
+
export function parseChartSpec(input) {
|
|
45
|
+
if (typeof input !== "object" || input === null) {
|
|
46
|
+
return { ok: false, reason: "Chart data must be an object." };
|
|
47
|
+
}
|
|
48
|
+
const o = input;
|
|
49
|
+
const kind = o.kind;
|
|
50
|
+
if (typeof kind !== "string" || !CHART_KINDS.includes(kind)) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: `Chart "kind" must be one of ${CHART_KINDS.join(", ")}.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(o.categories) ||
|
|
57
|
+
o.categories.length === 0 ||
|
|
58
|
+
!o.categories.every((c) => typeof c === "string" && c.length > 0)) {
|
|
59
|
+
return { ok: false, reason: 'Chart "categories" must be a non-empty array of labels.' };
|
|
60
|
+
}
|
|
61
|
+
const categories = o.categories;
|
|
62
|
+
if (!Array.isArray(o.series) || o.series.length === 0) {
|
|
63
|
+
return { ok: false, reason: 'Chart "series" must be a non-empty array.' };
|
|
64
|
+
}
|
|
65
|
+
if (o.series.length > CHART_MAX_SERIES) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
reason: `A chart carries at most ${CHART_MAX_SERIES} series (got ${o.series.length}). ` +
|
|
69
|
+
`Fold the smallest into an "Other" series, or split into separate charts — ` +
|
|
70
|
+
`a 9th colour cannot be checked for colourblind separation.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const series = [];
|
|
74
|
+
for (const [i, raw] of o.series.entries()) {
|
|
75
|
+
if (typeof raw !== "object" || raw === null) {
|
|
76
|
+
return { ok: false, reason: `Series ${i + 1} must be an object.` };
|
|
77
|
+
}
|
|
78
|
+
const s = raw;
|
|
79
|
+
if (typeof s.label !== "string" || s.label.length === 0) {
|
|
80
|
+
return { ok: false, reason: `Series ${i + 1} needs a non-empty "label".` };
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(s.values) ||
|
|
83
|
+
!s.values.every((v) => typeof v === "number" && Number.isFinite(v))) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: `Series "${s.label}" — "values" must be an array of finite numbers.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (s.values.length !== categories.length) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: `Series "${s.label}" has ${s.values.length} values but there are ` +
|
|
93
|
+
`${categories.length} categories. They must match one-to-one.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
series.push({ label: s.label, values: s.values });
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
spec: {
|
|
101
|
+
kind: kind,
|
|
102
|
+
categories,
|
|
103
|
+
series,
|
|
104
|
+
...(typeof o.unit === "string" ? { unit: o.unit } : {}),
|
|
105
|
+
...(typeof o.caption === "string" ? { caption: o.caption } : {}),
|
|
106
|
+
...(typeof o.title === "string" ? { title: o.title } : {}),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Parse the JSON string carried by a `data-chart` attribute.
|
|
112
|
+
*
|
|
113
|
+
* 🛑 The normalised error is deliberately NOT surfaced to the caller. The
|
|
114
|
+
* thrown value is a `SyntaxError` from `JSON.parse` carrying nothing an author
|
|
115
|
+
* can act on beyond "it is not JSON" — which the returned reason already says,
|
|
116
|
+
* in their language, at the place they will read it. Returning the engine's
|
|
117
|
+
* message instead is how server text reaches a user (Root Cause #86). Malformed
|
|
118
|
+
* authored content is an expected input here, not an incident to report.
|
|
119
|
+
*
|
|
120
|
+
* `normalizeError` is still called, because that is what `audit-catch-normalize`
|
|
121
|
+
* actually accepts: its header also lists "return immediately" as sufficient,
|
|
122
|
+
* but the implementation only tests for `normalizeError(var)` or `throw var`
|
|
123
|
+
* (audit-catch-normalize.mjs:210-213), so a bare early return is reported.
|
|
124
|
+
*/
|
|
125
|
+
export function parseChartAttr(json) {
|
|
126
|
+
try {
|
|
127
|
+
return parseChartSpec(JSON.parse(json));
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
void normalizeError(err);
|
|
131
|
+
return { ok: false, reason: "Chart data is not valid JSON." };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* How many series the palette can colour. Exposed so a caller can decide to
|
|
136
|
+
* fold a tail into "Other" BEFORE validation rejects the whole chart.
|
|
137
|
+
*/
|
|
138
|
+
export function exceedsSeriesLimit(count) {
|
|
139
|
+
return count > CHART_MAX_SERIES;
|
|
140
|
+
}
|
|
@@ -226,7 +226,7 @@ export declare const PADDING_PRESETS: {
|
|
|
226
226
|
readonly "content-banner": "py-[var(--appkit-space-10)] md:py-[var(--appkit-space-12)] lg:py-[var(--appkit-space-16)]";
|
|
227
227
|
};
|
|
228
228
|
export type PaddingPresetKey = PaddingKey;
|
|
229
|
-
export type XPaddingKey = "none" | "x-xs" | "x-sm" | "x-md" | "x-5" | "x-lg" | "x-xl" | "x-sm-md" | "x-sm-lg-md" | "x-md-lg" | "x-md-xl" | "x-md-2xl" | "x-page" | "l-xl";
|
|
229
|
+
export type XPaddingKey = "none" | "x-xs" | "x-sm" | "x-md" | "x-5" | "x-lg" | "x-xl" | "x-sm-md" | "x-sm-lg-md" | "x-md-lg" | "x-md-xl" | "x-md-2xl" | "x-page" | "l-md" | "l-xl";
|
|
230
230
|
type YPaddingKey = "none" | "y-2xs" | "y-2xs-tall" | "y-xs" | "y-xs-tall" | "y-sm" | "y-sm-tall" | "y-md" | "y-md-lg" | "y-lg" | "y-xl" | "y-2xl" | "y-3xl" | "y-4xl" | "y-5xl" | "y-6xl" | "y-2-5xl" | "t-2xs" | "b-2xs" | "t-xs" | "b-xs" | "t-sm" | "b-sm" | "t-md" | "b-md" | "b-md-lg" | "b-lg" | "t-md-b-lg";
|
|
231
231
|
export declare const X_ONLY_MAP: Record<XPaddingKey, string>;
|
|
232
232
|
export type OverflowKey = "auto" | "hidden" | "scroll" | "visible" | "x-auto" | "x-hidden" | "y-auto" | "y-hidden";
|
|
@@ -232,6 +232,15 @@ export const X_ONLY_MAP = {
|
|
|
232
232
|
// selection-checkbox gutter).
|
|
233
233
|
/** Asymmetric: `pl-8` — reserves a left gutter (e.g. for an overlaid selection checkbox). */
|
|
234
234
|
"l-xl": "pl-[var(--appkit-space-8)]",
|
|
235
|
+
/**
|
|
236
|
+
* Asymmetric: `pl-4` — indents a nested block under its own heading.
|
|
237
|
+
*
|
|
238
|
+
* The grouped order-item rows (admin and seller) are the motivating pair:
|
|
239
|
+
* a group header followed by its member lines, where the indent is the only
|
|
240
|
+
* thing expressing the nesting. `x-md` would pad the right edge too, which
|
|
241
|
+
* is not what an indent means.
|
|
242
|
+
*/
|
|
243
|
+
"l-md": "pl-[var(--appkit-space-4)]",
|
|
235
244
|
};
|
|
236
245
|
const Y_ONLY_MAP = {
|
|
237
246
|
none: "",
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/** Below this the toolbar defaults to collapsed. Matches the `lg` boundary
|
|
2
|
+
* that both toolbars already use to switch between their mobile and desktop
|
|
3
|
+
* rows, so the default flips at the same width the layout does. */
|
|
4
|
+
/**
|
|
5
|
+
* Below `lg` — the complement of `DESKTOP_QUERY` in `features/layout`.
|
|
6
|
+
*
|
|
7
|
+
* Exported so `OverflowRow` can dock its collapsed control into the bottom bar
|
|
8
|
+
* at exactly the width the tab bar exists at. It is NOT imported from
|
|
9
|
+
* `features/layout`: this is `ui/`, and a `ui → features` edge inverts the
|
|
10
|
+
* layering and is how `audit-import-cycles` regressions start. One literal in
|
|
11
|
+
* the lower layer, shared, beats two that can drift apart.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MOBILE_QUERY = "(max-width: 1023px)";
|
|
1
14
|
/**
|
|
2
15
|
* Tri-state on purpose: `null` means "the user has never chosen", which is
|
|
3
16
|
* what lets the viewport supply the default. A plain boolean cannot express
|
|
@@ -19,7 +19,16 @@ const STORAGE_PREFIX = "appkit:sticky-toolbar-collapsed:";
|
|
|
19
19
|
/** Below this the toolbar defaults to collapsed. Matches the `lg` boundary
|
|
20
20
|
* that both toolbars already use to switch between their mobile and desktop
|
|
21
21
|
* rows, so the default flips at the same width the layout does. */
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Below `lg` — the complement of `DESKTOP_QUERY` in `features/layout`.
|
|
24
|
+
*
|
|
25
|
+
* Exported so `OverflowRow` can dock its collapsed control into the bottom bar
|
|
26
|
+
* at exactly the width the tab bar exists at. It is NOT imported from
|
|
27
|
+
* `features/layout`: this is `ui/`, and a `ui → features` edge inverts the
|
|
28
|
+
* layering and is how `audit-import-cycles` regressions start. One literal in
|
|
29
|
+
* the lower layer, shared, beats two that can drift apart.
|
|
30
|
+
*/
|
|
31
|
+
export const MOBILE_QUERY = "(max-width: 1023px)";
|
|
23
32
|
/**
|
|
24
33
|
* Tri-state on purpose: `null` means "the user has never chosen", which is
|
|
25
34
|
* what lets the viewport supply the default. A plain boolean cannot express
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -95,6 +95,10 @@ export type { SliderProps } from "./components/Slider";
|
|
|
95
95
|
export { Slider } from "./components/Slider";
|
|
96
96
|
export type { CheckboxProps } from "./components/Checkbox";
|
|
97
97
|
export { Checkbox } from "./components/Checkbox";
|
|
98
|
+
export type { ChartProps } from "./components/Chart";
|
|
99
|
+
export { Chart, ChartFromJson } from "./components/Chart";
|
|
100
|
+
export type { ChartKind, ChartSeries, ChartSpec, ChartSpecResult, } from "./components/chart-spec";
|
|
101
|
+
export { CHART_KINDS, CHART_MAX_SERIES, exceedsSeriesLimit, parseChartAttr, parseChartSpec, } from "./components/chart-spec";
|
|
98
102
|
export type { RadioGroupProps, RadioOption, RadioItemProps } from "./components/Radio";
|
|
99
103
|
export { RadioGroup, RadioItem } from "./components/Radio";
|
|
100
104
|
export type { ToggleProps } from "./components/Toggle";
|
|
@@ -211,6 +215,8 @@ export type { RichTextProps } from "./rich-text/RichText";
|
|
|
211
215
|
export { RichText } from "./rich-text/RichText";
|
|
212
216
|
export type { RichTextRendererProps } from "./rich-text/RichTextRenderer";
|
|
213
217
|
export { RichTextRenderer } from "./rich-text/RichTextRenderer";
|
|
218
|
+
export type { RichTextSegment } from "./rich-text/chart-blocks";
|
|
219
|
+
export { chartFigureHtml, hasChartBlock, splitChartBlocks, } from "./rich-text/chart-blocks";
|
|
214
220
|
export type { RichTextEditorProps } from "./components/RichTextEditor";
|
|
215
221
|
export { RichTextEditor } from "./components/RichTextEditor";
|
|
216
222
|
export type { ListingLayoutProps, ListingLayoutLabels, } from "./components/ListingLayout";
|
package/dist/ui/index.js
CHANGED
|
@@ -82,6 +82,8 @@ export { DateInput, DateRangeInput } from "./components/DateInput.js";
|
|
|
82
82
|
export { Textarea } from "./components/Textarea.js";
|
|
83
83
|
export { Slider } from "./components/Slider.js";
|
|
84
84
|
export { Checkbox } from "./components/Checkbox.js";
|
|
85
|
+
export { Chart, ChartFromJson } from "./components/Chart.js";
|
|
86
|
+
export { CHART_KINDS, CHART_MAX_SERIES, exceedsSeriesLimit, parseChartAttr, parseChartSpec, } from "./components/chart-spec.js";
|
|
85
87
|
export { RadioGroup, RadioItem } from "./components/Radio.js";
|
|
86
88
|
export { Toggle } from "./components/Toggle.js";
|
|
87
89
|
export { QuantityStepper } from "./components/QuantityStepper.js";
|
|
@@ -148,6 +150,7 @@ export { classNames, mergeTailwindClasses } from "./style.helper.js";
|
|
|
148
150
|
export { SideModal } from "./components/SideModal.js";
|
|
149
151
|
export { RichText } from "./rich-text/RichText.js";
|
|
150
152
|
export { RichTextRenderer } from "./rich-text/RichTextRenderer.js";
|
|
153
|
+
export { chartFigureHtml, hasChartBlock, splitChartBlocks, } from "./rich-text/chart-blocks.js";
|
|
151
154
|
export { RichTextEditor } from "./components/RichTextEditor.js";
|
|
152
155
|
export { ListingLayout } from "./components/ListingLayout.js";
|
|
153
156
|
export { SlottedListingView } from "./components/SlottedListingView.js";
|
|
@@ -29,6 +29,15 @@ export interface RichTextProps {
|
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
31
|
highlightCode?: (code: string, lang: string) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Render `<figure data-chart>` placeholders as live charts. Default true.
|
|
34
|
+
*
|
|
35
|
+
* Set `false` for untrusted user-generated content where an author should not
|
|
36
|
+
* be able to mount a component at all — the placeholder then renders as the
|
|
37
|
+
* empty `<figure>` it is. The JSON is still validated either way; this is
|
|
38
|
+
* about whether a chart is an affordance on that surface.
|
|
39
|
+
*/
|
|
40
|
+
charts?: boolean;
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
* RichText renders sanitised HTML from a CMS / ProseMirror output.
|
|
@@ -42,4 +51,4 @@ export interface RichTextProps {
|
|
|
42
51
|
* <RichText html={desc} copyableCode />
|
|
43
52
|
* ```
|
|
44
53
|
*/
|
|
45
|
-
export declare function RichText({ html, className, proseClass, copyableCode, highlightCode, }: RichTextProps): import("react").JSX.Element;
|
|
54
|
+
export declare function RichText({ html, className, proseClass, copyableCode, highlightCode, charts, }: RichTextProps): import("react").JSX.Element;
|
|
@@ -3,6 +3,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
import { Div } from "../components/Div.js";
|
|
5
5
|
import { normalizeRichTextHtml } from "../../utils/string.formatter.js";
|
|
6
|
+
import { splitChartBlocks } from "./chart-blocks.js";
|
|
7
|
+
import { ChartFromJson } from "../components/Chart.js";
|
|
6
8
|
// --- Allowed HTML tags (safe subset — no script/object; iframe allowed for YouTube only) -------
|
|
7
9
|
const ALLOWED_TAGS = new Set([
|
|
8
10
|
"p",
|
|
@@ -52,6 +54,19 @@ const ALLOWED_ATTRS = {
|
|
|
52
54
|
"*": ["class", "id", "lang", "dir", "aria-label", "role", "tabindex"],
|
|
53
55
|
a: ["href", "title", "target", "rel"],
|
|
54
56
|
img: ["src", "alt", "width", "height", "loading", "data-align"],
|
|
57
|
+
/*
|
|
58
|
+
* `data-chart` carries a JSON ChartSpec that the renderer hydrates into
|
|
59
|
+
* <Chart>. Safe to allow because it is DATA, never markup: it is read with
|
|
60
|
+
* `getAttribute`, run through `JSON.parse`, and then validated field-by-field
|
|
61
|
+
* by `parseChartSpec`, which accepts only a fixed `kind` enum, string labels
|
|
62
|
+
* and finite numbers. Nothing in it reaches `innerHTML`, so a script payload
|
|
63
|
+
* smuggled into the string fails validation and renders as an error message.
|
|
64
|
+
*
|
|
65
|
+
* 🛑 It must stay on `figure` only. Allowing it via "*" would let it ride on
|
|
66
|
+
* an <a> or <iframe>, where the surrounding element's own attack surface is
|
|
67
|
+
* the problem rather than the attribute.
|
|
68
|
+
*/
|
|
69
|
+
figure: ["data-chart"],
|
|
55
70
|
td: ["colspan", "rowspan"],
|
|
56
71
|
th: ["colspan", "rowspan", "scope"],
|
|
57
72
|
ol: ["type", "start"],
|
|
@@ -167,7 +182,7 @@ function attachCopyButtons(container) {
|
|
|
167
182
|
* <RichText html={desc} copyableCode />
|
|
168
183
|
* ```
|
|
169
184
|
*/
|
|
170
|
-
export function RichText({ html, className = "", proseClass = "prose max-w-none dark:prose-invert", copyableCode = false, highlightCode, }) {
|
|
185
|
+
export function RichText({ html, className = "", proseClass = "prose max-w-none dark:prose-invert", copyableCode = false, highlightCode, charts = true, }) {
|
|
171
186
|
const safe = useMemo(() => {
|
|
172
187
|
const sanitized = sanitiseHtml(normalizeRichTextHtml(html));
|
|
173
188
|
if (!highlightCode || typeof document === "undefined")
|
|
@@ -186,5 +201,21 @@ export function RichText({ html, className = "", proseClass = "prose max-w-none
|
|
|
186
201
|
attachCopyButtons(el);
|
|
187
202
|
}
|
|
188
203
|
};
|
|
189
|
-
|
|
204
|
+
/*
|
|
205
|
+
* Split AFTER sanitising, never before.
|
|
206
|
+
*
|
|
207
|
+
* The sanitiser round-trips through `DOMParser` and re-serialises, which
|
|
208
|
+
* rewrites the single-quoted `data-chart='{"k":1}'` we emit into
|
|
209
|
+
* double-quoted `data-chart="{"k":1}"`. `splitChartBlocks` handles
|
|
210
|
+
* both spellings precisely because of this step — splitting first would
|
|
211
|
+
* leave chart segments that the sanitiser never got to inspect.
|
|
212
|
+
*/
|
|
213
|
+
const segments = useMemo(() => (charts ? splitChartBlocks(safe) : [{ type: "html", html: safe }]), [safe, charts]);
|
|
214
|
+
// Fast path: no charts. Byte-identical to the pre-chart render.
|
|
215
|
+
if (segments.length === 1 && segments[0].type === "html") {
|
|
216
|
+
return (_jsx(Div, { ref: ref, className: `appkit-rich-text ${proseClass} ${className}`, dangerouslySetInnerHTML: { __html: segments[0].html } }));
|
|
217
|
+
}
|
|
218
|
+
return (_jsx(Div, { ref: ref, className: `appkit-rich-text ${proseClass} ${className}`, children: segments.map((seg, i) => seg.type === "html" ? (_jsx("div", { className: "appkit-rich-text__seg", dangerouslySetInnerHTML: { __html: seg.html } }, `h${i}`)) : (
|
|
219
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
220
|
+
_jsx(ChartFromJson, { json: seg.json }, `c${i}`))) }));
|
|
190
221
|
}
|
|
@@ -25,6 +25,24 @@
|
|
|
25
25
|
border-bottom-color: var(--appkit-color-border);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/*
|
|
29
|
+
* The chart-draft panel the toolbar's "Chart" button opens.
|
|
30
|
+
*
|
|
31
|
+
* It sits between the toolbar and the editable area, so it needs the same
|
|
32
|
+
* recessive surface the toolbar has — without it the panel reads as part of the
|
|
33
|
+
* document being edited rather than as editor chrome, which is misleading when
|
|
34
|
+
* the thing inside it is a block of JSON.
|
|
35
|
+
*/
|
|
36
|
+
.appkit-rich-text-editor__chart {
|
|
37
|
+
background-color: var(--appkit-color-surface-raised);
|
|
38
|
+
border-bottom: 1px solid var(--appkit-color-border);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.dark .appkit-rich-text-editor__chart {
|
|
42
|
+
background-color: var(--appkit-color-slate-900);
|
|
43
|
+
border-bottom-color: var(--appkit-color-border);
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
.appkit-rich-text-editor__btn {
|
|
29
47
|
border: 1px solid var(--appkit-color-border);
|
|
30
48
|
color: var(--appkit-color-zinc-700);
|
|
@@ -331,3 +349,20 @@
|
|
|
331
349
|
.dark .appkit-rich-text del {
|
|
332
350
|
color: var(--appkit-color-zinc-400);
|
|
333
351
|
}
|
|
352
|
+
|
|
353
|
+
/*
|
|
354
|
+
* Chart-block segment wrapper.
|
|
355
|
+
*
|
|
356
|
+
* `RichTextRenderer` has to split the HTML around `<figure data-chart>` and
|
|
357
|
+
* each resulting run needs an element to carry `dangerouslySetInnerHTML` —
|
|
358
|
+
* there is no way to inject HTML without one. `display: contents` removes that
|
|
359
|
+
* element from the box tree so the runs lay out exactly as they did when they
|
|
360
|
+
* were one `innerHTML` dump.
|
|
361
|
+
*
|
|
362
|
+
* 🛑 Without this, `prose`'s adjacency and first/last-child margin rules
|
|
363
|
+
* restart at every chart, so paragraphs either side of a chart space
|
|
364
|
+
* differently from paragraphs anywhere else on the page.
|
|
365
|
+
*/
|
|
366
|
+
.appkit-rich-text__seg {
|
|
367
|
+
display: contents;
|
|
368
|
+
}
|