@mohasinac/appkit 2.7.18 → 2.7.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +12 -8
  2. package/dist/_internal/client/features/layout/filterNavItems.d.ts +15 -0
  3. package/dist/_internal/client/features/layout/filterNavItems.js +19 -0
  4. package/dist/_internal/server/features/seo/manifest.js +9 -3
  5. package/dist/_internal/server/features/site-settings/actions.d.ts +14 -0
  6. package/dist/_internal/server/features/site-settings/actions.js +29 -0
  7. package/dist/_internal/shared/features/layout/types.d.ts +11 -0
  8. package/dist/client.d.ts +2 -0
  9. package/dist/client.js +2 -0
  10. package/dist/contracts/auth.d.ts +2 -0
  11. package/dist/features/admin/schemas/firestore.d.ts +21 -0
  12. package/dist/features/admin/schemas/firestore.js +3 -0
  13. package/dist/features/admin/utils/checkActionAllowed.d.ts +4 -0
  14. package/dist/features/admin/utils/checkActionAllowed.js +21 -0
  15. package/dist/features/admin/utils/getDisabledRoutes.d.ts +2 -0
  16. package/dist/features/admin/utils/getDisabledRoutes.js +6 -0
  17. package/dist/features/admin/utils/getSiteSettingsGlobal.d.ts +2 -0
  18. package/dist/features/admin/utils/getSiteSettingsGlobal.js +4 -0
  19. package/dist/features/categories/components/CategoryProductsListing.js +18 -13
  20. package/dist/features/layout/AppLayoutShell.js +8 -1
  21. package/dist/features/layout/ListingLayout.js +1 -1
  22. package/dist/features/layout/MainNavbar.d.ts +4 -0
  23. package/dist/features/layout/NavItem.js +1 -1
  24. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +22 -14
  25. package/dist/features/products/components/AuctionsIndexListing.js +19 -16
  26. package/dist/features/products/components/ProductGrid.js +1 -1
  27. package/dist/features/products/components/ProductsIndexListing.js +34 -26
  28. package/dist/features/products/constants/action-defs.d.ts +64 -22
  29. package/dist/features/products/constants/action-defs.js +101 -64
  30. package/dist/features/stores/components/StoreAuctionsListing.js +15 -8
  31. package/dist/features/stores/components/StoreProductsListing.js +18 -13
  32. package/dist/react/contexts/SessionContext.d.ts +2 -0
  33. package/dist/react/hooks/useAuthGate.d.ts +8 -0
  34. package/dist/react/hooks/useAuthGate.js +35 -0
  35. package/dist/seed/site-settings-seed-data.js +3 -0
  36. package/dist/server.d.ts +4 -0
  37. package/dist/server.js +6 -0
  38. package/dist/tailwind-utilities.css +1 -1
  39. package/package.json +1 -1
@@ -27,6 +27,8 @@ import { AdminSidebar } from "../../../../features/admin/components/AdminSidebar
27
27
  import { StoreSidebar } from "../../../../features/seller/components/SellerSidebar";
28
28
  import { UserSidebar } from "../../../../features/account/components/UserSidebar";
29
29
  import { DASHBOARD_DESKTOP_MEDIA_QUERY } from "../../../shared/features/layout/config";
