@mohasinac/appkit 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/_internal/client/features/layout/useSidebarSearch.d.ts +19 -0
  2. package/dist/_internal/client/features/layout/useSidebarSearch.js +21 -0
  3. package/dist/_internal/server/features/auth/permissions.d.ts +2 -0
  4. package/dist/_internal/server/features/auth/permissions.js +4 -2
  5. package/dist/_internal/server/features/seo/sitemap.js +3 -2
  6. package/dist/client.d.ts +1 -1
  7. package/dist/client.js +1 -1
  8. package/dist/features/about/components/FAQPageView.d.ts +4 -1
  9. package/dist/features/about/components/FAQPageView.js +17 -7
  10. package/dist/features/account/components/UserSidebar.js +15 -9
  11. package/dist/features/admin/components/AdminSidebar.js +15 -9
  12. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +4 -1
  13. package/dist/features/admin/components/AdminUserEditorView.d.ts +3 -1
  14. package/dist/features/admin/components/AdminUserEditorView.js +10 -2
  15. package/dist/features/admin/components/AdminUsersView.js +1 -1
  16. package/dist/features/auth/role-predicates.d.ts +14 -0
  17. package/dist/features/auth/role-predicates.js +1 -0
  18. package/dist/features/auth/schemas/firestore.d.ts +2 -0
  19. package/dist/features/auth/schemas/firestore.js +1 -0
  20. package/dist/features/events/actions/event-actions.js +15 -8
  21. package/dist/features/events/repository/events.repository.d.ts +2 -0
  22. package/dist/features/events/repository/events.repository.js +9 -0
  23. package/dist/features/events/schemas/firestore.d.ts +1 -0
  24. package/dist/features/events/schemas/firestore.js +1 -0
  25. package/dist/features/faq/components/FAQSearchableList.d.ts +11 -0
  26. package/dist/features/faq/components/FAQSearchableList.js +21 -0
  27. package/dist/features/homepage/components/BeforeAfterCard.js +2 -1
  28. package/dist/features/homepage/components/BrandsSection.js +2 -1
  29. package/dist/features/homepage/components/CharacterHotspot.js +2 -1
  30. package/dist/features/homepage/components/CharacterHotspotForm.js +4 -3
  31. package/dist/features/homepage/components/HeroBanner.js +2 -1
  32. package/dist/features/homepage/components/PromoGrid.js +2 -1
  33. package/dist/features/homepage/components/ShopByCategorySection.js +2 -1
  34. package/dist/features/seller/components/SellerSidebar.js +15 -9
  35. package/dist/features/tester/actions/checklist-item-actions.d.ts +6 -0
  36. package/dist/features/tester/actions/checklist-item-actions.js +2 -0
  37. package/dist/features/tester/components/TesterHubView.js +5 -4
  38. package/dist/features/tester/schemas/firestore.d.ts +3 -1
  39. package/dist/features/tester/schemas/firestore.js +2 -0
  40. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +159 -6
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/next/api/routeHandler.js +25 -3
  44. package/dist/react/contexts/SessionContext.d.ts +2 -0
  45. package/dist/react/contexts/SessionContext.js +2 -0
  46. package/dist/styles.css +1 -1
  47. package/dist/tailwind-utilities.css +1 -1
  48. package/package.json +1 -1
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from "react";
4
+ import { Div, Input } from "../../../ui";
5
+ import { FAQAccordion } from "./FAQAccordion";
6
+ const STRIP_HTML = /<[^>]+>/g;
7
+ export function FAQSearchableList({ faqs, labels }) {
8
+ const [query, setQuery] = useState("");
9
+ const filtered = useMemo(() => {
10
+ const q = query.trim().toLowerCase();
11
+ if (!q)
12
+ return faqs;
13
+ return faqs.filter((faq) => {
14
+ const answerText = faq.answer.text.replace(STRIP_HTML, "").toLowerCase();
15
+ return (faq.question.toLowerCase().includes(q) ||
16
+ answerText.includes(q) ||
17
+ (faq.tags?.some((tag) => tag.toLowerCase().includes(q)) ?? false));
18
+ });
19
+ }, [faqs, query]);
20
+ return (_jsxs(Div, { children: [_jsx(Div, { className: "mb-6", children: _jsx(Input, { value: query, onChange: (event) => setQuery(event.target.value), placeholder: labels.searchPlaceholder, "aria-label": labels.searchPlaceholder }) }), _jsx(FAQAccordion, { faqs: filtered, labels: { noResults: labels.noResults } })] }));
21
+ }
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useRef, useState, useCallback, useEffect } from "react";
4
4
  import Image from "next/image";
5
5
  import { Div, Row, Span, Text } from "../../../ui";
6
+ import { resolveMediaUrl } from "../../../utils/media-url";
6
7
  const __P = {
7
8
  p4: "p-[var(--appkit-space-4)]",
8
9
  };
