@mohasinac/appkit 4.29.1 → 4.30.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/_internal/client/features/layout/SidebarCollapseToggle.js +1 -1
- package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
- package/dist/_internal/shared/features/blog/schema.js +2 -1
- package/dist/_internal/shared/features/brands/schema.d.ts +2 -2
- package/dist/_internal/shared/features/brands/schema.js +3 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +3 -3
- package/dist/_internal/shared/features/categories/bundle-schemas.js +2 -1
- package/dist/_internal/shared/features/events/schema.d.ts +2 -2
- package/dist/_internal/shared/features/events/schema.js +2 -1
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/schema.js +3 -2
- package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
- package/dist/_internal/shared/features/reviews/schema.js +2 -1
- package/dist/_internal/shared/types/action-result.d.ts +0 -2
- package/dist/client/api/surface-error.d.ts +9 -0
- package/dist/client/api/surface-error.js +29 -3
- package/dist/client/api/useApiMutation.d.ts +10 -0
- package/dist/client/api/useApiMutation.js +2 -1
- package/dist/core/server-action.d.ts +3 -14
- package/dist/core/server-action.js +11 -2
- package/dist/errors/error-envelope.d.ts +73 -0
- package/dist/errors/error-envelope.js +96 -0
- package/dist/errors/error-handler.js +14 -7
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/components/UserSidebar.js +2 -2
- package/dist/features/account/components/UserSupportView.js +2 -6
- package/dist/features/account/hooks/useProfile.d.ts +6 -0
- package/dist/features/account/hooks/useProfile.js +1 -0
- package/dist/features/admin/components/AdminAdEditorView.js +1 -3
- package/dist/features/admin/components/AdminAddressClustersView.js +1 -3
- package/dist/features/admin/components/AdminAddressEditorView.js +2 -4
- package/dist/features/admin/components/AdminAddressesView.js +1 -3
- package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -3
- package/dist/features/admin/components/AdminBidsView.js +1 -3
- package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
- package/dist/features/admin/components/AdminBrandEditorView.js +4 -5
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +1 -3
- package/dist/features/admin/components/AdminCategoryEditorView.js +2 -4
- package/dist/features/admin/components/AdminContactEditorView.js +1 -3
- package/dist/features/admin/components/AdminContactView.js +2 -6
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -4
- package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -9
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -4
- package/dist/features/admin/components/AdminFeatureEditorView.js +0 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +2 -4
- package/dist/features/admin/components/AdminNavEditorView.js +1 -3
- package/dist/features/admin/components/AdminNavigationView.js +3 -3
- package/dist/features/admin/components/AdminNewsletterView.js +1 -3
- package/dist/features/admin/components/AdminNotificationsView.js +2 -6
- package/dist/features/admin/components/AdminOffersView.js +1 -3
- package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
- package/dist/features/admin/components/AdminPaymentClustersView.js +1 -3
- package/dist/features/admin/components/AdminPaymentMethodsView.js +1 -3
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +2 -4
- package/dist/features/admin/components/AdminProductEditorView.js +2 -4
- package/dist/features/admin/components/AdminReturnRequestsView.js +2 -6
- package/dist/features/admin/components/AdminReviewsView.js +2 -6
- package/dist/features/admin/components/AdminScammerEditorView.js +2 -6
- package/dist/features/admin/components/AdminSectionsView.js +2 -6
- package/dist/features/admin/components/AdminSessionsView.js +1 -3
- package/dist/features/admin/components/AdminSidebar.js +2 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +1 -3
- package/dist/features/admin/components/AdminStoresView.js +2 -6
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -6
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +7 -21
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -4
- package/dist/features/admin/components/AdminUserEditorView.js +6 -18
- package/dist/features/admin/components/AdminUsersView.js +2 -6
- package/dist/features/admin/components/BrandQuickCreateForm.js +2 -4
- package/dist/features/admin/components/CategoryQuickCreateForm.js +2 -4
- package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
- package/dist/features/auth/components/ForgotPasswordView.js +1 -1
- package/dist/features/auth/components/LoginForm.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +1 -1
- package/dist/features/auth/components/ResetPasswordView.js +1 -1
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/repository/session.repository.js +13 -1
- package/dist/features/auth/repository/token.repository.d.ts +25 -2
- package/dist/features/auth/repository/token.repository.js +26 -18
- package/dist/features/blog/actions/blog-actions.d.ts +14 -14
- package/dist/features/blog/actions/blog-actions.js +6 -8
- package/dist/features/blog/schemas/index.d.ts +1 -1
- package/dist/features/blog/schemas/index.js +2 -4
- package/dist/features/categories/api/[id]/route.js +2 -1
- package/dist/features/categories/api/route.js +2 -1
- package/dist/features/events/schemas/index.d.ts +1 -1
- package/dist/features/events/schemas/index.js +2 -4
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/filters/AsyncFacetSection.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +1 -1
- package/dist/features/filters/RangeFilter.js +1 -1
- package/dist/features/filters/SwitchFilter.js +1 -1
- package/dist/features/homepage/api/carousel/[id]/route.js +4 -3
- package/dist/features/homepage/api/carousel/route.js +4 -3
- package/dist/features/homepage/api/route.js +3 -2
- package/dist/features/homepage/components/GoogleReviewsSection.js +1 -1
- package/dist/features/homepage/components/HeroBanner.js +1 -1
- package/dist/features/homepage/components/HeroCarousel.js +1 -1
- package/dist/features/homepage/components/SocialPostCard.js +1 -1
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +4 -4
- package/dist/features/layout/BottomNavbar.js +2 -2
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +24 -12
- package/dist/features/media/AvatarUpload.js +28 -10
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/modals/ImageEditor.js +1 -1
- package/dist/features/products/components/MakeOfferButton.js +1 -1
- package/dist/features/reviews/api/[id]/route.js +3 -2
- package/dist/features/reviews/api/route.js +3 -2
- package/dist/features/search/components/Search.js +2 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +3 -3
- package/dist/features/seller/components/SellerSidebar.js +2 -2
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +1 -1
- package/dist/features/stores/repository/store.repository.d.ts +21 -0
- package/dist/features/stores/repository/store.repository.js +32 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -2
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -7
- package/dist/next/api/api-response.d.ts +29 -12
- package/dist/next/api/api-response.js +9 -8
- package/dist/next/api/apiHandler.d.ts +6 -1
- package/dist/next/api/apiHandler.js +13 -1
- package/dist/next/api/routeHandler.js +6 -6
- package/dist/next/middleware/pii-scrubber.js +5 -2
- package/dist/providers/db-firebase/base.repository.d.ts +14 -0
- package/dist/providers/db-firebase/base.repository.js +22 -0
- package/dist/providers/db-firebase/sieve.d.ts +14 -1
- package/dist/providers/db-firebase/sieve.js +48 -4
- package/dist/security/browser.d.ts +1 -1
- package/dist/security/browser.js +1 -1
- package/dist/security/index.d.ts +2 -2
- package/dist/security/index.js +2 -2
- package/dist/security/pii-encrypt.d.ts +12 -10
- package/dist/security/pii-encrypt.js +91 -29
- package/dist/security/pii-mask.d.ts +39 -1
- package/dist/security/pii-mask.js +33 -4
- package/dist/security/pii-redact.d.ts +0 -13
- package/dist/security/pii-redact.js +3 -1
- package/dist/security/pii-schemas.d.ts +0 -6
- package/dist/security/pii-schemas.js +29 -6
- package/dist/security/settings-encryption.js +31 -3
- package/dist/seed/test-utils.js +6 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/ui/DataTable.js +2 -2
- package/dist/ui/components/CollapsibleSection.js +1 -1
- package/dist/ui/components/ImageGallery.js +1 -1
- package/dist/ui/components/ListingLayout.js +1 -1
- package/dist/ui/forms/FormShell.d.ts +0 -19
- package/dist/ui/forms/FormShell.js +4 -23
- package/dist/ui/forms/apply-zod-issues.d.ts +34 -0
- package/dist/ui/forms/apply-zod-issues.js +38 -0
- package/dist/ui/forms/index.d.ts +2 -1
- package/dist/ui/forms/index.js +2 -1
- package/dist/ui/icons/icon-registry.d.ts +8 -0
- package/dist/ui/icons/icon-registry.js +13 -1
- package/dist/utils/action-response.d.ts +1 -1
- package/dist/utils/media-url.d.ts +49 -0
- package/dist/utils/media-url.js +72 -1
- package/dist/validation/schemas.d.ts +15 -2
- package/dist/validation/schemas.js +18 -22
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +21 -6
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
// crypto is a Node.js built-in. Use require() to keep it out of the static
|
|
2
2
|
// import graph so Next.js Edge bundler does not warn about this file.
|
|
3
3
|
function nodeCrypto() { return require("crypto"); }
|
|
4
|
+
import { ENC_PREFIX, encEnvelopeKind } from "./pii-mask";
|
|
4
5
|
const ALGORITHM = "aes-256-gcm";
|
|
5
6
|
const IV_BYTES = 12;
|
|
6
|
-
|
|
7
|
+
// ONE declaration of the prefix, shared with the PII system that also uses it.
|
|
8
|
+
// Two independent `"enc:v1:"` literals is what let the two formats drift into
|
|
9
|
+
// looking identical to every "is this encrypted" check in the codebase.
|
|
10
|
+
const PREFIX = ENC_PREFIX;
|
|
11
|
+
/**
|
|
12
|
+
* Strip a trailing CR/LF (literal or escaped) and surrounding whitespace —
|
|
13
|
+
* including a leading BOM, which `String.prototype.trim` removes.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors `normalizePiiSecretValue` in pii-encrypt.ts rather than importing
|
|
16
|
+
* it: that module is the Node-crypto half of the PII system and this file must
|
|
17
|
+
* not depend on it. The two are four lines and must stay identical; the
|
|
18
|
+
* UNNORMALISED_HEX_KEY rule in audit-pii-crypto is what keeps them honest.
|
|
19
|
+
*/
|
|
20
|
+
function normalizeSecretEnvValue(raw) {
|
|
21
|
+
return (raw ?? "").replace(/(?:\\r|\\n|\r|\n)+$/g, "").trim();
|
|
22
|
+
}
|
|
7
23
|
function getMasterKey() {
|
|
8
|
-
|
|
24
|
+
// Normalise before measuring AND before Buffer.from. A BOM or trailing CR
|
|
25
|
+
// makes `length !== 64` true, so this threw a confusing "must be 64
|
|
26
|
+
// characters" on a key that looks correct in the dashboard — and had the
|
|
27
|
+
// length check not been there, Buffer.from would have truncated at the first
|
|
28
|
+
// non-hex byte and produced a silently WRONG key instead.
|
|
29
|
+
const hex = normalizeSecretEnvValue(process.env.SETTINGS_ENCRYPTION_KEY);
|
|
9
30
|
if (!hex || hex.length !== 64) {
|
|
10
31
|
throw new Error("SETTINGS_ENCRYPTION_KEY must be a 64-character hex string (32 bytes).");
|
|
11
32
|
}
|
|
@@ -27,6 +48,13 @@ export function decryptSecret(encrypted) {
|
|
|
27
48
|
if (!encrypted.startsWith(PREFIX)) {
|
|
28
49
|
return encrypted;
|
|
29
50
|
}
|
|
51
|
+
// Name the actual problem. A PII-encrypted value reaching here used to throw
|
|
52
|
+
// "expected 3 parts", which describes the symptom and not the cause — it is
|
|
53
|
+
// the wrong KEY and the wrong FORMAT, not a corrupt value.
|
|
54
|
+
const kind = encEnvelopeKind(encrypted);
|
|
55
|
+
if (kind === "pii") {
|
|
56
|
+
throw new Error("Value is PII-encrypted (PII_ENCRYPTION_KEY, colon-separated), not settings-encrypted — decryptSecret cannot read it");
|
|
57
|
+
}
|
|
30
58
|
const parts = encrypted.slice(PREFIX.length).split(".");
|
|
31
59
|
if (parts.length !== 3) {
|
|
32
60
|
throw new Error("Invalid encrypted value - expected 3 parts");
|
|
@@ -41,7 +69,7 @@ export function decryptSecret(encrypted) {
|
|
|
41
69
|
]).toString("utf8");
|
|
42
70
|
}
|
|
43
71
|
export function isSecretEncrypted(value) {
|
|
44
|
-
return value
|
|
72
|
+
return encEnvelopeKind(value) === "settings";
|
|
45
73
|
}
|
|
46
74
|
export function maskSecret(value) {
|
|
47
75
|
if (!value || value.length < 12) {
|
package/dist/seed/test-utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runSeed } from "./runner";
|
|
2
2
|
import { encryptPiiFields, decryptPiiFields, ENC_PREFIX } from "../security/pii-encrypt";
|
|
3
|
+
import { hasEncPrefix } from "../security/pii-mask";
|
|
3
4
|
import { DEFAULT_CATEGORIES } from "./defaults/categories";
|
|
4
5
|
import { DEFAULT_FAQS } from "./defaults/faqs";
|
|
5
6
|
import { makeProduct } from "./factories/product.factory";
|
|
@@ -24,8 +25,11 @@ export async function assertPiiRoundTrip(doc, piiFields) {
|
|
|
24
25
|
const encrypted = encryptPiiFields(doc, piiFields);
|
|
25
26
|
for (const field of piiFields) {
|
|
26
27
|
const val = encrypted[field];
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// `hasEncPrefix`, not a bare startsWith: the assertion is "this is not
|
|
29
|
+
// cleartext", which both crypto formats satisfy. And the value NEVER goes
|
|
30
|
+
// in the message — this helper runs over real fixture PII.
|
|
31
|
+
if (!hasEncPrefix(val)) {
|
|
32
|
+
throw new Error(`assertPiiRoundTrip: field "${String(field)}" was NOT encrypted (got ${val.length} chars, no ${ENC_PREFIX} prefix).`);
|
|
29
33
|
}
|
|
30
34
|
// Verify blind index was written
|
|
31
35
|
const indexKey = `${String(field)}Index`;
|
package/dist/server.d.ts
CHANGED
|
@@ -462,7 +462,7 @@ export { encryptPii } from "./security/index";
|
|
|
462
462
|
export { decryptPii } from "./security/index";
|
|
463
463
|
export { isPiiEncrypted } from "./security/index";
|
|
464
464
|
export { piiBlindIndex } from "./security/index";
|
|
465
|
-
export {
|
|
465
|
+
export { piiIndicesFor } from "./security/index";
|
|
466
466
|
export { getPiiConfigError } from "./security/index";
|
|
467
467
|
export { encryptShippingAddress } from "./security/index";
|
|
468
468
|
export { decryptShippingAddress } from "./security/index";
|
package/dist/server.js
CHANGED
|
@@ -1294,7 +1294,7 @@ export { encryptPii } from "./security/index";
|
|
|
1294
1294
|
export { decryptPii } from "./security/index";
|
|
1295
1295
|
export { isPiiEncrypted } from "./security/index";
|
|
1296
1296
|
export { piiBlindIndex } from "./security/index";
|
|
1297
|
-
export {
|
|
1297
|
+
export { piiIndicesFor } from "./security/index";
|
|
1298
1298
|
export { getPiiConfigError } from "./security/index";
|
|
1299
1299
|
export { encryptShippingAddress } from "./security/index";
|
|
1300
1300
|
export { decryptShippingAddress } from "./security/index";
|
package/dist/ui/DataTable.js
CHANGED
|
@@ -108,7 +108,7 @@ pageSize: pageSizeProp, stickyHeader: stickyHeaderProp, striped: stripedProp, sh
|
|
|
108
108
|
const renderViewToggle = () => {
|
|
109
109
|
if (!showViewToggle)
|
|
110
110
|
return null;
|
|
111
|
-
return (_jsxs(Div, { className: "appkit-data-table__view-toggle", role: "toolbar", "aria-label": "View mode", children: [showTableView && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => handleViewModeChange("table"), "aria-label": tableView, "aria-pressed": activeViewMode === "table", className: `appkit-data-table__view-btn ${activeViewMode === "table" ? "appkit-data-table__view-btn--active" : ""} hidden sm:flex`, children: _jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 10h18M3 6h18M3 14h18M3 18h18" }) }) })), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => handleViewModeChange("grid"), "aria-label": gridView, "aria-pressed": activeViewMode === "grid", className: `appkit-data-table__view-btn ${activeViewMode === "grid" ? "appkit-data-table__view-btn--active" : ""}`, children: _jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" }) }) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => handleViewModeChange("list"), "aria-label": listView, "aria-pressed": activeViewMode === "list", className: `appkit-data-table__view-btn ${activeViewMode === "list" ? "appkit-data-table__view-btn--active" : ""}`, children: _jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" }) }) })] }));
|
|
111
|
+
return (_jsxs(Div, { className: "appkit-data-table__view-toggle", role: "toolbar", "aria-label": "View mode", children: [showTableView && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => handleViewModeChange("table"), "aria-label": tableView, "aria-pressed": activeViewMode === "table", className: `appkit-data-table__view-btn ${activeViewMode === "table" ? "appkit-data-table__view-btn--active" : ""} hidden sm:flex`, children: _jsx("svg", { className: "w-4 h-4 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 10h18M3 6h18M3 14h18M3 18h18" }) }) })), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => handleViewModeChange("grid"), "aria-label": gridView, "aria-pressed": activeViewMode === "grid", className: `appkit-data-table__view-btn ${activeViewMode === "grid" ? "appkit-data-table__view-btn--active" : ""}`, children: _jsx("svg", { className: "w-4 h-4 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" }) }) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => handleViewModeChange("list"), "aria-label": listView, "aria-pressed": activeViewMode === "list", className: `appkit-data-table__view-btn ${activeViewMode === "list" ? "appkit-data-table__view-btn--active" : ""}`, children: _jsx("svg", { className: "w-4 h-4 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" }) }) })] }));
|
|
112
112
|
};
|
|
113
113
|
// --- Card views (grid / list) ------------------------------------------------
|
|
114
114
|
const renderCardGrid = (mode) => {
|
|
@@ -158,5 +158,5 @@ function SelectableCard({ id, selectable, selected, onToggle, children, listMode
|
|
|
158
158
|
selected
|
|
159
159
|
? "border-2 border-primary bg-primary"
|
|
160
160
|
: "border-2 border-zinc-500 dark:border-slate-400 bg-transparent group-hover:border-primary",
|
|
161
|
-
].join(" "), checked: selected, onChange: (e) => onToggle(id, e.target.checked), "aria-label": "Select item" }), selected && (_jsx("svg", { className: "absolute inset-0 m-auto h-3 w-3 text-white pointer-events-none", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 3, d: "M5 13l4 4L19 7" }) }))] }) }) }), selected && (_jsx(Div, { className: "absolute inset-0 z-[var(--appkit-z-raised)] ring-2 ring-primary ring-offset-0 pointer-events-none", rounded: "xl", "aria-hidden": "true" })), children] }));
|
|
161
|
+
].join(" "), checked: selected, onChange: (e) => onToggle(id, e.target.checked), "aria-label": "Select item" }), selected && (_jsx("svg", { className: "absolute inset-0 m-auto h-3 w-3 text-white pointer-events-none shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 3, d: "M5 13l4 4L19 7" }) }))] }) }) }), selected && (_jsx(Div, { className: "absolute inset-0 z-[var(--appkit-z-raised)] ring-2 ring-primary ring-offset-0 pointer-events-none", rounded: "xl", "aria-hidden": "true" })), children] }));
|
|
162
162
|
}
|
|
@@ -9,7 +9,7 @@ import { Collapse } from "./Motion";
|
|
|
9
9
|
* server-side merge/default-collapsed semantics). Purely presentational
|
|
10
10
|
* here: isCollapsed/onToggle are always controlled by the caller. */
|
|
11
11
|
export function CollapsibleSection({ title, isCollapsed, onToggle, renderHeaderExtra, keepMounted = false, id, className = "", children, }) {
|
|
12
|
-
return (_jsxs(Div, { id: id, className: className, rounded: "xl", border: "default", surface: "default", children: [_jsxs(Row, { align: "center", justify: "between", paddingX: "x-md", paddingY: "y-sm", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: onToggle, paddingX: "none", paddingY: "none", rounded: "none", className: "min-w-0 flex-1", "aria-expanded": !isCollapsed, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { weight: "semibold", size: "sm", children: title }), _jsx("svg", { className: `w-4 h-4 transition-transform duration-150 ${isCollapsed ? "" : "rotate-180"}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) })] }) }), renderHeaderExtra && _jsx(Div, { className: "ml-3 shrink-0", children: renderHeaderExtra() })] }), keepMounted ? (
|
|
12
|
+
return (_jsxs(Div, { id: id, className: className, rounded: "xl", border: "default", surface: "default", children: [_jsxs(Row, { align: "center", justify: "between", paddingX: "x-md", paddingY: "y-sm", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: onToggle, paddingX: "none", paddingY: "none", rounded: "none", className: "min-w-0 flex-1", "aria-expanded": !isCollapsed, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { weight: "semibold", size: "sm", children: title }), _jsx("svg", { className: `w-4 h-4 transition-transform duration-150 ${isCollapsed ? "" : "rotate-180"} shrink-0`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) })] }) }), renderHeaderExtra && _jsx(Div, { className: "ml-3 shrink-0", children: renderHeaderExtra() })] }), keepMounted ? (
|
|
13
13
|
// No <Collapse>: it unmounts on close, which would discard uncommitted
|
|
14
14
|
// child state (see `keepMounted` docs). `hidden` keeps the subtree
|
|
15
15
|
// mounted and removes it from the a11y tree at the same time.
|
|
@@ -107,7 +107,7 @@ export default function ImageGallery({ images, initialIndex = 0, showThumbnails
|
|
|
107
107
|
? "zoom-out"
|
|
108
108
|
: "zoom-in"
|
|
109
109
|
: "default",
|
|
110
|
-
} }), images.length > 1 && !isZoomed && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: goToPrevious, disabled: currentIndex === 0, className: `appkit-image-gallery__nav-btn appkit-image-gallery__nav-btn--prev ${themed.bgSecondary} ${themed.textPrimary}`, "aria-label": "Previous image", children: _jsx("svg", { className: "w-5 h-5 md:w-6 md:h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { variant: "ghost", onClick: goToNext, disabled: currentIndex === images.length - 1, className: `appkit-image-gallery__nav-btn appkit-image-gallery__nav-btn--next ${themed.bgSecondary} ${themed.textPrimary}`, "aria-label": "Next image", children: _jsx("svg", { className: "w-5 h-5 md:w-6 md:h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) }) })] })), images.length > 1 && (_jsxs("div", { className: `appkit-image-gallery__counter ${themed.bgSecondary} ${themed.textPrimary}`, "data-section": "imagegallery-div-519", children: [currentIndex + 1, " / ", images.length] })), allowZoom && isZoomed && (_jsxs("div", { className: `appkit-image-gallery__zoom-indicator ${themed.bgSecondary} ${themed.textPrimary}`, "data-section": "imagegallery-div-520", children: [Math.round(scale * 100), "%"] }))] }), showCaptions && currentImage.caption && (_jsx("div", { className: `appkit-image-gallery__caption ${themed.border}`, "data-section": "imagegallery-div-521", children: _jsx(Text, { className: themed.textSecondary, children: currentImage.caption }) }))] }), showThumbnails && images.length > 1 && (_jsx(HorizontalScroller, { snapToItems: true, className: "mt-4 pb-2", children: images.map((image, index) => (_jsx(Button, { variant: "ghost", onClick: () => goToIndex(index), className: `appkit-image-gallery__thumbnail ${index === currentIndex
|
|
110
|
+
} }), images.length > 1 && !isZoomed && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: goToPrevious, disabled: currentIndex === 0, className: `appkit-image-gallery__nav-btn appkit-image-gallery__nav-btn--prev ${themed.bgSecondary} ${themed.textPrimary}`, "aria-label": "Previous image", children: _jsx("svg", { className: "w-5 h-5 md:w-6 md:h-6 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { variant: "ghost", onClick: goToNext, disabled: currentIndex === images.length - 1, className: `appkit-image-gallery__nav-btn appkit-image-gallery__nav-btn--next ${themed.bgSecondary} ${themed.textPrimary}`, "aria-label": "Next image", children: _jsx("svg", { className: "w-5 h-5 md:w-6 md:h-6 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) }) })] })), images.length > 1 && (_jsxs("div", { className: `appkit-image-gallery__counter ${themed.bgSecondary} ${themed.textPrimary}`, "data-section": "imagegallery-div-519", children: [currentIndex + 1, " / ", images.length] })), allowZoom && isZoomed && (_jsxs("div", { className: `appkit-image-gallery__zoom-indicator ${themed.bgSecondary} ${themed.textPrimary}`, "data-section": "imagegallery-div-520", children: [Math.round(scale * 100), "%"] }))] }), showCaptions && currentImage.caption && (_jsx("div", { className: `appkit-image-gallery__caption ${themed.border}`, "data-section": "imagegallery-div-521", children: _jsx(Text, { className: themed.textSecondary, children: currentImage.caption }) }))] }), showThumbnails && images.length > 1 && (_jsx(HorizontalScroller, { snapToItems: true, className: "mt-4 pb-2", children: images.map((image, index) => (_jsx(Button, { variant: "ghost", onClick: () => goToIndex(index), className: `appkit-image-gallery__thumbnail ${index === currentIndex
|
|
111
111
|
? "appkit-image-gallery__thumbnail--active"
|
|
112
112
|
: themed.border}`, "aria-label": `View image ${index + 1}`, children: _jsx(MediaImage, { src: image.thumbnail || image.src, alt: image.alt, size: "thumbnail", className: "w-full h-full object-cover" }) }, index))) })), _jsx("div", { className: `appkit-image-gallery__help ${themed.textMuted}`, "data-section": "imagegallery-div-522", children: allowZoom
|
|
113
113
|
? "Swipe to navigate • Double tap to zoom"
|
|
@@ -106,5 +106,5 @@ export function ListingLayout({ headerSlot, statusTabsSlot, filterContent, filte
|
|
|
106
106
|
: l.applyFilters })] }), children: filterContent })), overlays] }));
|
|
107
107
|
}
|
|
108
108
|
function FilterIcon() {
|
|
109
|
-
return (_jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 4a1 1 0 011-1h16a1 1 0 011 1v2a1 1 0 01-.293.707L13 13.414V19a1 1 0 01-.553.894l-4 2A1 1 0 017 21v-7.586L3.293 6.707A1 1 0 013 6V4z" }) }));
|
|
109
|
+
return (_jsx("svg", { className: "w-4 h-4 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 4a1 1 0 011-1h16a1 1 0 011 1v2a1 1 0 01-.293.707L13 13.414V19a1 1 0 01-.553.894l-4 2A1 1 0 017 21v-7.586L3.293 6.707A1 1 0 013 6V4z" }) }));
|
|
110
110
|
}
|
|
@@ -94,25 +94,6 @@ export interface UseFormShellStateResult {
|
|
|
94
94
|
*/
|
|
95
95
|
markSubmitAttempted: () => void;
|
|
96
96
|
}
|
|
97
|
-
/**
|
|
98
|
-
* `applyZodIssues` — pipe every issue from a Zod safeParse failure into the
|
|
99
|
-
* FormShellContext error map. Use after `schema.safeParse(values)` returns
|
|
100
|
-
* `success: false` to surface inline errors on `<FieldInput>` etc.
|
|
101
|
-
*
|
|
102
|
-
* Keys are the full dotted/indexed path (`"video.duration"`, `"images.2"`),
|
|
103
|
-
* not just the top-level segment — a plain `issue.path[0]` key would collapse
|
|
104
|
-
* every nested issue under one object onto the same map entry, silently
|
|
105
|
-
* dropping all but the last. Top-level-only fields (the common case) are
|
|
106
|
-
* unaffected: a single-segment path joins to the same string as before.
|
|
107
|
-
*
|
|
108
|
-
* Root-level `.refine()` issues (empty `path: []`, no owning field) are still
|
|
109
|
-
* skipped — there is no field to attach them to, and no schema in this
|
|
110
|
-
* codebase currently relies on one being surfaced this way.
|
|
111
|
-
*/
|
|
112
|
-
export declare function applyZodIssues(issues: ReadonlyArray<{
|
|
113
|
-
path: ReadonlyArray<PropertyKey>;
|
|
114
|
-
message: string;
|
|
115
|
-
}>, setFieldError: (name: string, error: string | null) => void): void;
|
|
116
97
|
/**
|
|
117
98
|
* `useFormShellState(schema?, nav?)` — caller-owned form state with optional
|
|
118
99
|
* Zod validation. When a schema is supplied, `validate(values)` runs it and
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { applyZodIssues } from "./apply-zod-issues";
|
|
4
5
|
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
5
6
|
export const FormShellContext = createContext(null);
|
|
6
7
|
export function useFormShell() {
|
|
@@ -9,29 +10,9 @@ export function useFormShell() {
|
|
|
9
10
|
throw new Error("useFormShell must be used inside <FormShell>");
|
|
10
11
|
return ctx;
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* `success: false` to surface inline errors on `<FieldInput>` etc.
|
|
16
|
-
*
|
|
17
|
-
* Keys are the full dotted/indexed path (`"video.duration"`, `"images.2"`),
|
|
18
|
-
* not just the top-level segment — a plain `issue.path[0]` key would collapse
|
|
19
|
-
* every nested issue under one object onto the same map entry, silently
|
|
20
|
-
* dropping all but the last. Top-level-only fields (the common case) are
|
|
21
|
-
* unaffected: a single-segment path joins to the same string as before.
|
|
22
|
-
*
|
|
23
|
-
* Root-level `.refine()` issues (empty `path: []`, no owning field) are still
|
|
24
|
-
* skipped — there is no field to attach them to, and no schema in this
|
|
25
|
-
* codebase currently relies on one being surfaced this way.
|
|
26
|
-
*/
|
|
27
|
-
export function applyZodIssues(issues, setFieldError) {
|
|
28
|
-
for (const issue of issues) {
|
|
29
|
-
if (!issue.path || issue.path.length === 0)
|
|
30
|
-
continue;
|
|
31
|
-
const key = issue.path.map(String).join(".");
|
|
32
|
-
setFieldError(key, issue.message);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
13
|
+
// `applyZodIssues` lives in ./apply-zod-issues — `surface-error.ts`, which is
|
|
14
|
+
// deliberately React-free, needs it to route server-side validation failures
|
|
15
|
+
// to inline field errors and cannot import this "use client" module.
|
|
35
16
|
/**
|
|
36
17
|
* `useFormShellState(schema?, nav?)` — caller-owned form state with optional
|
|
37
18
|
* Zod validation. When a schema is supplied, `validate(values)` runs it and
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `applyZodIssues` — pipe every issue from a validation failure into a form's
|
|
3
|
+
* error map, so they render inline on `<FieldInput>` etc. rather than as a
|
|
4
|
+
* toast.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS ITS OWN FILE
|
|
7
|
+
* ------------------------
|
|
8
|
+
* It used to live in `FormShell.tsx`, which is a `"use client"` React module.
|
|
9
|
+
* `client/api/surface-error.ts` needs this function to route server-side
|
|
10
|
+
* validation failures to inline field errors, and that file is deliberately
|
|
11
|
+
* hook-free and React-free (see its header) so it can be called from outside a
|
|
12
|
+
* hook boundary — e.g. inside Button's non-hook click wrapper. Importing
|
|
13
|
+
* FormShell there would have dragged React and the whole form runtime into it.
|
|
14
|
+
*
|
|
15
|
+
* This is a MOVE, not a barrel re-export: `FormShell.tsx` imports it from here.
|
|
16
|
+
*
|
|
17
|
+
* Keys are the full dotted/indexed path (`"video.duration"`, `"images.2"`),
|
|
18
|
+
* not just the top-level segment — a plain `issue.path[0]` key would collapse
|
|
19
|
+
* every nested issue under one object onto the same map entry, silently
|
|
20
|
+
* dropping all but the last. Top-level-only fields (the common case) are
|
|
21
|
+
* unaffected: a single-segment path joins to the same string as before.
|
|
22
|
+
*
|
|
23
|
+
* Root-level `.refine()` issues (empty `path: []`, no owning field) are still
|
|
24
|
+
* skipped — there is no field to attach them to, so the caller must surface
|
|
25
|
+
* those some other way (they fall through to a toast in `surfaceError`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function applyZodIssues(issues: ReadonlyArray<{
|
|
28
|
+
path?: ReadonlyArray<PropertyKey>;
|
|
29
|
+
message: string;
|
|
30
|
+
}>, setFieldError: (name: string, error: string | null) => void): void;
|
|
31
|
+
/** True when at least one issue carries a field path we can attach it to. */
|
|
32
|
+
export declare function hasAttachableIssue(issues: ReadonlyArray<{
|
|
33
|
+
path?: ReadonlyArray<PropertyKey>;
|
|
34
|
+
}> | undefined): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `applyZodIssues` — pipe every issue from a validation failure into a form's
|
|
3
|
+
* error map, so they render inline on `<FieldInput>` etc. rather than as a
|
|
4
|
+
* toast.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS IS ITS OWN FILE
|
|
7
|
+
* ------------------------
|
|
8
|
+
* It used to live in `FormShell.tsx`, which is a `"use client"` React module.
|
|
9
|
+
* `client/api/surface-error.ts` needs this function to route server-side
|
|
10
|
+
* validation failures to inline field errors, and that file is deliberately
|
|
11
|
+
* hook-free and React-free (see its header) so it can be called from outside a
|
|
12
|
+
* hook boundary — e.g. inside Button's non-hook click wrapper. Importing
|
|
13
|
+
* FormShell there would have dragged React and the whole form runtime into it.
|
|
14
|
+
*
|
|
15
|
+
* This is a MOVE, not a barrel re-export: `FormShell.tsx` imports it from here.
|
|
16
|
+
*
|
|
17
|
+
* Keys are the full dotted/indexed path (`"video.duration"`, `"images.2"`),
|
|
18
|
+
* not just the top-level segment — a plain `issue.path[0]` key would collapse
|
|
19
|
+
* every nested issue under one object onto the same map entry, silently
|
|
20
|
+
* dropping all but the last. Top-level-only fields (the common case) are
|
|
21
|
+
* unaffected: a single-segment path joins to the same string as before.
|
|
22
|
+
*
|
|
23
|
+
* Root-level `.refine()` issues (empty `path: []`, no owning field) are still
|
|
24
|
+
* skipped — there is no field to attach them to, so the caller must surface
|
|
25
|
+
* those some other way (they fall through to a toast in `surfaceError`).
|
|
26
|
+
*/
|
|
27
|
+
export function applyZodIssues(issues, setFieldError) {
|
|
28
|
+
for (const issue of issues) {
|
|
29
|
+
if (!issue.path || issue.path.length === 0)
|
|
30
|
+
continue;
|
|
31
|
+
const key = issue.path.map(String).join(".");
|
|
32
|
+
setFieldError(key, issue.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** True when at least one issue carries a field path we can attach it to. */
|
|
36
|
+
export function hasAttachableIssue(issues) {
|
|
37
|
+
return Boolean(issues?.some((i) => i.path && i.path.length > 0));
|
|
38
|
+
}
|
package/dist/ui/forms/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { FormShellProviderProps, FormShellStep, FormShellNav, FormShellContextValue, UseFormShellStateResult, } from "./FormShell";
|
|
2
|
-
export { FormShellProvider, FormShellContext, useFormShell, useFormShellState
|
|
2
|
+
export { FormShellProvider, FormShellContext, useFormShell, useFormShellState } from "./FormShell";
|
|
3
|
+
export { applyZodIssues, hasAttachableIssue } from "./apply-zod-issues";
|
|
3
4
|
export type { FieldInputProps } from "./FieldInput";
|
|
4
5
|
export { FieldInput } from "./FieldInput";
|
|
5
6
|
export type { FieldSelectProps } from "./FieldSelect";
|
package/dist/ui/forms/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { FormShellProvider, FormShellContext, useFormShell, useFormShellState
|
|
1
|
+
export { FormShellProvider, FormShellContext, useFormShell, useFormShellState } from "./FormShell";
|
|
2
|
+
export { applyZodIssues, hasAttachableIssue } from "./apply-zod-issues";
|
|
2
3
|
export { FieldInput } from "./FieldInput";
|
|
3
4
|
export { FieldSelect } from "./FieldSelect";
|
|
4
5
|
export { FieldCheckbox } from "./FieldCheckbox";
|
|
@@ -50,6 +50,14 @@ export declare const ICONS: {
|
|
|
50
50
|
readonly settings: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
51
51
|
readonly expand: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
52
52
|
readonly user: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
53
|
+
readonly notification: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
54
|
+
readonly compare: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
55
|
+
readonly help: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
56
|
+
readonly menu: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
57
|
+
readonly close: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
58
|
+
readonly themeLight: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
59
|
+
readonly themeDark: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
60
|
+
readonly deals: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
|
|
53
61
|
};
|
|
54
62
|
/** Every valid `iconKey`. A typo is a compile error, not a silent blank. */
|
|
55
63
|
export type IconKey = keyof typeof ICONS;
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* @tag consumers:Button,RowActionMenu,BulkActionBar,action-registry
|
|
40
40
|
* @tag sideEffects:none
|
|
41
41
|
*/
|
|
42
|
-
import { Archive, ArrowLeft, Ban, Check, CheckCircle, ChevronDown, Copy, Download, Edit, Eye, EyeOff, FileDown, Filter, Flag, Gavel, Heart, Link as LinkIcon, Lock, Mail, MessageSquare, Package, Pause, Play, Plus, RefreshCw, Reply, RotateCcw, Save, Search, Send, Settings, Share2, ShieldCheck, ShoppingCart, Star, Trash2, TrendingUp, Truck, Upload, User, X, XCircle, } from "lucide-react";
|
|
42
|
+
import { Archive, ArrowLeft, Ban, Bell, Check, CheckCircle, ChevronDown, Copy, Download, Edit, Eye, EyeOff, FileDown, Filter, Flag, Gavel, HelpCircle, Heart, Link as LinkIcon, Lock, Mail, Menu, MessageSquare, Moon, Package, Pause, Play, Plus, RefreshCw, Reply, RotateCcw, Save, Search, Send, Settings, Share2, ShieldCheck, ShoppingCart, Star, Sun, Scale, Tag, Trash2, TrendingUp, Truck, Upload, User, X, XCircle, } from "lucide-react";
|
|
43
43
|
/**
|
|
44
44
|
* Semantic name → component.
|
|
45
45
|
*
|
|
@@ -98,6 +98,18 @@ export const ICONS = {
|
|
|
98
98
|
settings: Settings,
|
|
99
99
|
expand: ChevronDown,
|
|
100
100
|
user: User,
|
|
101
|
+
// Header chrome. Added 2026-08-27 when TitleBarLayout stopped hand-inlining
|
|
102
|
+
// ten raw <svg> elements — a raw <svg> carries no `shrink-0`, so inside a
|
|
103
|
+
// padded flex control it silently shrank rather than rendering at its stated
|
|
104
|
+
// size, and no audit could see a size it never declared.
|
|
105
|
+
notification: Bell,
|
|
106
|
+
compare: Scale,
|
|
107
|
+
help: HelpCircle,
|
|
108
|
+
menu: Menu,
|
|
109
|
+
close: X,
|
|
110
|
+
themeLight: Sun,
|
|
111
|
+
themeDark: Moon,
|
|
112
|
+
deals: Tag,
|
|
101
113
|
};
|
|
102
114
|
/**
|
|
103
115
|
* The ONLY sanctioned glyph sizes.
|
|
@@ -7,6 +7,55 @@
|
|
|
7
7
|
export declare const FIREBASE_STORAGE_HOST = "firebasestorage.googleapis.com";
|
|
8
8
|
/** Google Cloud Storage host (used by public-asset URLs). */
|
|
9
9
|
export declare const GCS_HOST = "storage.googleapis.com";
|
|
10
|
+
/**
|
|
11
|
+
* The canonical stored form of every media reference: `/media/<slug>`.
|
|
12
|
+
*
|
|
13
|
+
* `POST /api/media/finalize` mints exactly this, `next.config.js` rewrites
|
|
14
|
+
* `/media/:path*` → `/api/media/:path*`, and CLAUDE.md § Media Architecture
|
|
15
|
+
* makes it mandatory ("never write raw firebasestorage.googleapis.com URLs
|
|
16
|
+
* into Firestore"). Exported so validation can name the same prefix the
|
|
17
|
+
* resolver below branches on, instead of re-typing the literal.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MEDIA_PROXY_PREFIX = "/media/";
|
|
20
|
+
/** Cap for any stored media reference. Matches the previous schema bound. */
|
|
21
|
+
export declare const MEDIA_URL_MAX_LENGTH = 2048;
|
|
22
|
+
/**
|
|
23
|
+
* Absolute hosts a stored media reference may point at.
|
|
24
|
+
*
|
|
25
|
+
* These exist for BACK-COMPAT only — rows written before the `/media/` proxy,
|
|
26
|
+
* and `seedExtMedia()` fixtures pointing at third-party sample assets. New
|
|
27
|
+
* writes should always be producing `/media/<slug>`. This list was previously
|
|
28
|
+
* duplicated in `appkit/src/validation/schemas.ts` and
|
|
29
|
+
* `src/validation/request-schemas.ts`; it lives here now so there is one copy.
|
|
30
|
+
*/
|
|
31
|
+
export declare const APPROVED_MEDIA_DOMAINS: readonly string[];
|
|
32
|
+
export declare const MEDIA_URL_MESSAGE = "Must be a stored media reference (/media/<slug>) or a URL on an approved CDN domain";
|
|
33
|
+
/**
|
|
34
|
+
* Is `value` something we are willing to PERSIST as a media reference?
|
|
35
|
+
*
|
|
36
|
+
* The single definition of that rule — `mediaUrlSchema` in both validation
|
|
37
|
+
* modules refines on it, and everything that stores an image/video URL should
|
|
38
|
+
* go through one of those.
|
|
39
|
+
*
|
|
40
|
+
* Accepts:
|
|
41
|
+
* - `/media/<slug>` — the canonical form (`/api/media/finalize` output);
|
|
42
|
+
* - an absolute URL on an approved host — see `APPROVED_MEDIA_DOMAINS`.
|
|
43
|
+
*
|
|
44
|
+
* Rejects:
|
|
45
|
+
* - `blob:` / `data:` — valid only in the tab that created them. Persisting
|
|
46
|
+
* one stores a reference that is already dead by the time anyone reads it;
|
|
47
|
+
* - `/api/media/ext?url=…` — a RENDER-time transform produced by
|
|
48
|
+
* `resolveMediaUrl` below. Storing it double-wraps on the next render and
|
|
49
|
+
* pins the value to a proxy route rather than to the asset;
|
|
50
|
+
* - anything else, including bare relative paths that are not `/media/`.
|
|
51
|
+
*
|
|
52
|
+
* NOTE this is deliberately a strict SUPERSET of the rule it replaced (which
|
|
53
|
+
* was `.url()` + an approved-host check). That old rule rejected the canonical
|
|
54
|
+
* `/media/<slug>` form outright — which is what made every avatar save fail —
|
|
55
|
+
* while happily accepting the raw Storage URLs CLAUDE.md bans. No value that
|
|
56
|
+
* validated before stops validating now.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isStoredMediaRef(value: string): boolean;
|
|
10
59
|
export interface ResolveMediaUrlOptions {
|
|
11
60
|
/**
|
|
12
61
|
* When false, a URL we do not own is returned UNCHANGED instead of being sent
|
package/dist/utils/media-url.js
CHANGED
|
@@ -9,7 +9,78 @@ import { normalizeError } from "../errors/normalize";
|
|
|
9
9
|
export const FIREBASE_STORAGE_HOST = "firebasestorage.googleapis.com";
|
|
10
10
|
/** Google Cloud Storage host (used by public-asset URLs). */
|
|
11
11
|
export const GCS_HOST = "storage.googleapis.com";
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The canonical stored form of every media reference: `/media/<slug>`.
|
|
14
|
+
*
|
|
15
|
+
* `POST /api/media/finalize` mints exactly this, `next.config.js` rewrites
|
|
16
|
+
* `/media/:path*` → `/api/media/:path*`, and CLAUDE.md § Media Architecture
|
|
17
|
+
* makes it mandatory ("never write raw firebasestorage.googleapis.com URLs
|
|
18
|
+
* into Firestore"). Exported so validation can name the same prefix the
|
|
19
|
+
* resolver below branches on, instead of re-typing the literal.
|
|
20
|
+
*/
|
|
21
|
+
export const MEDIA_PROXY_PREFIX = "/media/";
|
|
22
|
+
const PROXY_PREFIX = MEDIA_PROXY_PREFIX;
|
|
23
|
+
/** Cap for any stored media reference. Matches the previous schema bound. */
|
|
24
|
+
export const MEDIA_URL_MAX_LENGTH = 2048;
|
|
25
|
+
/**
|
|
26
|
+
* Absolute hosts a stored media reference may point at.
|
|
27
|
+
*
|
|
28
|
+
* These exist for BACK-COMPAT only — rows written before the `/media/` proxy,
|
|
29
|
+
* and `seedExtMedia()` fixtures pointing at third-party sample assets. New
|
|
30
|
+
* writes should always be producing `/media/<slug>`. This list was previously
|
|
31
|
+
* duplicated in `appkit/src/validation/schemas.ts` and
|
|
32
|
+
* `src/validation/request-schemas.ts`; it lives here now so there is one copy.
|
|
33
|
+
*/
|
|
34
|
+
export const APPROVED_MEDIA_DOMAINS = [
|
|
35
|
+
FIREBASE_STORAGE_HOST,
|
|
36
|
+
GCS_HOST,
|
|
37
|
+
"res.cloudinary.com",
|
|
38
|
+
"images.unsplash.com",
|
|
39
|
+
];
|
|
40
|
+
export const MEDIA_URL_MESSAGE = "Must be a stored media reference (/media/<slug>) or a URL on an approved CDN domain";
|
|
41
|
+
/**
|
|
42
|
+
* Is `value` something we are willing to PERSIST as a media reference?
|
|
43
|
+
*
|
|
44
|
+
* The single definition of that rule — `mediaUrlSchema` in both validation
|
|
45
|
+
* modules refines on it, and everything that stores an image/video URL should
|
|
46
|
+
* go through one of those.
|
|
47
|
+
*
|
|
48
|
+
* Accepts:
|
|
49
|
+
* - `/media/<slug>` — the canonical form (`/api/media/finalize` output);
|
|
50
|
+
* - an absolute URL on an approved host — see `APPROVED_MEDIA_DOMAINS`.
|
|
51
|
+
*
|
|
52
|
+
* Rejects:
|
|
53
|
+
* - `blob:` / `data:` — valid only in the tab that created them. Persisting
|
|
54
|
+
* one stores a reference that is already dead by the time anyone reads it;
|
|
55
|
+
* - `/api/media/ext?url=…` — a RENDER-time transform produced by
|
|
56
|
+
* `resolveMediaUrl` below. Storing it double-wraps on the next render and
|
|
57
|
+
* pins the value to a proxy route rather than to the asset;
|
|
58
|
+
* - anything else, including bare relative paths that are not `/media/`.
|
|
59
|
+
*
|
|
60
|
+
* NOTE this is deliberately a strict SUPERSET of the rule it replaced (which
|
|
61
|
+
* was `.url()` + an approved-host check). That old rule rejected the canonical
|
|
62
|
+
* `/media/<slug>` form outright — which is what made every avatar save fail —
|
|
63
|
+
* while happily accepting the raw Storage URLs CLAUDE.md bans. No value that
|
|
64
|
+
* validated before stops validating now.
|
|
65
|
+
*/
|
|
66
|
+
export function isStoredMediaRef(value) {
|
|
67
|
+
if (!value || value.length > MEDIA_URL_MAX_LENGTH)
|
|
68
|
+
return false;
|
|
69
|
+
if (value.startsWith(MEDIA_PROXY_PREFIX))
|
|
70
|
+
return true;
|
|
71
|
+
if (value.startsWith("blob:") || value.startsWith("data:"))
|
|
72
|
+
return false;
|
|
73
|
+
if (value.startsWith(MEDIA_ENDPOINTS.EXT))
|
|
74
|
+
return false;
|
|
75
|
+
try {
|
|
76
|
+
const { hostname } = new URL(value);
|
|
77
|
+
return APPROVED_MEDIA_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
78
|
+
}
|
|
79
|
+
catch (_err) {
|
|
80
|
+
void normalizeError(_err); // not an absolute URL, and not /media/ — reject
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
13
84
|
/**
|
|
14
85
|
* Our own Storage bucket, as it appears as the first path segment of a
|
|
15
86
|
* `storage.googleapis.com` URL. Only URLs pointing at THIS bucket may have that
|
|
@@ -20,8 +20,21 @@ export declare const paginationQuerySchema: z.ZodObject<{
|
|
|
20
20
|
}>;
|
|
21
21
|
export declare const objectIdSchema: z.ZodString;
|
|
22
22
|
export declare const urlSchema: z.ZodString;
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* A media reference we are willing to PERSIST.
|
|
25
|
+
*
|
|
26
|
+
* The rule lives in `isStoredMediaRef` (`utils/media-url.ts`) so this schema
|
|
27
|
+
* and its zod-4 twin in `src/validation/request-schemas.ts` cannot drift —
|
|
28
|
+
* the two Zod major versions mean the schema object can't be shared, but the
|
|
29
|
+
* predicate can.
|
|
30
|
+
*
|
|
31
|
+
* This used to be `.url()` + an approved-host refine, which rejected the
|
|
32
|
+
* canonical `/media/<slug>` form that `/api/media/finalize` actually mints.
|
|
33
|
+
* That is what made every avatar save fail with a bare "Validation failed".
|
|
34
|
+
*
|
|
35
|
+
* For a genuine EXTERNAL link (a brand website, a social handle, a tracking
|
|
36
|
+
* URL) use `urlSchema` above — not this.
|
|
37
|
+
*/
|
|
25
38
|
export declare const mediaUrlSchema: z.ZodEffects<z.ZodString, string, string>;
|
|
26
39
|
export declare const dateStringSchema: z.ZodString;
|
|
27
40
|
/** Discount percentage — 0–100. */
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { normalizeError } from "../errors/normalize";
|
|
3
2
|
// ============================================
|
|
4
3
|
// COMMON SCHEMAS
|
|
5
4
|
// ============================================
|
|
@@ -12,29 +11,26 @@ export const paginationQuerySchema = z.object({
|
|
|
12
11
|
});
|
|
13
12
|
export const objectIdSchema = z.string().regex(/^[a-z0-9-]+$/);
|
|
14
13
|
export const urlSchema = z.string().url().max(2048);
|
|
15
|
-
import {
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
import { isStoredMediaRef, MEDIA_URL_MAX_LENGTH, MEDIA_URL_MESSAGE, } from "../utils/media-url";
|
|
15
|
+
/**
|
|
16
|
+
* A media reference we are willing to PERSIST.
|
|
17
|
+
*
|
|
18
|
+
* The rule lives in `isStoredMediaRef` (`utils/media-url.ts`) so this schema
|
|
19
|
+
* and its zod-4 twin in `src/validation/request-schemas.ts` cannot drift —
|
|
20
|
+
* the two Zod major versions mean the schema object can't be shared, but the
|
|
21
|
+
* predicate can.
|
|
22
|
+
*
|
|
23
|
+
* This used to be `.url()` + an approved-host refine, which rejected the
|
|
24
|
+
* canonical `/media/<slug>` form that `/api/media/finalize` actually mints.
|
|
25
|
+
* That is what made every avatar save fail with a bare "Validation failed".
|
|
26
|
+
*
|
|
27
|
+
* For a genuine EXTERNAL link (a brand website, a social handle, a tracking
|
|
28
|
+
* URL) use `urlSchema` above — not this.
|
|
29
|
+
*/
|
|
24
30
|
export const mediaUrlSchema = z
|
|
25
31
|
.string()
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
.refine((url) => {
|
|
29
|
-
try {
|
|
30
|
-
const { hostname } = new URL(url);
|
|
31
|
-
return APPROVED_MEDIA_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
32
|
-
}
|
|
33
|
-
catch (_err) {
|
|
34
|
-
void normalizeError(_err);
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}, { message: "Image or video URL must be hosted on an approved CDN domain" });
|
|
32
|
+
.max(MEDIA_URL_MAX_LENGTH)
|
|
33
|
+
.refine(isStoredMediaRef, { message: MEDIA_URL_MESSAGE });
|
|
38
34
|
export const dateStringSchema = z.string().datetime();
|
|
39
35
|
// ============================================
|
|
40
36
|
// SHARED PRIMITIVES (W2-4 — 2026-05-23)
|