30
+ import { filterNavItems } from "./filterNavItems";
31
+ import { useSiteSettings } from "../../../../core/hooks/useSiteSettings";
30
32
  /**
31
33
  * Hoisted drawer-state hook — the matchMedia-aware open/close logic that was
32
34
  * triplicated across admin/store/user layouts. Used internally by
@@ -67,16 +69,15 @@ function useResponsiveDrawer() {
67
69
  }, [registerNav, unregisterNav, open, close, toggle]);
68
70
  return { desktopOpen, mobileOpen, close, toggle };
69
71
  }
70
- /** Filter admin nav groups to only show items the user has permission to see. */
71
- function filterAdminGroups(groups, permissions) {
72
- // null = admin (show everything); undefined = no filtering (backwards compat)
73
- if (permissions === null || permissions === undefined)
72
+ /** Filter nav groups by navConfig (enabled toggle) + requiredPermission. */
73
+ function filterGroups(groups, navConfig, permissions) {
74
+ // null permissions = admin (show everything); undefined = no filtering (backwards compat)
75
+ if (permissions === null && !navConfig)
74
76
  return groups;
75
77
  return groups
76
78
  .map((group) => ({
77
79
  ...group,
78
- items: group.items.filter((item) => !item.requiredPermission ||
79
- permissions.includes(item.requiredPermission)),
80
+ items: filterNavItems(group.items, navConfig, permissions ?? undefined),
80
81
  }))
81
82
  .filter((group) => group.items.length > 0);
82
83
  }
@@ -84,8 +85,11 @@ export function DashboardLayoutClient({ variant, groups, permissions, activeHref
84
85
  const pathname = usePathname();
85
86
  const activeHref = explicitActiveHref ?? pathname ?? "";
86
87
  const { desktopOpen, mobileOpen, close, toggle } = useResponsiveDrawer();
88
+ const { data: settings } = useSiteSettings();
89
+ const navConfig = settings?.navConfig;
90
+ const filteredGroups = filterGroups(groups, navConfig, permissions);
87
91
  const adminGroups = variant === "admin"
88
- ? filterAdminGroups(groups, permissions)
92
+ ? filteredGroups
89
93
  : groups;
90
- return (_jsxs(_Fragment, { children: [variant === "admin" && (_jsx(AdminSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activePath: activeHref, groups: adminGroups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "store" && (_jsx(StoreSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activeHref: activeHref, items: [], groups: groups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "user" && (_jsx(UserSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, items: groups.flatMap((g) => g.items), groups: groups, onCloseMobile: close, onToggle: toggle, className: className })), children] }));
94
+ return (_jsxs(_Fragment, { children: [variant === "admin" && (_jsx(AdminSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activePath: activeHref, groups: adminGroups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "store" && (_jsx(StoreSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activeHref: activeHref, items: [], groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "user" && (_jsx(UserSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, items: filteredGroups.flatMap((g) => g.items), groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className })), children] }));
91
95
  }
@@ -0,0 +1,15 @@
1
+ type FilterableItem = {
2
+ id?: string;
3
+ requiredPermission?: string;
4
+ };
5
+ /**
6
+ * Filters nav items by:
7
+ * 1. siteSettings.navConfig[item.id].enabled (admin toggle)
8
+ * 2. requiredPermission — item hidden when user lacks the permission
9
+ *
10
+ * Items without an `id` always pass through (legacy / unmanaged items).
11
+ */
12
+ export declare function filterNavItems<T extends FilterableItem>(items: T[], navConfig: Record<string, {
13
+ enabled: boolean;
14
+ }> | undefined, userPermissions: string[] | undefined): T[];
15
+ export {};
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ /**
3
+ * Filters nav items by:
4
+ * 1. siteSettings.navConfig[item.id].enabled (admin toggle)
5
+ * 2. requiredPermission — item hidden when user lacks the permission
6
+ *
7
+ * Items without an `id` always pass through (legacy / unmanaged items).
8
+ */
9
+ export function filterNavItems(items, navConfig, userPermissions) {
10
+ return items.filter((item) => {
11
+ if (!item.id)
12
+ return true;
13
+ if (!(navConfig?.[item.id]?.enabled ?? true))
14
+ return false;
15
+ if (item.requiredPermission && !userPermissions?.includes(item.requiredPermission))
16
+ return false;
17
+ return true;
18
+ });
19
+ }
@@ -15,9 +15,15 @@ export function buildManifest({ name, shortName, description }) {
15
15
  prefer_related_applications: false,
16
16
  icons: [
17
17
  {
18
- src: "/favicon.svg",
19
- sizes: "any",
20
- type: "image/svg+xml",
18
+ src: "/favicon/android-chrome-192x192.png",
19
+ sizes: "192x192",
20
+ type: "image/png",
21
+ purpose: "any maskable",
22
+ },
23
+ {
24
+ src: "/favicon/android-chrome-512x512.png",
25
+ sizes: "512x512",
26
+ type: "image/png",
21
27
  purpose: "any maskable",
22
28
  },
23
29
  ],
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Toggle a single action's enabled state in siteSettings.actionConfig.
3
+ * Performs a sparse merge — only updates the one key.
4
+ */
5
+ export declare function updateActionConfigDomain(actionId: string, enabled: boolean): Promise<void>;
6
+ /**
7
+ * Toggle a single nav item's enabled state in siteSettings.navConfig.
8
+ * Also recomputes the derived disabledRoutes[] array from all current nav items.
9
+ * The `allNavItems` argument is the full { id, href } list so we can derive disabledRoutes.
10
+ */
11
+ export declare function updateNavConfigDomain(navId: string, enabled: boolean, allNavItems: Array<{
12
+ id: string;
13
+ href: string;
14
+ }>): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { siteSettingsRepository } from "../../../../features/admin/repository/site-settings.repository";
2
+ /**
3
+ * Toggle a single action's enabled state in siteSettings.actionConfig.
4
+ * Performs a sparse merge — only updates the one key.
5
+ */
6
+ export async function updateActionConfigDomain(actionId, enabled) {
7
+ await siteSettingsRepository.updateSingleton({
8
+ actionConfig: { [actionId]: { enabled } },
9
+ });
10
+ }
11
+ /**
12
+ * Toggle a single nav item's enabled state in siteSettings.navConfig.
13
+ * Also recomputes the derived disabledRoutes[] array from all current nav items.
14
+ * The `allNavItems` argument is the full { id, href } list so we can derive disabledRoutes.
15
+ */
16
+ export async function updateNavConfigDomain(navId, enabled, allNavItems) {
17
+ const current = await siteSettingsRepository.getSingleton();
18
+ const merged = {
19
+ ...(current.navConfig ?? {}),
20
+ [navId]: { enabled },
21
+ };
22
+ const disabledRoutes = allNavItems
23
+ .filter((item) => (merged[item.id]?.enabled ?? true) === false)
24
+ .map((item) => item.href);
25
+ await siteSettingsRepository.updateSingleton({
26
+ navConfig: merged,
27
+ disabledRoutes,
28
+ });
29
+ }
@@ -23,6 +23,13 @@ export interface SidebarNavItem {
23
23
  label: string;
24
24
  icon?: ReactNode;
25
25
  badge?: number;
26
+ /**
27
+ * Stable nav-* slug used as the key in siteSettings.navConfig.
28
+ * If absent, item is always visible (no admin toggle, no permission check).
29
+ */
30
+ id?: string;
31
+ /** RBAC permission key — item only shows to users with this permission. */
32
+ requiredPermission?: string;
26
33
  }
27
34
  /** Sidebar group with title + items + default-open state. */
28
35
  export interface SidebarNavGroup {
@@ -37,6 +44,10 @@ export interface MainNavItem {
37
44
  icon?: ReactNode;
38
45
  /** Translation key (consumer fills in `label` at render time via t(key)). */
39
46
  key?: string;
47
+ /** Stable nav-* slug used as the key in siteSettings.navConfig. */
48
+ id?: string;
49
+ /** RBAC permission key — item only shows to users with this permission. */
50
+ requiredPermission?: string;
40
51
  }
41
52
  /** Branding config — propagated to TitleBar / Footer / mobile chrome. */
42
53
  export interface BrandingConfig {
package/dist/client.d.ts CHANGED
@@ -134,6 +134,8 @@ export { FORM_ACTION_ID, FORM_ACTION_META, FORM_FOOTER_PRESET } from "./features
134
134
  export type { FormActionId, FormActionMeta } from "./features/products/index";
135
135
  export { DASHBOARD_QUICK_ACTION_ID, DASHBOARD_QUICK_ACTION_META, DASHBOARD_QUICK_ACTIONS } from "./features/products/index";
136
136
  export type { DashboardQuickActionId, DashboardQuickActionMeta } from "./features/products/index";
137
+ export { useAuthGate } from "./react/hooks/useAuthGate";
138
+ export type { UseAuthGateReturn } from "./react/hooks/useAuthGate";
137
139
  export { useActionDispatch } from "./react/hooks/use-action-dispatch";
138
140
  export type { DispatchAction, UseActionDispatchOptions } from "./react/hooks/use-action-dispatch";
139
141
  export { usePanelStore } from "./stores/panel-store";
package/dist/client.js CHANGED
@@ -174,6 +174,8 @@ export { ROW_ACTION_ID, ROW_ACTION_META, ADMIN_ROW_ACTIONS, SELLER_ROW_ACTIONS,
174
174
  export { FORM_ACTION_ID, FORM_ACTION_META, FORM_FOOTER_PRESET } from "./features/products/index";
175
175
  // Dashboard quick actions
176
176
  export { DASHBOARD_QUICK_ACTION_ID, DASHBOARD_QUICK_ACTION_META, DASHBOARD_QUICK_ACTIONS } from "./features/products/index";
177
+ // [CLIENT-ONLY] useAuthGate: pre-dispatch auth gate using ACTION_ID registry.
178
+ export { useAuthGate } from "./react/hooks/useAuthGate";
177
179
  // Action dispatch hook + panel store
178
180
  export { useActionDispatch } from "./react/hooks/use-action-dispatch";
179
181
  export { usePanelStore } from "./stores/panel-store";
@@ -12,6 +12,8 @@ export interface AuthPayload {
12
12
  phoneNumber?: string | null;
13
13
  /** Any extra custom claims stored on the token */
14
14
  claims?: Record<string, unknown>;
15
+ /** RBAC permission keys granted to this user. */
16
+ permissions?: string[];
15
17
  }
16
18
  export interface AuthUser {
17
19
  uid: string;
@@ -385,6 +385,27 @@ export interface SiteSettingsDocument {
385
385
  accentDark?: string;
386
386
  };
387
387
  featuredResults?: FeaturedResult[];
388
+ /**
389
+ * Per-action runtime enable/disable overrides.
390
+ * Key = ActionId value string (e.g. "checkout", "add-to-wishlist").
391
+ * Absent key = action is enabled by default.
392
+ */
393
+ actionConfig?: Partial<Record<string, {
394
+ enabled: boolean;
395
+ }>>;
396
+ /**
397
+ * Per-nav-item runtime enable/disable overrides.
398
+ * Key = NavItem.id (nav-* slug, e.g. "nav-products").
399
+ * Absent key = nav item is enabled by default.
400
+ */
401
+ navConfig?: Record<string, {
402
+ enabled: boolean;
403
+ }>;
404
+ /**
405
+ * Derived hrefs of disabled nav items — written by updateNavConfigAction
406
+ * alongside navConfig. Read by RSC public layouts to block disabled routes.
407
+ */
408
+ disabledRoutes?: string[];
388
409
  createdAt: Date;
389
410
  updatedAt: Date;
390
411
  }
@@ -196,6 +196,9 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
196
196
  enabled: true,
197
197
  message: "🎉 Up to 15% Off on Pokémon TCG this week — Use code SAVE15",
198
198
  },
199
+ actionConfig: {},
200
+ navConfig: {},
201
+ disabledRoutes: [],
199
202
  };
200
203
  export const SITE_SETTINGS_PUBLIC_FIELDS = [
201
204
  "siteName",
@@ -0,0 +1,4 @@
1
+ import type { ActionId } from "../../products/constants/action-defs";
2
+ import type { AuthPayload } from "../../../contracts/auth";
3
+ /** Throws if the action is disabled, requires auth the caller lacks, or requires a permission the caller lacks. */
4
+ export declare function checkActionAllowed(actionId: ActionId, user: AuthPayload | null): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { getSiteSettingsGlobal } from "./getSiteSettingsGlobal";
2
+ import { ACTION_META } from "../../products/constants/action-defs";
3
+ import { AuthenticationError } from "../../../errors/authentication-error";
4
+ import { AuthorizationError } from "../../../errors/authorization-error";
5
+ /** Throws if the action is disabled, requires auth the caller lacks, or requires a permission the caller lacks. */
6
+ export async function checkActionAllowed(actionId, user) {
7
+ const settings = await getSiteSettingsGlobal();
8
+ const enabled = settings?.actionConfig?.[actionId]?.enabled ??
9
+ ACTION_META[actionId]?.defaultEnabled ??
10
+ true;
11
+ if (!enabled) {
12
+ throw new AuthorizationError("This action is currently unavailable.");
13
+ }
14
+ const meta = ACTION_META[actionId];
15
+ if (meta?.requiresAuth && !user?.uid) {
16
+ throw new AuthenticationError("Authentication required.");
17
+ }
18
+ if (meta?.requiredPermission && !user?.permissions?.includes(meta.requiredPermission)) {
19
+ throw new AuthorizationError("You don't have permission to perform this action.");
20
+ }
21
+ }
@@ -0,0 +1,2 @@
1
+ /** Returns the hrefs of all disabled nav items (empty array when none disabled). */
2
+ export declare function getDisabledRoutes(): Promise<string[]>;
@@ -0,0 +1,6 @@
1
+ import { getSiteSettingsGlobal } from "./getSiteSettingsGlobal";
2
+ /** Returns the hrefs of all disabled nav items (empty array when none disabled). */
3
+ export async function getDisabledRoutes() {
4
+ const settings = await getSiteSettingsGlobal();
5
+ return settings?.disabledRoutes ?? [];
6
+ }
@@ -0,0 +1,2 @@
1
+ /** React.cache deduplication — one Firestore read per RSC request tree. */
2
+ export declare const getSiteSettingsGlobal: () => Promise<import("..").SiteSettingsDocument>;
@@ -0,0 +1,4 @@
1
+ import { cache } from "react";
2
+ import { siteSettingsRepository } from "../repository/site-settings.repository";
3
+ /** React.cache deduplication — one Firestore read per RSC request tree. */
4
+ export const getSiteSettingsGlobal = cache(() => siteSettingsRepository.getSingleton());
@@ -4,7 +4,9 @@ import { useState, useCallback } from "react";
4
4
  import { Search, SlidersHorizontal, LayoutGrid, List, X } from "lucide-react";
5
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { useProducts } from "../../products/hooks/useProducts";
7
- import { Pagination, SortDropdown, useToast } from "../../../ui";
7
+ import { Pagination, SortDropdown, useToast, LoginRequiredModal } from "../../../ui";
8
+ import { useAuthGate } from "../../../react/hooks/useAuthGate";
9
+ import { ACTION_ID } from "../../products/constants/action-defs";
8
10
  import { ROUTES } from "../../../next";
9
11
  import { ProductGrid } from "../../products/components/ProductGrid";
10
12
  import { ProductFilters } from "../../products/components/ProductFilters";
@@ -15,6 +17,7 @@ import { pushCartOp, pushWishlistOp } from "../../cart/utils/pending-ops";
15
17
  export function CategoryProductsListing({ categorySlug, categoryId, brandName, initialData, }) {
16
18
  const table = useUrlTable({ defaults: { pageSize: "24", sort: "-createdAt" } });
17
19
  const { showToast } = useToast();
20
+ const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
18
21
  const [searchInput, setSearchInput] = useState(table.get("q") || "");
19
22
  const [filterOpen, setFilterOpen] = useState(false);
20
23
  const [view, setView] = useState(table.get("view") || "card");
@@ -40,17 +43,19 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
40
43
  }, [searchInput, table]);
41
44
  const handleWishlistToggle = useCallback((productId) => {
42
45
  const isWishlisted = wishlistedIds.has(productId);
43
- if (isWishlisted) {
44
- localWishlist.remove(productId, "product");
45
- pushWishlistOp({ op: "remove", itemId: productId, type: "product" });
46
- showToast("Removed from wishlist", "info");
47
- }
48
- else {
49
- localWishlist.add(productId, "product");
50
- pushWishlistOp({ op: "add", itemId: productId, type: "product" });
51
- showToast("Added to wishlist", "success");
52
- }
53
- }, [wishlistedIds, localWishlist, showToast]);
46
+ requireAuth(isWishlisted ? ACTION_ID.REMOVE_FROM_WISHLIST : ACTION_ID.ADD_TO_WISHLIST, () => {
47
+ if (isWishlisted) {
48
+ localWishlist.remove(productId, "product");
49
+ pushWishlistOp({ op: "remove", itemId: productId, type: "product" });
50
+ showToast("Removed from wishlist", "info");
51
+ }
52
+ else {
53
+ localWishlist.add(productId, "product");
54
+ pushWishlistOp({ op: "add", itemId: productId, type: "product" });
55
+ showToast("Added to wishlist", "success");
56
+ }
57
+ });
58
+ }, [wishlistedIds, localWishlist, showToast, requireAuth]);
54
59
  const handleAddToCart = useCallback((product) => {
55
60
  localCart.add(product.id, 1, {
56
61
  productTitle: product.title,
@@ -65,5 +70,5 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
65
70
  table.set("view", next);
66
71
  };
67
72
  const closeFilters = () => setFilterOpen(false);
68
- return (_jsxs("div", { className: "min-h-[200px]", children: [_jsx("div", { className: "sticky top-[var(--header-height,0px)] z-20 border-b border-zinc-200 dark:border-slate-700 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm py-2.5 px-4", children: _jsxs("div", { className: "flex items-center gap-2.5 max-w-full", children: [_jsxs("button", { type: "button", onClick: () => setFilterOpen(true), className: "flex shrink-0 items-center gap-2 rounded-lg border border-zinc-300 dark:border-slate-600 px-3.5 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: "Filters" })] }), _jsxs("div", { className: "flex flex-1 items-center overflow-hidden rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-900", children: [_jsx("input", { type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), onKeyDown: (e) => e.key === "Enter" && commitSearch(), placeholder: `Search in ${categorySlug.replace(/-/g, " ")}...`, className: "min-w-0 flex-1 bg-transparent px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none" }), _jsx("button", { type: "button", onClick: commitSearch, className: "flex shrink-0 items-center justify-center px-3 py-2 text-zinc-400 hover:text-primary dark:hover:text-primary-400 transition-colors", "aria-label": "Search", children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1.5 text-sm text-zinc-500 dark:text-zinc-400", children: [_jsx("span", { className: "hidden md:inline whitespace-nowrap", children: "Sort by" }), _jsx(SortDropdown, { value: table.get("sort") || "-createdAt", onChange: (v) => { table.set("sort", v); }, options: PRODUCT_PUBLIC_SORT_OPTIONS })] }), _jsxs("div", { className: "flex shrink-0 items-center rounded-lg border border-zinc-300 dark:border-slate-600 overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => handleViewToggle("card"), "aria-label": "Grid view", className: `p-2 transition-colors ${view === "card" ? "bg-primary text-white" : "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400"}`, children: _jsx(LayoutGrid, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: () => handleViewToggle("list"), "aria-label": "List view", className: `p-2 transition-colors ${view === "list" ? "bg-primary text-white" : "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400"}`, children: _jsx(List, { className: "h-4 w-4" }) })] })] }) }), _jsxs("div", { className: "py-6", children: [isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6", children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" })] })] }, i))) })) : (_jsx(ProductGrid, { products: products, getProductHref: (p) => String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug || p.id)), view: view, emptyLabel: "No products found in this category.", onWishlistToggle: handleWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: handleAddToCart })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: closeFilters }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsx("button", { type: "button", onClick: closeFilters, "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(ProductFilters, { table: table, currencyPrefix: "\u20B9" }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsx("button", { type: "button", onClick: closeFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors", children: "Apply filters" }) })] })] }))] }));
73
+ return (_jsxs("div", { className: "min-h-[200px]", children: [_jsx("div", { className: "sticky top-[var(--header-height,0px)] z-20 border-b border-zinc-200 dark:border-slate-700 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm py-2.5 px-4", children: _jsxs("div", { className: "flex items-center gap-2.5 max-w-full", children: [_jsxs("button", { type: "button", onClick: () => setFilterOpen(true), className: "flex shrink-0 items-center gap-2 rounded-lg border border-zinc-300 dark:border-slate-600 px-3.5 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-200 hover:bg-zinc-50 dark:hover:bg-slate-800 transition-colors", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: "Filters" })] }), _jsxs("div", { className: "flex flex-1 items-center overflow-hidden rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-900", children: [_jsx("input", { type: "text", value: searchInput, onChange: (e) => setSearchInput(e.target.value), onKeyDown: (e) => e.key === "Enter" && commitSearch(), placeholder: `Search in ${categorySlug.replace(/-/g, " ")}...`, className: "min-w-0 flex-1 bg-transparent px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 outline-none" }), _jsx("button", { type: "button", onClick: commitSearch, className: "flex shrink-0 items-center justify-center px-3 py-2 text-zinc-400 hover:text-primary dark:hover:text-primary-400 transition-colors", "aria-label": "Search", children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1.5 text-sm text-zinc-500 dark:text-zinc-400", children: [_jsx("span", { className: "hidden md:inline whitespace-nowrap", children: "Sort by" }), _jsx(SortDropdown, { value: table.get("sort") || "-createdAt", onChange: (v) => { table.set("sort", v); }, options: PRODUCT_PUBLIC_SORT_OPTIONS })] }), _jsxs("div", { className: "flex shrink-0 items-center rounded-lg border border-zinc-300 dark:border-slate-600 overflow-hidden", children: [_jsx("button", { type: "button", onClick: () => handleViewToggle("card"), "aria-label": "Grid view", className: `p-2 transition-colors ${view === "card" ? "bg-primary text-white" : "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400"}`, children: _jsx(LayoutGrid, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: () => handleViewToggle("list"), "aria-label": "List view", className: `p-2 transition-colors ${view === "list" ? "bg-primary text-white" : "text-zinc-500 hover:bg-zinc-50 dark:hover:bg-slate-800 dark:text-zinc-400"}`, children: _jsx(List, { className: "h-4 w-4" }) })] })] }) }), _jsxs("div", { className: "py-6", children: [isLoading ? (_jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6", children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" })] })] }, i))) })) : (_jsx(ProductGrid, { products: products, getProductHref: (p) => String(ROUTES.PUBLIC.PRODUCT_DETAIL(p.slug || p.id)), view: view, emptyLabel: "No products found in this category.", onWishlistToggle: handleWishlistToggle, wishlistedIds: wishlistedIds, onAddToCart: handleAddToCart })), totalPages > 1 && (_jsx("div", { className: "mt-8 flex justify-center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: closeFilters }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsx("button", { type: "button", onClick: closeFilters, "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(ProductFilters, { table: table, currencyPrefix: "\u20B9" }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsx("button", { type: "button", onClick: closeFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors", children: "Apply filters" }) })] })] })), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
69
74
  }
@@ -10,6 +10,9 @@ const ROLE_DOT_COLORS = {
10
10
  employee: "#f59e0b",
11
11
  };
12
12
  import { useTheme } from "../../react";
13
+ import { useAuth } from "../../react/contexts/SessionContext";
14
+ import { useSiteSettings } from "../../core/hooks/useSiteSettings";
15
+ import { filterNavItems } from "../../_internal/client/features/layout/filterNavItems";
13
16
  import { useBottomActionsContext } from "./BottomActionsContext";
14
17
  import BottomActions from "./BottomActions";
15
18
  import { AutoBreadcrumbs } from "./AutoBreadcrumbs";
@@ -50,6 +53,10 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
50
53
  const { theme, toggleTheme } = useTheme();
51
54
  const { closeNav: closeDashboardNav, hasNav: hasDashboardNav, toggleNav: toggleDashboardNav } = useDashboardNav();
52
55
  const { state: bottomActionsState } = useBottomActionsContext();
56
+ const { user: authUser } = useAuth();
57
+ const { data: siteSettingsData } = useSiteSettings();
58
+ const navConfig = siteSettingsData?.navConfig;
59
+ const visibleNavItems = filterNavItems(navItems, navConfig, authUser?.permissions);
53
60
  const headerRef = useRef(null);
54
61
  useEffect(() => {
55
62
  const el = headerRef.current;
@@ -154,7 +161,7 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
154
161
  opacity: darkBackground.overlay?.opacity ?? 0,
155
162
  },
156
163
  };
157
- return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(Div, { className: "flex min-h-screen w-full flex-col overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(MainNavbar, { navItems: navItems, hiddenNavItems: hiddenNavItems }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { className: "relative flex w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsxs(Div, { className: "flex items-center justify-between gap-3", children: [_jsxs(Div, { className: "flex items-center gap-3 flex-1 min-w-0", children: [_jsxs(Div, { className: "flex-shrink-0 relative", children: [_jsx(AvatarDisplay, { cropData: user.avatarMetadata
164
+ return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(Div, { className: "flex min-h-screen w-full flex-col overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(MainNavbar, { navItems: visibleNavItems, hiddenNavItems: hiddenNavItems }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { className: "relative flex w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsxs(Div, { className: "flex items-center justify-between gap-3", children: [_jsxs(Div, { className: "flex items-center gap-3 flex-1 min-w-0", children: [_jsxs(Div, { className: "flex-shrink-0 relative", children: [_jsx(AvatarDisplay, { cropData: user.avatarMetadata
158
165
  ? {
159
166
  url: user.avatarMetadata.url,
160
167
  position: user.avatarMetadata.position ?? {
@@ -76,7 +76,7 @@ export function ListingLayout({ headerSlot, statusTabsSlot, filterContent, filte
76
76
  "text-zinc-600 dark:text-slate-300",
77
77
  "hover:bg-zinc-50 dark:hover:bg-slate-800/60 transition-colors",
78
78
  filterActiveCount > 0
79
- ? "border-primary/40 bg-primary/5 text-primary"
79
+ ? "border-primary/40 bg-primary/5 text-zinc-800 dark:text-zinc-100"
80
80
  : "",
81
81
  ].join(" "), children: [l.filtersTitle, filterActiveCount > 0 && (_jsx(Span, { className: "inline-flex items-center justify-center w-4 h-4 text-[10px] font-bold rounded-full bg-primary text-white", children: filterActiveCount }))] })), searchSlot && _jsx(Div, { className: "flex-1 min-w-0", children: searchSlot })] }), (sortSlot || viewToggleSlot || actionsSlot) && (_jsx(Div, { className: "flex items-stretch min-h-[44px] gap-2 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: _jsxs(Row, { className: "gap-2 flex-shrink-0 pb-px", children: [sortSlot, viewToggleSlot, actionsSlot] }) }))] }), selectedCount > 0 && bulkActionBarSlot && (_jsx(Div, { className: "hidden md:block pt-2 mt-2 border-t border-zinc-100 dark:border-slate-800", children: bulkActionBarSlot }))] }), _jsxs(Div, { className: "flex gap-4 lg:gap-6 items-start", children: [hasFilter && (_jsx(Aside, { "aria-label": panelTitle, className: [
82
82
  "hidden lg:block flex-shrink-0 self-start",
@@ -8,6 +8,10 @@ export interface MainNavbarItem {
8
8
  label: string;
9
9
  icon?: React.ReactNode;
10
10
  highlighted?: boolean;
11
+ /** Stable nav-* slug for siteSettings.navConfig toggling. */
12
+ id?: string;
13
+ /** RBAC permission key — item hidden when user lacks this permission. */
14
+ requiredPermission?: string;
11
15
  }
12
16
  export interface MainNavbarProps {
13
17
  /** Pre-translated nav items to display */
@@ -18,7 +18,7 @@ export function NavItem({ href, label, icon, isActive = false, variant = "horizo
18
18
  }
19
19
  if (highlighted) {
20
20
  return (_jsxs(Link, { href: href, className: [
21
- "flex items-center gap-2 px-3 py-1 rounded-full border border-primary-700/30 dark:border-primary/30 bg-primary-700/5 dark:bg-primary/5 text-primary-700 dark:text-primary text-sm font-medium transition-all hover:bg-primary-700/10 dark:hover:bg-primary/10",
21
+ "flex items-center gap-2 px-3 py-1 rounded-full border border-primary-700/30 dark:border-primary/30 bg-primary-700/5 dark:bg-primary/5 text-zinc-800 dark:text-zinc-100 text-sm font-medium transition-all hover:bg-primary-700/10 dark:hover:bg-primary/10",
22
22
  className,
23
23
  ]
24
24
  .filter(Boolean)
@@ -4,7 +4,8 @@ import { useState, useCallback, useMemo } from "react";
4
4
  import { Columns, Heart, ShoppingCart, SlidersHorizontal, X } from "lucide-react";
5
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { useProducts } from "../../products/hooks/useProducts";
7
- import { Pagination, useToast, BulkActionBar, ListingToolbar } from "../../../ui";
7
+ import { Pagination, useToast, BulkActionBar, ListingToolbar, LoginRequiredModal } from "../../../ui";
8
+ import { useAuthGate } from "../../../react/hooks/useAuthGate";
8
9
  import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../../products/constants/action-defs";
9
10
  import { CompareOverlay } from "../../products/components/CompareOverlay";
10
11
  import { ROUTES } from "../../../next";
@@ -32,6 +33,7 @@ const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.BRAND, TABLE_KEYS.MIN_PRICE
32
33
  export function PreOrdersIndexListing({ initialData, categorySlug, brandName }) {
33
34
  const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
34
35
  const { showToast } = useToast();
36
+ const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
35
37
  const [searchInput, setSearchInput] = useState(table.get(TABLE_KEYS.QUERY) || "");
36
38
  const [filterOpen, setFilterOpen] = useState(false);
37
39
  const [compareIds, setCompareIds] = useState([]);
@@ -132,15 +134,19 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
132
134
  };
133
135
  const wishlistActions = {
134
136
  addToWishlist: (productId) => {
135
- localWishlist.add(productId, "preorder");
136
- pushWishlistOp({ op: "add", itemId: productId, type: "preorder" });
137
- showToast("Added to wishlist", "success");
137
+ requireAuth(ACTION_ID.ADD_TO_WISHLIST, () => {
138
+ localWishlist.add(productId, "preorder");
139
+ pushWishlistOp({ op: "add", itemId: productId, type: "preorder" });
140
+ showToast("Added to wishlist", "success");
141
+ });
138
142
  return Promise.resolve();
139
143
  },
140
144
  removeFromWishlist: (productId) => {
141
- localWishlist.remove(productId, "preorder");
142
- pushWishlistOp({ op: "remove", itemId: productId, type: "preorder" });
143
- showToast("Removed from wishlist", "info");
145
+ requireAuth(ACTION_ID.REMOVE_FROM_WISHLIST, () => {
146
+ localWishlist.remove(productId, "preorder");
147
+ pushWishlistOp({ op: "remove", itemId: productId, type: "preorder" });
148
+ showToast("Removed from wishlist", "info");
149
+ });
144
150
  return Promise.resolve();
145
151
  },
146
152
  isWishlisted: (productId) => wishlistedIds.has(productId),
@@ -178,13 +184,15 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
178
184
  icon: _jsx(Heart, { className: "h-3.5 w-3.5" }),
179
185
  variant: "secondary",
180
186
  onClick: () => {
181
- const selected = preOrders.filter((p) => selection.selectedIdSet.has(p.id));
182
- selected.forEach((p) => {
183
- localWishlist.add(p.id, "preorder");
184
- pushWishlistOp({ op: "add", itemId: p.id, type: "preorder" });
187
+ requireAuth(ACTION_ID.ADD_TO_WISHLIST, () => {
188
+ const selected = preOrders.filter((p) => selection.selectedIdSet.has(p.id));
189
+ selected.forEach((p) => {
190
+ localWishlist.add(p.id, "preorder");
191
+ pushWishlistOp({ op: "add", itemId: p.id, type: "preorder" });
192
+ });
193
+ showToast(`${selected.length} items added to wishlist`, "success");
194
+ selection.clearSelection();
185
195
  });
186
- showToast(`${selected.length} items added to wishlist`, "success");
187
- selection.clearSelection();
188
196
  },
189
197
  },
190
198
  {
@@ -201,5 +209,5 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
201
209
  ] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx("div", { className: "py-6", children: isLoading ? (_jsx("div", { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs("div", { className: "rounded-xl border border-zinc-100 dark:border-slate-700 overflow-hidden animate-pulse", children: [_jsx("div", { className: "aspect-square bg-zinc-200 dark:bg-slate-700" }), _jsxs("div", { className: "p-3 space-y-2", children: [_jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-3/4" }), _jsx("div", { className: "h-3 bg-zinc-200 dark:bg-slate-700 rounded w-1/2" }), _jsx("div", { className: "h-4 bg-zinc-200 dark:bg-slate-700 rounded w-1/3" }), _jsx("div", { className: "h-8 bg-zinc-200 dark:bg-slate-700 rounded" })] })] }, i))) })) : preOrders.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-zinc-500 dark:text-zinc-400", children: "No pre-orders found." })) : view === "list" ? (_jsx("div", { className: "flex flex-col divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800", children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "list", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) : (_jsx("div", { className: gridClass, children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "grid", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "preorder", onClose: () => {
202
210
  setCompareIds([]);
203
211
  selection.clearSelection();
204
- }, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== id)) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsxs("div", { className: "flex items-center gap-2", children: [pendingFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(PreOrderFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", pendingFilterCount > 0 ? ` (${pendingFilterCount})` : ""] }) })] })] }))] }));
212
+ }, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== id)) }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsxs("span", { className: "flex items-center gap-2 text-base font-semibold text-zinc-900 dark:text-zinc-100", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), "Filters"] }), _jsxs("div", { className: "flex items-center gap-2", children: [pendingFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close filters", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(PreOrderFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", pendingFilterCount > 0 ? ` (${pendingFilterCount})` : ""] }) })] })] })), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
205
213
  }