@@ -68,5 +69,5 @@ export function BeforeAfterCard({ item, beforeLabel = "Before", afterLabel = "Af
68
69
  setPosition((p) => Math.max(0, p - 2));
69
70
  if (e.key === "ArrowRight")
70
71
  setPosition((p) => Math.min(100, p + 2));
71
- }, children: [_jsx(Image, { src: item.afterImage, alt: afterLabel, fill: true, sizes: "(max-width: 640px) 100vw, 50vw", className: "object-cover" }), _jsx(Div, { ref: beforeClipRef, className: "absolute inset-0 overflow-hidden", children: _jsx(Image, { src: item.beforeImage, alt: beforeLabel, fill: true, sizes: "(max-width: 640px) 100vw, 50vw", className: "object-cover" }) }), _jsx(Div, { ref: dividerRef, shadow: "md", className: "absolute top-0 bottom-0 z-10 w-0.5 bg-[var(--appkit-color-surface)]", children: _jsx(Row, { className: "absolute top-1/2 left-1/2 h-8 w-8 -translate-x-1/2 -translate-y-1/2 bg-card", align: "center", justify: "center", rounded: "full", shadow: "lg", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 8L1 5M4 8L1 11M4 8H12M12 8L15 5M12 8L15 11", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) }), _jsx(Span, { surface: "overlay-sm", color: "inverse", size: "xs", weight: "medium", className: "absolute top-3 left-3 z-10", padding: "pill-sm", rounded: "full", children: beforeLabel }), _jsx(Span, { surface: "overlay-sm", color: "inverse", size: "xs", weight: "medium", className: "absolute top-3 right-3 z-10", padding: "pill-sm", rounded: "full", children: afterLabel })] }), _jsx(Div, { className: `${__P.p4}`, children: _jsx(Text, { className: "text-foreground", size: "sm", weight: "medium", children: item.caption }) })] }));
72
+ }, children: [_jsx(Image, { src: resolveMediaUrl(item.afterImage), alt: afterLabel, fill: true, sizes: "(max-width: 640px) 100vw, 50vw", className: "object-cover" }), _jsx(Div, { ref: beforeClipRef, className: "absolute inset-0 overflow-hidden", children: _jsx(Image, { src: resolveMediaUrl(item.beforeImage), alt: beforeLabel, fill: true, sizes: "(max-width: 640px) 100vw, 50vw", className: "object-cover" }) }), _jsx(Div, { ref: dividerRef, shadow: "md", className: "absolute top-0 bottom-0 z-10 w-0.5 bg-[var(--appkit-color-surface)]", children: _jsx(Row, { className: "absolute top-1/2 left-1/2 h-8 w-8 -translate-x-1/2 -translate-y-1/2 bg-card", align: "center", justify: "center", rounded: "full", shadow: "lg", children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 8L1 5M4 8L1 11M4 8H12M12 8L15 5M12 8L15 11", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) }), _jsx(Span, { surface: "overlay-sm", color: "inverse", size: "xs", weight: "medium", className: "absolute top-3 left-3 z-10", padding: "pill-sm", rounded: "full", children: beforeLabel }), _jsx(Span, { surface: "overlay-sm", color: "inverse", size: "xs", weight: "medium", className: "absolute top-3 right-3 z-10", padding: "pill-sm", rounded: "full", children: afterLabel })] }), _jsx(Div, { className: `${__P.p4}`, children: _jsx(Text, { className: "text-foreground", size: "sm", weight: "medium", children: item.caption }) })] }));
72
73
  }
@@ -6,6 +6,7 @@ import Link from "next/link";
6
6
  import { THEMED_TEXT_PRIMARY } from "../../../_internal/shared/styles/themed";
7
7
  import { Button, Div, Heading, HorizontalScroller, Row, Section, Text } from "../../../ui";
8
8
  import { ROUTES } from "../../../next";
9
+ import { resolveMediaUrl } from "../../../utils/media-url";
9
10
  import { useTopBrands } from "../hooks/useTopBrands";
10
11
  const __O = {
11
12
  hidden: "overflow-hidden",
@@ -27,7 +28,7 @@ function BrandLogo({ brand }) {
27
28
  const iconSrc = brand.display?.icon;
28
29
  const coverImage = brand.display?.coverImage;
29
30
  const initial = brand.name[0]?.toUpperCase() ?? "?";
30
- return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(brand.slug), className: "flex h-32 w-36 flex-col items-center justify-center gap-[var(--appkit-space-2)] rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] p-[var(--appkit-space-3)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] md:h-40", children: [iconSrc || coverImage ? (_jsx(Image, { src: iconSrc ?? coverImage, alt: brand.name, width: 64, height: 64, className: "h-16 w-16 rounded object-contain" })) : (_jsx(Row, { textWeight: "bold", textSize: "sm", className: "h-16 w-16 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: initial })), _jsx(Text, { className: "w-full truncate", color: "muted", size: "xs", weight: "medium", align: "start", children: brand.name })] }));
31
+ return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(brand.slug), className: "flex h-32 w-36 flex-col items-center justify-center gap-[var(--appkit-space-2)] rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] p-[var(--appkit-space-3)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] md:h-40", children: [iconSrc || coverImage ? (_jsx(Image, { src: resolveMediaUrl(iconSrc ?? coverImage), alt: brand.name, width: 64, height: 64, className: "h-16 w-16 rounded object-contain" })) : (_jsx(Row, { textWeight: "bold", textSize: "sm", className: "h-16 w-16 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: initial })), _jsx(Text, { className: "w-full truncate", color: "muted", size: "xs", weight: "medium", align: "start", children: brand.name })] }));
31
32
  }
32
33
  export function BrandsSection({ title = "Top Brands", subtitle, limit = 12, viewMoreHref, viewMoreLabel = "All brands →", className = "", initialItems, cta, filters, autoScroll = true, scrollInterval = 4000, loop = true, }) {
33
34
  const themed = { textPrimary: THEMED_TEXT_PRIMARY };
@@ -7,6 +7,7 @@ import { GRID_COLS_6_LG } from "../../../_internal/shared/styles/grid";
7
7
  import { useMediaQuery } from "../../../react";
8
8
  import { Button, Div, Heading, Row, Section, Span, Stack, Text } from "../../../ui";
9
9
  import { ROUTES } from "../../../next/routing/route-map";
10
+ import { resolveMediaUrl } from "../../../utils/media-url";
10
11
  const __O = {
11
12
  hidden: "overflow-hidden",
12
13
  };
@@ -203,7 +204,7 @@ function HotspotHeaderOverlay({ heading, subheading, shopAllHref, }) {
203
204
  }, children: "SHOP ALL \u2192" })] }) }));
204
205
  }
205
206
  function HotspotImageLayer({ panoramicImage, panoramicAlt, }) {
206
- return (_jsxs(Div, { className: `absolute inset-0 ${__O.hidden}`, children: [_jsx(Image, { src: panoramicImage, alt: panoramicAlt, fill: true, className: "object-cover object-top", sizes: "100vw", priority: false }), _jsx(Div, { className: "absolute inset-0", style: {
207
+ return (_jsxs(Div, { className: `absolute inset-0 ${__O.hidden}`, children: [_jsx(Image, { src: resolveMediaUrl(panoramicImage), alt: panoramicAlt, fill: true, className: "object-cover object-top", sizes: "100vw", priority: false }), _jsx(Div, { className: "absolute inset-0", style: {
207
208
  background: "linear-gradient(to right, rgba(10,10,18,0.72) 0%, rgba(10,10,18,0.30) 50%, rgba(10,10,18,0.72) 100%)",
208
209
  } }), _jsx(Div, { className: "absolute inset-0", style: {
209
210
  background: "linear-gradient(to bottom, rgba(10,10,18,0.25) 0%, transparent 40%, rgba(10,10,18,0.65) 100%)",
@@ -4,6 +4,7 @@ import { normalizeError } from "../../../errors/normalize";
4
4
  import { useState, useRef } from "react";
5
5
  import Image from "next/image";
6
6
  import { Button, Details, Div, Heading, Input, Label, Li, Row, Span, Stack, Summary, Text, Textarea, Ul } from "../../../ui";
7
+ import { resolveMediaUrl } from "../../../utils/media-url";
7
8
  import { DEFAULT_ACCENT_HEX } from "../lib/franchise-colors";
8
9
  function randomId() {
9
10
  return Math.random().toString(36).slice(2, 9);
@@ -143,7 +144,7 @@ export function CharacterHotspotForm({ initial, onUploadImage, onSave, onAfterSa
143
144
  className: "mt-1 text-center text-[10px]", style: { color: "var(--color-muted)" }, children: s.label })] }, s.key))) }), step === "image" && (_jsxs(Div, { className: CLS_STEP, style: {
144
145
  borderColor: "var(--border-ink)",
145
146
  background: "var(--surface-elevated)",
146
- }, children: [_jsx(Heading, { level: 2, size: "lg", weight: "bold", children: "Upload Background Image" }), _jsx(Text, { style: { color: "var(--color-muted)" }, size: "sm", children: "Choose a wide panoramic image that shows all the characters. You will place pins on it in the next step." }), _jsxs(Label, { layout: "flex-col-center", gap: "md", rounded: "lg", padding: "xl", className: "cursor-pointer border-2 border-dashed transition-colors hover:opacity-80", style: { borderColor: "var(--border-ink)" }, children: [_jsx(Span, { size: "3xl", children: "\uD83D\uDDBC" }), _jsx(Span, { weight: "medium", children: uploading ? "Uploading…" : "Click to choose image" }), _jsx(Span, { size: "xs", style: { color: "var(--color-muted)" }, children: "JPG, PNG, WebP \u2014 wide landscape images work best" }), _jsx(Input, { type: "file", accept: "image/*", className: "hidden", onChange: handleImageUpload, disabled: uploading })] }), imageUrl && (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: CLS_IMG_WRAP, style: { paddingTop: "37.5%" }, children: _jsx(Image, { src: imageUrl, alt: imageAlt, fill: true, className: "object-cover", sizes: "680px" }) }), _jsx(Text, { className: CLS_SUCCESS_TEXT, children: "\u2713 Image uploaded" })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "bold", children: "Image Alt Text" }), _jsx(Input, { type: "text", value: imageAlt, onChange: (e) => setImageAlt(e.target.value), placeholder: "DC, Marvel and Anime characters", className: CLS_INPUT, style: {
147
+ }, children: [_jsx(Heading, { level: 2, size: "lg", weight: "bold", children: "Upload Background Image" }), _jsx(Text, { style: { color: "var(--color-muted)" }, size: "sm", children: "Choose a wide panoramic image that shows all the characters. You will place pins on it in the next step." }), _jsxs(Label, { layout: "flex-col-center", gap: "md", rounded: "lg", padding: "xl", className: "cursor-pointer border-2 border-dashed transition-colors hover:opacity-80", style: { borderColor: "var(--border-ink)" }, children: [_jsx(Span, { size: "3xl", children: "\uD83D\uDDBC" }), _jsx(Span, { weight: "medium", children: uploading ? "Uploading…" : "Click to choose image" }), _jsx(Span, { size: "xs", style: { color: "var(--color-muted)" }, children: "JPG, PNG, WebP \u2014 wide landscape images work best" }), _jsx(Input, { type: "file", accept: "image/*", className: "hidden", onChange: handleImageUpload, disabled: uploading })] }), imageUrl && (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: CLS_IMG_WRAP, style: { paddingTop: "37.5%" }, children: _jsx(Image, { src: resolveMediaUrl(imageUrl), alt: imageAlt, fill: true, className: "object-cover", sizes: "680px" }) }), _jsx(Text, { className: CLS_SUCCESS_TEXT, children: "\u2713 Image uploaded" })] })), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "bold", children: "Image Alt Text" }), _jsx(Input, { type: "text", value: imageAlt, onChange: (e) => setImageAlt(e.target.value), placeholder: "DC, Marvel and Anime characters", className: CLS_INPUT, style: {
147
148
  borderColor: "var(--border-ink)",
148
149
  background: "var(--surface-elevated)",
149
150
  color: "var(--color-black)",
@@ -160,7 +161,7 @@ export function CharacterHotspotForm({ initial, onUploadImage, onSave, onAfterSa
160
161
  paddingTop: "56.25%",
161
162
  cursor: "crosshair",
162
163
  background: "black",
163
- }, onClick: handleImageClick, children: [_jsx(Image, { src: imageUrl, alt: imageAlt, fill: true, className: "object-cover", sizes: "(max-width: 680px) 100vw" }), pins.map((pin) => (_jsxs(Div, { className: "pointer-events-none absolute", style: {
164
+ }, onClick: handleImageClick, children: [_jsx(Image, { src: resolveMediaUrl(imageUrl), alt: imageAlt, fill: true, className: "object-cover", sizes: "(max-width: 680px) 100vw" }), pins.map((pin) => (_jsxs(Div, { className: "pointer-events-none absolute", style: {
164
165
  left: `${pin.xPct}%`,
165
166
  top: `${pin.yPct}%`,
166
167
  transform: STY_CENTERED,
@@ -289,7 +290,7 @@ export function CharacterHotspotForm({ initial, onUploadImage, onSave, onAfterSa
289
290
  }, children: "Save Pin & Add Another \u2192" })] })] })] })), step === "review" && (_jsxs(Div, { className: CLS_STEP, style: {
290
291
  borderColor: "var(--border-ink)",
291
292
  background: "var(--surface-elevated)",
292
- }, children: [_jsxs(Row, { justify: "between", children: [_jsx(Heading, { level: 2, size: "lg", weight: "bold", children: "Review & Save" }), _jsxs(Span, { rounded: "full", padding: "pill-md", size: "sm", weight: "bold", style: { background: "var(--surface-warm)" }, children: [pins.length, " pin", pins.length !== 1 ? "s" : ""] })] }), _jsxs(Div, { className: CLS_IMG_WRAP, style: { paddingTop: "56.25%", background: "black" }, children: [_jsx(Image, { src: imageUrl, alt: imageAlt, fill: true, className: "object-cover", sizes: "(max-width: 680px) 100vw" }), pins.map((pin) => (_jsxs(Div, { className: "pointer-events-none absolute", style: {
293
+ }, children: [_jsxs(Row, { justify: "between", children: [_jsx(Heading, { level: 2, size: "lg", weight: "bold", children: "Review & Save" }), _jsxs(Span, { rounded: "full", padding: "pill-md", size: "sm", weight: "bold", style: { background: "var(--surface-warm)" }, children: [pins.length, " pin", pins.length !== 1 ? "s" : ""] })] }), _jsxs(Div, { className: CLS_IMG_WRAP, style: { paddingTop: "56.25%", background: "black" }, children: [_jsx(Image, { src: resolveMediaUrl(imageUrl), alt: imageAlt, fill: true, className: "object-cover", sizes: "(max-width: 680px) 100vw" }), pins.map((pin) => (_jsxs(Div, { className: "pointer-events-none absolute", style: {
293
294
  left: `${pin.xPct}%`,
294
295
  top: `${pin.yPct}%`,
295
296
  transform: STY_CENTERED,
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef, useCallback } from "react";
4
4
  import Image from "next/image";
5
5
  import Link from "next/link";
6
6
  import { Button, Div, Heading, Row, Span, Stack } from "../../../ui";
7
+ import { resolveMediaUrl } from "../../../utils/media-url";
7
8
  export function HeroBanner({ banners, autoplayMs = 5000 }) {
8
9
  const [current, setCurrent] = useState(0);
9
10
  const [animating, setAnimating] = useState(false);
@@ -38,5 +39,5 @@ export function HeroBanner({ banners, autoplayMs = 5000 }) {
38
39
  }, [banner.backgroundColor, banner.textColor]);
39
40
  if (banners.length === 0)
40
41
  return null;
41
- return (_jsxs(Div, { ref: rootRef, className: "relative overflow-hidden min-h-svh [background:var(--hero-bg,var(--dark-section-deep))]", children: [banners.map((b, i) => (_jsx(Div, { className: `absolute inset-0 transition-opacity duration-700 z-[1] ${i === current ? "opacity-100" : "opacity-0"}`, children: b.backgroundImage && (_jsx(Image, { src: b.backgroundImage, alt: b.title, fill: true, className: "object-cover object-center", priority: i === 0, sizes: "100vw" })) }, b.id))), _jsx(Div, { className: "absolute inset-0 z-[2] [background:linear-gradient(110deg,rgba(0,0,0,0.82)_0%,rgba(0,0,0,0.45)_55%,rgba(0,0,0,0.18)_100%)]" }), _jsx(Div, { className: "absolute inset-0 z-[3] [background:linear-gradient(to_bottom,rgba(0,0,0,0.08)_0%,transparent_30%,rgba(0,0,0,0.55)_100%)]" }), _jsxs(Stack, { justify: "end", className: "relative pb-[4rem] sm:px-[3rem] sm:pb-[6rem] lg:px-[5rem] min-h-svh z-[10] pt-[var(--header-height,4rem)]", padding: "x-md", children: [banner.subtitle && (_jsx(Div, { className: "mb-3", children: _jsx(Span, { className: "inline-block font-black uppercase tracking-[0.18em] bg-[var(--color-red)] text-white [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.16em]", padding: "pill-md", size: "xs", children: banner.subtitle }) })), _jsx(Heading, { level: 2, textShadow: "soft-dark", className: "mb-5 max-w-xl leading-none [font-family:var(--font-bangers,Bangers,cursive)] text-[clamp(3rem,9vw,6.5rem)] tracking-[0.04em] text-[var(--hero-text,white)]", children: banner.title }), banner.ctaLabel && banner.ctaUrl && (_jsx(Row, { wrap: true, gap: "md", children: _jsxs(Link, { href: banner.ctaUrl, className: "inline-flex items-center gap-[var(--appkit-space-2)] sm:gap-[var(--appkit-space-3)] px-[var(--appkit-space-5)] py-[var(--appkit-space-3)] sm:px-[var(--appkit-space-8)] sm:py-[var(--appkit-space-3-5)] font-black uppercase transition-all hover:-translate-y-0.5 [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.1em] text-[clamp(0.85rem,2.5vw,1.1rem)] bg-[var(--color-yellow)] text-[var(--appkit-color-text)] [box-shadow:0_4px_24px_rgba(255,229,0,0.35)]", children: [banner.ctaLabel, _jsx(Span, { "aria-hidden": "true", className: "leading-none", smSize: "xl", size: "lg", children: "\u2192" })] }) }))] }), banners.length > 1 && (_jsxs(_Fragment, { children: [_jsx(Button, { rounded: "full", onClick: prev, "aria-label": "Previous slide", variant: "ghost", size: "sm", className: "absolute left-3 sm:left-6 top-1/2 -translate-y-1/2 justify-[center] transition-all hover:scale-105 z-[15] w-10 h-10 bg-[rgba(255,255,255,0.12)] border border-white/20 text-white backdrop-blur-sm", children: _jsx("svg", { className: "h-4 w-4 sm:h-5 sm:w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { rounded: "full", onClick: next, "aria-label": "Next slide", variant: "ghost", size: "sm", className: "absolute right-3 sm:right-6 top-1/2 -translate-y-1/2 justify-[center] transition-all hover:scale-105 z-[15] w-10 h-10 bg-[rgba(255,255,255,0.12)] border border-white/20 text-white backdrop-blur-sm", children: _jsx("svg", { className: "h-4 w-4 sm:h-5 sm:w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M9 5l7 7-7 7" }) }) })] })), banners.length > 1 && (_jsxs(Row, { className: "absolute bottom-8 right-6 sm:right-12 z-[15]", gap: "3", children: [_jsxs(Span, { className: "font-black tabular-nums text-white/60 [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.1em]", size: "xs", children: [String(current + 1).padStart(2, "0"), " /", " ", String(banners.length).padStart(2, "0")] }), _jsx(Row, { gap: "xs", className: "", children: banners.map((_, i) => (_jsx(Button, { onClick: () => goTo(i), "aria-label": `Go to slide ${i + 1}`, variant: "ghost", size: "sm", className: `transition-all h-[3px] rounded-[2px] ${i === current ? "w-7 bg-[var(--color-yellow)]" : "w-2.5 bg-white/35"}` }, i))) })] })), _jsx(Stack, { className: "absolute bottom-6 left-1/2 -translate-x-1/2 z-[15] opacity-50", align: "center", gap: "xs", "aria-hidden": "true", children: _jsx(Div, { className: "h-8 w-px [background:linear-gradient(to_bottom,rgba(255,255,255,0),rgba(255,255,255,0.8))]" }) })] }));
42
+ return (_jsxs(Div, { ref: rootRef, className: "relative overflow-hidden min-h-svh [background:var(--hero-bg,var(--dark-section-deep))]", children: [banners.map((b, i) => (_jsx(Div, { className: `absolute inset-0 transition-opacity duration-700 z-[1] ${i === current ? "opacity-100" : "opacity-0"}`, children: b.backgroundImage && (_jsx(Image, { src: resolveMediaUrl(b.backgroundImage), alt: b.title, fill: true, className: "object-cover object-center", priority: i === 0, sizes: "100vw" })) }, b.id))), _jsx(Div, { className: "absolute inset-0 z-[2] [background:linear-gradient(110deg,rgba(0,0,0,0.82)_0%,rgba(0,0,0,0.45)_55%,rgba(0,0,0,0.18)_100%)]" }), _jsx(Div, { className: "absolute inset-0 z-[3] [background:linear-gradient(to_bottom,rgba(0,0,0,0.08)_0%,transparent_30%,rgba(0,0,0,0.55)_100%)]" }), _jsxs(Stack, { justify: "end", className: "relative pb-[4rem] sm:px-[3rem] sm:pb-[6rem] lg:px-[5rem] min-h-svh z-[10] pt-[var(--header-height,4rem)]", padding: "x-md", children: [banner.subtitle && (_jsx(Div, { className: "mb-3", children: _jsx(Span, { className: "inline-block font-black uppercase tracking-[0.18em] bg-[var(--color-red)] text-white [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.16em]", padding: "pill-md", size: "xs", children: banner.subtitle }) })), _jsx(Heading, { level: 2, textShadow: "soft-dark", className: "mb-5 max-w-xl leading-none [font-family:var(--font-bangers,Bangers,cursive)] text-[clamp(3rem,9vw,6.5rem)] tracking-[0.04em] text-[var(--hero-text,white)]", children: banner.title }), banner.ctaLabel && banner.ctaUrl && (_jsx(Row, { wrap: true, gap: "md", children: _jsxs(Link, { href: banner.ctaUrl, className: "inline-flex items-center gap-[var(--appkit-space-2)] sm:gap-[var(--appkit-space-3)] px-[var(--appkit-space-5)] py-[var(--appkit-space-3)] sm:px-[var(--appkit-space-8)] sm:py-[var(--appkit-space-3-5)] font-black uppercase transition-all hover:-translate-y-0.5 [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.1em] text-[clamp(0.85rem,2.5vw,1.1rem)] bg-[var(--color-yellow)] text-[var(--appkit-color-text)] [box-shadow:0_4px_24px_rgba(255,229,0,0.35)]", children: [banner.ctaLabel, _jsx(Span, { "aria-hidden": "true", className: "leading-none", smSize: "xl", size: "lg", children: "\u2192" })] }) }))] }), banners.length > 1 && (_jsxs(_Fragment, { children: [_jsx(Button, { rounded: "full", onClick: prev, "aria-label": "Previous slide", variant: "ghost", size: "sm", className: "absolute left-3 sm:left-6 top-1/2 -translate-y-1/2 justify-[center] transition-all hover:scale-105 z-[15] w-10 h-10 bg-[rgba(255,255,255,0.12)] border border-white/20 text-white backdrop-blur-sm", children: _jsx("svg", { className: "h-4 w-4 sm:h-5 sm:w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { rounded: "full", onClick: next, "aria-label": "Next slide", variant: "ghost", size: "sm", className: "absolute right-3 sm:right-6 top-1/2 -translate-y-1/2 justify-[center] transition-all hover:scale-105 z-[15] w-10 h-10 bg-[rgba(255,255,255,0.12)] border border-white/20 text-white backdrop-blur-sm", children: _jsx("svg", { className: "h-4 w-4 sm:h-5 sm:w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M9 5l7 7-7 7" }) }) })] })), banners.length > 1 && (_jsxs(Row, { className: "absolute bottom-8 right-6 sm:right-12 z-[15]", gap: "3", children: [_jsxs(Span, { className: "font-black tabular-nums text-white/60 [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.1em]", size: "xs", children: [String(current + 1).padStart(2, "0"), " /", " ", String(banners.length).padStart(2, "0")] }), _jsx(Row, { gap: "xs", className: "", children: banners.map((_, i) => (_jsx(Button, { onClick: () => goTo(i), "aria-label": `Go to slide ${i + 1}`, variant: "ghost", size: "sm", className: `transition-all h-[3px] rounded-[2px] ${i === current ? "w-7 bg-[var(--color-yellow)]" : "w-2.5 bg-white/35"}` }, i))) })] })), _jsx(Stack, { className: "absolute bottom-6 left-1/2 -translate-x-1/2 z-[15] opacity-50", align: "center", gap: "xs", "aria-hidden": "true", children: _jsx(Div, { className: "h-8 w-px [background:linear-gradient(to_bottom,rgba(255,255,255,0),rgba(255,255,255,0.8))]" }) })] }));
42
43
  }
@@ -2,13 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Image from "next/image";
3
3
  import Link from "next/link";
4
4
  import { Div, Heading, Row, Section, Span, Text } from "../../../ui";
5
+ import { resolveMediaUrl } from "../../../utils/media-url";
5
6
  const __P = {
6
7
  p4: "p-[var(--appkit-space-4)]",
7
8
  };
8
9
  export function PromoGrid({ banners, eyebrow = "LIMITED TIME", heading = "HOT DEALS & PROMOS", }) {
9
10
  if (banners.length === 0)
10
11
  return null;
11
- return (_jsx(Section, { paddingY: "y-4xl", className: "bg-[var(--dark-section-alt)] [border-top:1px_solid_rgba(255,255,255,0.06)] [border-bottom:1px_solid_rgba(255,255,255,0.06)]", children: _jsxs(Div, { className: "mx-auto max-w-7xl", padding: "x-md", children: [_jsx(Row, { wrap: true, align: "end", justify: "between", gap: "sm", className: "mb-7 gap-y-2", children: _jsxs(Div, { children: [_jsx(Text, { className: "mb-1 font-black uppercase tracking-widest text-[var(--color-red)] tracking-[0.18em]", size: "xs", children: eyebrow }), _jsx(Heading, { level: 2, className: "[font-family:var(--font-bangers,Bangers,cursive)] text-[clamp(1.6rem,4vw,2.4rem)] tracking-[0.08em] text-white leading-none", children: heading })] }) }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: banners.slice(0, 4).map((banner, i) => (_jsxs(Link, { href: banner.ctaUrl, className: `group relative block overflow-hidden bg-[var(--dark-section-card)] ${i === 0 ? "sm:row-span-2 min-h-[clamp(220px,32vh,480px)]" : "min-h-[clamp(130px,16vh,240px)]"}`, children: [_jsx(Image, { src: banner.image, alt: banner.title, fill: true, className: "object-cover transition-transform duration-500 group-hover:scale-105", sizes: i === 0
12
+ return (_jsx(Section, { paddingY: "y-4xl", className: "bg-[var(--dark-section-alt)] [border-top:1px_solid_rgba(255,255,255,0.06)] [border-bottom:1px_solid_rgba(255,255,255,0.06)]", children: _jsxs(Div, { className: "mx-auto max-w-7xl", padding: "x-md", children: [_jsx(Row, { wrap: true, align: "end", justify: "between", gap: "sm", className: "mb-7 gap-y-2", children: _jsxs(Div, { children: [_jsx(Text, { className: "mb-1 font-black uppercase tracking-widest text-[var(--color-red)] tracking-[0.18em]", size: "xs", children: eyebrow }), _jsx(Heading, { level: 2, className: "[font-family:var(--font-bangers,Bangers,cursive)] text-[clamp(1.6rem,4vw,2.4rem)] tracking-[0.08em] text-white leading-none", children: heading })] }) }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", children: banners.slice(0, 4).map((banner, i) => (_jsxs(Link, { href: banner.ctaUrl, className: `group relative block overflow-hidden bg-[var(--dark-section-card)] ${i === 0 ? "sm:row-span-2 min-h-[clamp(220px,32vh,480px)]" : "min-h-[clamp(130px,16vh,240px)]"}`, children: [_jsx(Image, { src: resolveMediaUrl(banner.image), alt: banner.title, fill: true, className: "object-cover transition-transform duration-500 group-hover:scale-105", sizes: i === 0
12
13
  ? "(max-width: 640px) 100vw, 40vw"
13
14
  : "(max-width: 640px) 50vw, 30vw" }), _jsx(Div, { className: `absolute inset-0 ${i === 0 ? "[background:linear-gradient(to_top,rgba(0,0,0,0.9)_0%,rgba(0,0,0,0.3)_50%,transparent_100%)]" : "[background:linear-gradient(to_top,rgba(0,0,0,0.85)_0%,rgba(0,0,0,0.15)_100%)]"}` }), _jsx(Div, { className: "absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100 ring-2 ring-[var(--color-yellow)] ring-inset" }), _jsxs(Div, { className: `absolute bottom-0 left-0 right-0 ${__P.p4}`, children: [_jsx(Text, { className: `leading-tight [font-family:var(--font-bangers,Bangers,cursive)] tracking-[0.06em] text-white ${i === 0 ? "text-[1.35rem]" : "text-[1rem]"}`, children: banner.title }), _jsxs(Span, { layout: "inline-flex", gap: "xs", className: "mt-1 font-black uppercase text-[var(--color-yellow)]", size: "xs", children: [banner.ctaLabel, " \u2192"] })] })] }, banner.id))) })] }) }));
14
15
  }
@@ -6,6 +6,7 @@ import Link from "next/link";
6
6
  import { THEMED_TEXT_PRIMARY } from "../../../_internal/shared/styles/themed";
7
7
  import { Button, Div, Heading, HorizontalScroller, Row, Section, Span, Stack, Text } from "../../../ui";
8
8
  import { ROUTES } from "../../../next";
9
+ import { resolveMediaUrl } from "../../../utils/media-url";
9
10
  import { useTopCategories } from "../hooks/useTopCategories";
10
11
  import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view";
11
12
  const __P = {
@@ -22,7 +23,7 @@ function CategoryChip({ category }) {
22
23
  const coverImage = category.display?.coverImage;
23
24
  const initial = category.name[0]?.toUpperCase() ?? "?";
24
25
  const productCount = category.metrics?.productCount ?? 0;
25
- return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug), className: "group flex w-full min-h-[180px] sm:min-h-[220px] flex-col overflow-hidden rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] dark:hover:border-primary-600", children: [coverImage && isImageUrl(coverImage) ? (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, surface: "subtle", children: _jsx(Image, { src: coverImage, alt: category.name, width: 320, height: 180, className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Div, { surface: "muted", className: "aspect-video w-full" })), _jsxs(Stack, { className: `flex-1 ${__P.p3} text-left`, children: [_jsx(Row, { textWeight: "bold", textSize: "sm", className: "mb-2 h-9 w-9 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: iconSrc && isImageUrl(iconSrc) ? (_jsx(Image, { src: iconSrc, alt: "", width: 24, height: 24, className: "h-6 w-6 rounded object-cover", "aria-hidden": true })) : iconSrc ? (_jsx(Span, { size: "lg", "aria-hidden": "true", className: "leading-none", children: iconSrc })) : (initial) }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", truncate: 2, children: category.name }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: [productCount.toLocaleString(), " items"] }), _jsx(Text, { className: "mt-auto text-primary dark:text-primary-400 pt-[0.75rem]", size: "xs", weight: "medium", children: "Browse category \u2192" })] })] }));
26
+ return (_jsxs(Link, { href: ROUTES.PUBLIC.CATEGORY_DETAIL(category.slug), className: "group flex w-full min-h-[180px] sm:min-h-[220px] flex-col overflow-hidden rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] dark:hover:border-primary-600", children: [coverImage && isImageUrl(coverImage) ? (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, surface: "subtle", children: _jsx(Image, { src: resolveMediaUrl(coverImage), alt: category.name, width: 320, height: 180, className: "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Div, { surface: "muted", className: "aspect-video w-full" })), _jsxs(Stack, { className: `flex-1 ${__P.p3} text-left`, children: [_jsx(Row, { textWeight: "bold", textSize: "sm", className: "mb-2 h-9 w-9 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: iconSrc && isImageUrl(iconSrc) ? (_jsx(Image, { src: resolveMediaUrl(iconSrc), alt: "", width: 24, height: 24, className: "h-6 w-6 rounded object-cover", "aria-hidden": true })) : iconSrc ? (_jsx(Span, { size: "lg", "aria-hidden": "true", className: "leading-none", children: iconSrc })) : (initial) }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", truncate: 2, children: category.name }), _jsxs(Text, { className: "mt-1", color: "muted", size: "xs", children: [productCount.toLocaleString(), " items"] }), _jsx(Text, { className: "mt-auto text-primary dark:text-primary-400 pt-[0.75rem]", size: "xs", weight: "medium", children: "Browse category \u2192" })] })] }));
26
27
  }
27
28
  const CTA_CLASSES = {
28
29
  filled: "rounded-lg bg-[var(--appkit-color-primary)] px-[var(--appkit-space-5)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-semibold text-white hover:opacity-90",
@@ -3,10 +3,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useEffect, useState, useCallback } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import Link from "next/link";
6
- import { Button, Div, IconButton, Li, Nav, Row, Span, Stack, Text, Ul } from "../../../ui";
6
+ import { Button, Div, IconButton, Input, Li, Nav, Row, Span, Stack, Text, Ul } from "../../../ui";
7
7
  import { MediaImage } from "../../media/MediaImage";
8
8
  import { BottomSheet } from "../../layout/BottomSheet";
9
9
  import { SidebarCollapseToggle } from "../../../_internal/client/features/layout/SidebarCollapseToggle";
10
+ import { useSidebarSearch } from "../../../_internal/client/features/layout/useSidebarSearch";
10
11
  const __O = {
11
12
  hidden: "overflow-hidden",
12
13
  yAuto: "overflow-y-auto",
@@ -37,14 +38,19 @@ function GroupsContent({ groups, activeHref, storeName, storeLogoURL, onItemClic
37
38
  const match = groups.find((g) => g.defaultOpen ?? g.items.some((i) => activeHref === i.href));
38
39
  return match?.title ?? null;
39
40
  });
40
- const toggle = useCallback((title) => setOpenGroup((p) => (p === title ? null : title)), []);
41
- return (_jsxs(_Fragment, { children: [storeName && (_jsxs(Row, { border: "bottom-subtle", gap: "3", padding: "inline", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName, size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName })] })), _jsx(Nav, { "aria-label": "Store navigation", padding: "y-xs", children: groups.map((group) => {
42
- const isOpen = openGroup === group.title;
43
- const hasActive = group.items.some((i) => activeHref === i.href);
44
- return (_jsxs(Div, { className: "mb-0.5", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => toggle(group.title), paddingX: "md", paddingY: "sm", weight: "semibold", rounded: "none", className: `w-full text-[0.6875rem] uppercase tracking-widest transition-colors ${hasActive && !isOpen
45
- ? CLS_NAV_ICON_ACTIVE
46
- : "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: isNavItemActive(item, activeHref), onClick: onItemClick }) }, item.href))) }))] }, group.title));
47
- }) })] }));
41
+ const { query, setQuery, isSearching, filteredGroups } = useSidebarSearch(groups);
42
+ const toggle = useCallback((title) => {
43
+ if (isSearching)
44
+ return;
45
+ setOpenGroup((p) => (p === title ? null : title));
46
+ }, [isSearching]);
47
+ return (_jsxs(_Fragment, { children: [storeName && (_jsxs(Row, { border: "bottom-subtle", gap: "3", padding: "inline", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName, size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName })] })), _jsxs(Nav, { "aria-label": "Store navigation", padding: "y-xs", children: [_jsx(Div, { padding: "x-md", paddingY: "y-sm", children: _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search navigation\u2026", "aria-label": "Search navigation" }) }), isSearching && filteredGroups.length === 0 && (_jsx(Div, { padding: "x-md", paddingY: "y-sm", children: _jsxs(Span, { size: "xs", color: "muted", children: ["No matches for \u201C", query, "\u201D."] }) })), filteredGroups.map((group) => {
48
+ const isOpen = isSearching || openGroup === group.title;
49
+ const hasActive = group.items.some((i) => activeHref === i.href);
50
+ return (_jsxs(Div, { className: "mb-0.5", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => toggle(group.title), paddingX: "md", paddingY: "sm", weight: "semibold", rounded: "none", className: `w-full text-[0.6875rem] uppercase tracking-widest transition-colors ${hasActive && !isOpen
51
+ ? CLS_NAV_ICON_ACTIVE
52
+ : "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: isNavItemActive(item, activeHref), onClick: onItemClick }) }, item.href))) }))] }, group.title));
53
+ })] })] }));
48
54
  }
49
55
  function DrawerPanel({ title, onClose, children }) {
50
56
  return (_jsxs(Div, { className: "hidden lg:block", children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 z-40 backdrop-blur-sm", onClick: onClose, "aria-hidden": "true" }), _jsxs(Stack, { border: "default", shadow: "2xl", role: "dialog", "aria-modal": "true", "aria-label": title, className: "fixed top-0 right-0 z-50 h-full w-64 border-l", surface: "default", children: [_jsxs(Row, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", align: "center", justify: "between", children: [_jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: title }), _jsx(IconButton, { "aria-label": "Close", variant: "ghost", size: "sm", onClick: onClose, icon: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: children })] })] }));
@@ -10,6 +10,7 @@ export declare const checklistItemCreateSchema: z.ZodObject<{
10
10
  href: z.ZodOptional<z.ZodString>;
11
11
  order: z.ZodDefault<z.ZodNumber>;
12
12
  isActive: z.ZodDefault<z.ZodBoolean>;
13
+ adminOnly: z.ZodDefault<z.ZodBoolean>;
13
14
  }, "strip", z.ZodTypeAny, {
14
15
  label: string;
15
16
  order: number;
@@ -18,6 +19,7 @@ export declare const checklistItemCreateSchema: z.ZodObject<{
18
19
  pageKey: string;
19
20
  groupLabel: string;
20
21
  pageLabel: string;
22
+ adminOnly: boolean;
21
23
  href?: string | undefined;
22
24
  description?: string | undefined;
23
25
  }, {
@@ -30,6 +32,7 @@ export declare const checklistItemCreateSchema: z.ZodObject<{
30
32
  description?: string | undefined;
31
33
  order?: number | undefined;
32
34
  isActive?: boolean | undefined;
35
+ adminOnly?: boolean | undefined;
33
36
  }>;
34
37
  export declare const checklistItemUpdateSchema: z.ZodObject<{
35
38
  groupKey: z.ZodOptional<z.ZodString>;
@@ -41,6 +44,7 @@ export declare const checklistItemUpdateSchema: z.ZodObject<{
41
44
  href: z.ZodOptional<z.ZodOptional<z.ZodString>>;
42
45
  order: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
43
46
  isActive: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
47
+ adminOnly: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
44
48
  }, "strip", z.ZodTypeAny, {
45
49
  label?: string | undefined;
46
50
  href?: string | undefined;
@@ -51,6 +55,7 @@ export declare const checklistItemUpdateSchema: z.ZodObject<{
51
55
  pageKey?: string | undefined;
52
56
  groupLabel?: string | undefined;
53
57
  pageLabel?: string | undefined;
58
+ adminOnly?: boolean | undefined;
54
59
  }, {
55
60
  label?: string | undefined;
56
61
  href?: string | undefined;
@@ -61,6 +66,7 @@ export declare const checklistItemUpdateSchema: z.ZodObject<{
61
66
  pageKey?: string | undefined;
62
67
  groupLabel?: string | undefined;
63
68
  pageLabel?: string | undefined;
69
+ adminOnly?: boolean | undefined;
64
70
  }>;
65
71
  export type ChecklistItemCreateInput = z.infer<typeof checklistItemCreateSchema>;
66
72
  export type ChecklistItemUpdateInput = z.infer<typeof checklistItemUpdateSchema>;
@@ -12,6 +12,7 @@ const checklistItemBaseSchema = z.object({
12
12
  href: z.string().max(300).optional(),
13
13
  order: z.number().int().min(0).default(0),
14
14
  isActive: z.boolean().default(true),
15
+ adminOnly: z.boolean().default(false),
15
16
  });
16
17
  export const checklistItemCreateSchema = checklistItemBaseSchema;
17
18
  export const checklistItemUpdateSchema = checklistItemBaseSchema.partial();
@@ -27,6 +28,7 @@ export async function createChecklistItem(input) {
27
28
  href: input.href,
28
29
  order: input.order ?? 0,
29
30
  isActive: input.isActive ?? true,
31
+ adminOnly: input.adminOnly ?? false,
30
32
  });
31
33
  serverLogger.info("createChecklistItem", { itemId: item.id });
32
34
  return item;
@@ -7,11 +7,12 @@ import { apiClient } from "../../../http";
7
7
  import { useApiMutation } from "../../../client";
8
8
  import { useSession } from "../../../react";
9
9
  import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints";
10
+ import { isAdminUser } from "../../auth/role-predicates";
10
11
  import { TesterChecklistStepRow } from "./TesterChecklistStepRow";
11
12
  function matchesQuery(item, query) {
12
13
  if (!query)
13
14
  return true;
14
- const haystack = `${item.label} ${item.description ?? ""} ${item.groupLabel} ${item.pageLabel}`.toLowerCase();
15
+ const haystack = `${item.label} ${item.description ?? ""} ${item.groupLabel} ${item.pageLabel} ${item.href ?? ""}`.toLowerCase();
15
16
  return haystack.includes(query.toLowerCase());
16
17
  }
17
18
  export function TesterHubView({ sandboxExpiresAt }) {
@@ -31,8 +32,8 @@ export function TesterHubView({ sandboxExpiresAt }) {
31
32
  void queryClient.invalidateQueries({ queryKey: ["user", "tester-checklist"] });
32
33
  },
33
34
  });
34
- if (!user?.isTester) {
35
- return (_jsx(Alert, { variant: "warning", title: "Testers only", children: "This page is only available to accounts flagged as testers. Contact an admin if you believe this is a mistake." }));
35
+ if (!user?.isTester && !isAdminUser(user)) {
36
+ return (_jsx(Alert, { variant: "warning", title: "Testers only", children: "This page is only available to accounts flagged as testers (and admins). Contact an admin if you believe this is a mistake." }));
36
37
  }
37
38
  const items = query.data?.items ?? [];
38
39
  const filtered = items.filter((item) => matchesQuery(item, search));
@@ -53,7 +54,7 @@ export function TesterHubView({ sandboxExpiresAt }) {
53
54
  const handleSaveNote = async (checklistItemId, comment, screenshotUrl) => {
54
55
  await upsertMutation.mutateAsync({ checklistItemId, patch: { comment, screenshotUrl } });
55
56
  };
56
- return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Hub" }), _jsx(Text, { color: "muted", children: "Work through the checklist below and answer Yes/No for each test case. Add a comment and screenshot wherever something looks off \u2014 colors, styles, readability, bugs. Your answers save automatically." }), sandboxExpiresAt && (_jsxs(Alert, { variant: "info", title: "Shared test sandbox", children: ["The shared test store/products/categories expire on ", new Date(sandboxExpiresAt).toLocaleDateString(), "."] })), _jsx(FieldInput, { name: "checklist-search", label: "Search test cases", value: search, onChange: setSearch, placeholder: "e.g. checkout, google, bidding..." }), query.isLoading && _jsx(Text, { color: "muted", children: "Loading checklist\u2026" }), Array.from(groups.entries()).map(([groupKey, group]) => (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 3, children: group.groupLabel }), Array.from(group.pages.entries()).map(([pageKey, page]) => {
57
+ return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Hub" }), _jsx(Text, { color: "muted", children: "Work through the checklist below and answer Yes/No for each test case. Add a comment and screenshot wherever something looks off \u2014 colors, styles, readability, bugs. Your answers save automatically." }), sandboxExpiresAt && (_jsxs(Alert, { variant: "info", title: "Shared test sandbox", children: ["The shared test store/products/categories expire on ", new Date(sandboxExpiresAt).toLocaleDateString(), "."] })), _jsx(FieldInput, { name: "checklist-search", label: "Search test cases", value: search, onChange: setSearch, placeholder: "Search by title or route, e.g. checkout, /store/payouts..." }), query.isLoading && _jsx(Text, { color: "muted", children: "Loading checklist\u2026" }), Array.from(groups.entries()).map(([groupKey, group]) => (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 3, children: group.groupLabel }), Array.from(group.pages.entries()).map(([pageKey, page]) => {
57
58
  const answeredCount = page.items.filter((i) => i.answer).length;
58
59
  return (_jsxs(Details, { tone: "card", defaultOpen: false, children: [_jsxs(Summary, { children: [page.pageLabel, " (", answeredCount, "/", page.items.length, " answered)"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: page.items
59
60
  .slice()
@@ -17,13 +17,14 @@ export interface TesterChecklistItemDocument extends BaseDocument {
17
17
  order: number;
18
18
  isActive: boolean;
19
19
  searchTokens: string[];
20
+ adminOnly?: boolean;
20
21
  }
21
22
  export declare const TESTER_CHECKLIST_ITEM_COLLECTION: "testerChecklistItems";
22
23
  export declare const TESTER_CHECKLIST_ITEM_ID_PREFIX: "checklist-";
23
24
  export declare const TESTER_CHECKLIST_ITEM_INDEXED_FIELDS: readonly ["groupKey", "pageKey", "order", "isActive", "searchTokens", "createdAt"];
24
25
  export declare const DEFAULT_TESTER_CHECKLIST_ITEM_DATA: Partial<TesterChecklistItemDocument>;
25
26
  export type TesterChecklistItemCreateInput = Omit<TesterChecklistItemDocument, "id" | "createdAt" | "updatedAt" | "searchTokens">;
26
- export type TesterChecklistItemUpdateInput = Partial<Pick<TesterChecklistItemDocument, "groupKey" | "groupLabel" | "pageKey" | "pageLabel" | "label" | "description" | "href" | "order" | "isActive">>;
27
+ export type TesterChecklistItemUpdateInput = Partial<Pick<TesterChecklistItemDocument, "groupKey" | "groupLabel" | "pageKey" | "pageLabel" | "label" | "description" | "href" | "order" | "isActive" | "adminOnly">>;
27
28
  export declare const testerChecklistItemQueryHelpers: {
28
29
  readonly byGroup: (groupKey: string) => readonly ["groupKey", "==", string];
29
30
  readonly byPage: (pageKey: string) => readonly ["pageKey", "==", string];
@@ -66,6 +67,7 @@ export declare const TESTER_CHECKLIST_ITEM_FIELDS: {
66
67
  readonly ORDER: "order";
67
68
  readonly IS_ACTIVE: "isActive";
68
69
  readonly SEARCH_TOKENS: "searchTokens";
70
+ readonly ADMIN_ONLY: "adminOnly";
69
71
  readonly CREATED_AT: "createdAt";
70
72
  readonly UPDATED_AT: "updatedAt";
71
73
  };
@@ -20,6 +20,7 @@ export const DEFAULT_TESTER_CHECKLIST_ITEM_DATA = {
20
20
  order: 0,
21
21
  isActive: true,
22
22
  searchTokens: [],
23
+ adminOnly: false,
23
24
  };
24
25
  export const testerChecklistItemQueryHelpers = {
25
26
  byGroup: (groupKey) => ["groupKey", "==", groupKey],
@@ -74,6 +75,7 @@ export const TESTER_CHECKLIST_ITEM_FIELDS = {
74
75
  ORDER: "order",
75
76
  IS_ACTIVE: "isActive",
76
77
  SEARCH_TOKENS: "searchTokens",
78
+ ADMIN_ONLY: "adminOnly",
77
79
  CREATED_AT: "createdAt",
78
80
  UPDATED_AT: "updatedAt",
79
81
  };