@mohasinac/appkit 4.52.3 → 4.52.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-stamp.json +3 -3
- package/dist/features/layout/AppLayoutShell.d.ts +1 -3
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.d.ts +1 -3
- package/dist/features/layout/TitleBarLayout.js +2 -3
- package/dist/features/tester/seed-data/authored/design-ux__header-controls-and-messages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__header-controls-and-messages.js +85 -0
- package/dist/features/tester/seed-data/authored/design-ux__nav-reachability.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/design-ux__nav-reachability.js +88 -0
- package/dist/features/tester/seed-data/authored/index.js +4 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +86 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/ImageLightbox.js +1 -1
- package/package.json +1 -1
package/dist/.build-stamp.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"stampVersion": 1,
|
|
3
|
-
"version": "4.52.
|
|
4
|
-
"head": "
|
|
3
|
+
"version": "4.52.5",
|
|
4
|
+
"head": "7b24e76346651ca12f0a941e0d1ca87bb173a242",
|
|
5
5
|
"branch": "main",
|
|
6
6
|
"dirty": true,
|
|
7
|
-
"builtAt": "2026-10-
|
|
7
|
+
"builtAt": "2026-10-16T16:23:26.992Z"
|
|
8
8
|
}
|
|
@@ -101,8 +101,6 @@ export interface AppLayoutShellProps {
|
|
|
101
101
|
* every offset above them reads.
|
|
102
102
|
*/
|
|
103
103
|
showBottomNav?: boolean;
|
|
104
|
-
/** When provided, renders a tour-start icon button in the title bar. Null in Patch 1. */
|
|
105
|
-
onTourStart?: () => void;
|
|
106
104
|
/**
|
|
107
105
|
* Override className for the main content wrapper div.
|
|
108
106
|
* When set, replaces the default `w-full px-4 py-6 …`
|
|
@@ -128,4 +126,4 @@ export interface AppLayoutShellProps {
|
|
|
128
126
|
};
|
|
129
127
|
};
|
|
130
128
|
}
|
|
131
|
-
export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, showBottomNav,
|
|
129
|
+
export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, showBottomNav, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): React.JSX.Element;
|
|
@@ -127,7 +127,7 @@ function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions,
|
|
|
127
127
|
].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, hasActive: !!activeProfileLink, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: activeProfileLink?.href === profileHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === profileHref ? "page" : undefined, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: activeProfileLink?.href === userOrdersHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userOrdersHref ? "page" : undefined, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: activeProfileLink?.href === userWishlistHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userWishlistHref ? "page" : undefined, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: activeProfileLink?.href === userSettingsHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userSettingsHref ? "page" : undefined, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.reviewsCount }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Reviews" })] })), user.stats.auctionsWon != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.auctionsWon }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Auctions Won" })] })), user.stats.itemsSold != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.itemsSold }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Items Sold" })] }))] })), isAuthenticated && isAdminOrSeller && (adminHref || resolvedStoreHref) && (_jsx(CollapsibleNavGroup, { title: labels.dashboardSectionTitle, hasActive: !!activeDashboardLink, children: _jsxs(Ul, { spacing: "2xs", children: [adminHref && role === "admin" && (_jsx(Li, { children: _jsx(TextLink, { href: adminHref, variant: "none", className: activeDashboardLink?.href === adminHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === adminHref ? "page" : undefined, children: labels.adminDashboard }) })), resolvedStoreHref && isAdminOrSeller && (_jsx(Li, { children: _jsx(TextLink, { href: resolvedStoreHref, variant: "none", className: activeDashboardLink?.href === resolvedStoreHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === resolvedStoreHref ? "page" : undefined, children: labels.storeDashboard }) }))] }) })), normalizedSections.map((section, sectionIndex) => (_jsx(CollapsibleSidebarSection, { section: section, navItemClass: navItemClass, navItemActiveClass: navItemActiveClass, activeHref: activeHref }, `sidebar-section-${sectionIndex}`))), (sidebarLocaleSlot || showThemeToggleInSidebar || (isAuthenticated && onLogout)) && (_jsxs(Stack, { border: "default", className: "border-t border-[var(--appkit-color-border-subtle)]", padding: "t-md", gap: "3", children: [sidebarLocaleSlot, showThemeToggleInSidebar && (_jsxs("button", { type: "button", onClick: toggleTheme, className: "flex w-full items-center justify-end gap-[var(--appkit-space-3)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] transition-colors hover:bg-primary-surface hover:text-primary-800", children: [_jsx(Span, { "aria-hidden": "true", children: theme === "dark" ? "☀️" : "🌙" }), activeThemeName] })), isAuthenticated && onLogout && (_jsx("button", { type: "button", onClick: () => { onLogout(); onAfterLogout(); }, className: CLS_LOGOUT_BTN, children: labels.logout }))] }))] }));
|
|
128
128
|
}
|
|
129
129
|
// ─── Main component ────────────────────────────────────────────────────────────
|
|
130
|
-
export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, showBottomNav = true,
|
|
130
|
+
export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, showBottomNav = true, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
|
|
131
131
|
// QueryClientProvider used to be created here so AppLayoutShell could stand
|
|
132
132
|
// alone. The real cause of the 2026-06-10/11 "No QueryClient set" prod crash
|
|
133
133
|
// turned out to be a peer-dep duplicate of @tanstack/query-core under
|
|
@@ -194,7 +194,7 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
|
|
|
194
194
|
opacity: darkBackground.overlay?.opacity ?? 0,
|
|
195
195
|
},
|
|
196
196
|
};
|
|
197
|
-
return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full 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, messagesHref: messagesHref, unreadMessageCount: unreadMessageCount, 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,
|
|
197
|
+
return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full 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, messagesHref: messagesHref, unreadMessageCount: unreadMessageCount, 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(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot), eventBannerSlot] }), _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content",
|
|
198
198
|
// No bottom-chrome clearance here on purpose. FooterLayout is a
|
|
199
199
|
// SIBLING rendered after <Main>, so a margin here only gaps
|
|
200
200
|
// main-to-footer — it never clears the bottom of the page, which is
|
|
@@ -91,5 +91,5 @@ export function NavbarLayout({ items, activeHref, id = "main-navbar", ariaLabel
|
|
|
91
91
|
if (inline) {
|
|
92
92
|
return (_jsx(Ul, { "aria-label": ariaLabel, className: "hidden xl:flex items-center justify-center gap-[var(--appkit-space-0-5)] xl:gap-[var(--appkit-space-1)] w-full", children: items.map((item) => (_jsx(Li, { children: renderItem ? (renderItem(item, isItemActive(item))) : (_jsx(DefaultNavItem, { item: item, isActive: isItemActive(item) })) }, item.href))) }));
|
|
93
93
|
}
|
|
94
|
-
return (_jsx(Nav, { border: "subtle", id: id, "aria-label": ariaLabel, className: "hidden lg:block bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b", children: _jsxs(Row, { className: "container mx-auto sm:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)] max-w-[1920px] h-10", padding: "x-md", align: "center", reverse: "hand", children: [_jsxs(Div, { className: `relative flex-1 ${__O.hidden}`, children: [canScrollLeft && (_jsx(NavbarChevron, { direction: "left", onClick: () => scroll("left") })), _jsx(Ul, { ref: scrollRef, className: "flex items-center justify-center gap-[var(--appkit-space-0-5)] lg:gap-[var(--appkit-space-1)] overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]", children: items.map((item) => (_jsx(Li, { className: "shrink-0", children: renderItem ? (renderItem(item, isItemActive(item))) : (_jsx(DefaultNavItem, { item: item, isActive: isItemActive(item) })) }, item.href))) }), canScrollRight && (_jsx(NavbarChevron, { direction: "right", onClick: () => scroll("right") }))] }), rightSlot && _jsx(Div, { className: "shrink-0", children: rightSlot })] }) }));
|
|
94
|
+
return (_jsx(Nav, { border: "subtle", id: id, "aria-label": ariaLabel, className: "hidden lg:block bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b", children: _jsxs(Row, { className: "container mx-auto sm:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)] max-w-[1920px] h-10", padding: "x-md", align: "center", reverse: "hand", children: [_jsxs(Div, { className: `relative flex-1 ${__O.hidden}`, children: [canScrollLeft && (_jsx(NavbarChevron, { direction: "left", onClick: () => scroll("left") })), _jsx(Ul, { ref: scrollRef, className: "flex items-center justify-center-safe gap-[var(--appkit-space-0-5)] lg:gap-[var(--appkit-space-1)] overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]", children: items.map((item) => (_jsx(Li, { className: "shrink-0", children: renderItem ? (renderItem(item, isItemActive(item))) : (_jsx(DefaultNavItem, { item: item, isActive: isItemActive(item) })) }, item.href))) }), canScrollRight && (_jsx(NavbarChevron, { direction: "right", onClick: () => scroll("right") }))] }), rightSlot && _jsx(Div, { className: "shrink-0", children: rightSlot })] }) }));
|
|
95
95
|
}
|
|
@@ -55,8 +55,6 @@ export interface TitleBarLayoutProps {
|
|
|
55
55
|
onToggleDashboardNav?: () => void;
|
|
56
56
|
/** Hide the public sidebar toggle button when nested layouts own navigation. */
|
|
57
57
|
hideSidebarToggle?: boolean;
|
|
58
|
-
/** When provided, renders a tour-start icon button before the theme toggle. Null in Patch 1. */
|
|
59
|
-
onTourStart?: () => void;
|
|
60
58
|
id?: string;
|
|
61
59
|
className?: string;
|
|
62
60
|
}
|
|
@@ -76,4 +74,4 @@ export interface TitleBarLayoutProps {
|
|
|
76
74
|
*
|
|
77
75
|
* Receives all domain data as props — zero domain imports.
|
|
78
76
|
*/
|
|
79
|
-
export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, messagesHref, unreadMessageCount, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme,
|
|
77
|
+
export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, messagesHref, unreadMessageCount, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): React.JSX.Element;
|
|
@@ -39,11 +39,10 @@ const countBadge = "appkit-hand-badge absolute -top-0.5 flex items-center justif
|
|
|
39
39
|
*
|
|
40
40
|
* Receives all domain data as props — zero domain imports.
|
|
41
41
|
*/
|
|
42
|
-
export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, messagesHref, unreadMessageCount = 0, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme,
|
|
42
|
+
export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, messagesHref, unreadMessageCount = 0, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
|
|
43
43
|
// ── Element builders ────────────────────────────────────────────────────────
|
|
44
44
|
const promotionsEl = promotionsHref ? (_jsxs(Link, { href: promotionsHref, "aria-label": "Today's deals", className: "flex items-center gap-[var(--appkit-space-1)] px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] rounded-full text-[length:var(--appkit-text-xs)] font-bold bg-primary-100 text-primary-700 dark:bg-secondary-900/40 dark:text-secondary-400 hover:bg-primary-surface transition-colors border border-primary-200/60 dark:border-secondary-700/40", children: [_jsx(Icon, { name: "deals", size: "md", filled: true }), _jsx(Span, { className: "hidden sm:inline", children: "Today's Deals" })] })) : null;
|
|
45
45
|
const themeBtn = onToggleTheme ? (_jsx(IconButton, { size: "touch", variant: "ghost", "aria-label": isDark ? "Switch to light mode" : "Switch to dark mode", onClick: onToggleTheme, className: iconBtn, icon: _jsx(Icon, { name: isDark ? "themeLight" : "themeDark", size: "lg" }) })) : null;
|
|
46
|
-
const tourBtn = onTourStart ? (_jsx(IconButton, { size: "touch", variant: "ghost", "aria-label": "Start product tour", onClick: onTourStart, className: iconBtn, icon: _jsx(Icon, { name: "help", size: "lg" }) })) : null;
|
|
47
46
|
const hamburgerBtn = !hideSidebarToggle ? (_jsx(IconButton, { size: "touch", variant: "ghost", "aria-label": sidebarOpen ? "Close menu" : hasDashboardNav ? "Open dashboard navigation" : "Open menu", "aria-expanded": sidebarOpen, "aria-controls": "secondary-sidebar", onClick: hasDashboardNav && onToggleDashboardNav ? onToggleDashboardNav : onToggleSidebar, className: iconBtn, icon: _jsx(Icon, { name: sidebarOpen ? "close" : "menu", size: "lg" }) })) : null;
|
|
48
47
|
// Compare is always lg+ (desktop-only feature, less critical on mobile)
|
|
49
48
|
const compareEl = compareHref ? (_jsx(Link, { href: compareHref, "aria-label": "Compare items", className: `${iconBtn} hidden lg:flex`, children: _jsx(Icon, { name: "compare", size: "lg" }) })) : null;
|
|
@@ -67,7 +66,7 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
|
|
|
67
66
|
// ── Render ───────────────────────────────────────────────────────────────────
|
|
68
67
|
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-[length:var(--appkit-text-xs)] text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", reverse: "hand", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsxs(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center gap-[var(--appkit-space-2)] transition-opacity hover:opacity-80", children: [_jsx(Div, { className: "flex md:hidden", children: _jsx(SiteMark, { title: brandShortName ?? brandName, size: "sm" }) }), _jsx(SiteLogo, { title: brandName, size: "md" })] }) }), _jsx(Row, { className: "hidden md:flex absolute inset-y-0 left-1/2 -translate-x-1/2", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteMark, { src: siteLogoUrl, title: brandName, size: "lg" }) }) }), _jsxs(Row, { gap: "xs", reverse: "hand", children: [devSlot, navSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), messagesEl && _jsx(Div, { className: "hidden lg:flex", children: messagesEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
|
|
69
68
|
? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
|
|
70
|
-
: authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn,
|
|
69
|
+
: authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, _jsx(Div, { className: "hidden sm:flex", children: promotionsEl }), themeBtn, hamburgerBtn] })] }), hasTb2 && (
|
|
71
70
|
// `justify="end"` is CORRECT alongside `reverse="hand"` — do not
|
|
72
71
|
// "fix" it to "start". `row-reverse` inverts the main axis, so
|
|
73
72
|
// main-end IS the left edge: flex-end already packs this row left in
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Authored six-part procedures for the design-ux/header-controls-and-messages page.
|
|
3
|
+
* WHAT: 6 case(s), keyed by full checklist id.
|
|
4
|
+
*
|
|
5
|
+
* @tag domain:tester
|
|
6
|
+
* @tag layer:seed
|
|
7
|
+
* @tag pattern:none
|
|
8
|
+
* @tag access:server-only
|
|
9
|
+
* @tag consumers:seed-data/authored/index.ts
|
|
10
|
+
* @tag sideEffects:none
|
|
11
|
+
*/
|
|
12
|
+
export const authored = {
|
|
13
|
+
"checklist-design-ux-header-controls-and-messages-no-help-or-tour-icon": {
|
|
14
|
+
roles: ["guest"],
|
|
15
|
+
startPage: "/products",
|
|
16
|
+
steps: [
|
|
17
|
+
"Open /products at a maximised window.",
|
|
18
|
+
"Read every icon in the top header row, right of the logo.",
|
|
19
|
+
"Repeat at 1024px, 768px and 320px.",
|
|
20
|
+
],
|
|
21
|
+
expectedBehaviour: "The tour control was removed outright, along with the prop that fed it, so there is no width at which it can reappear.",
|
|
22
|
+
expectedUiState: "No '?' icon anywhere in the header at any of the four widths. The right-hand cluster is search, Today's Deals (640px and up), theme toggle and hamburger — plus the signed-in icons at 1024px and up. The removed control previously sat between the theme toggle and the hamburger.",
|
|
23
|
+
endResult: "Nothing persists; this is a pure chrome check. The hamburger must still be present and fully visible at 320px.",
|
|
24
|
+
},
|
|
25
|
+
"checklist-design-ux-header-controls-and-messages-tour-does-not-autostart": {
|
|
26
|
+
roles: ["guest", "buyer"],
|
|
27
|
+
startPage: "/",
|
|
28
|
+
steps: [
|
|
29
|
+
"Signed out, load /, then /products, then a product detail page.",
|
|
30
|
+
"Wait a few seconds on each without clicking anything.",
|
|
31
|
+
"Sign in and repeat the same three pages.",
|
|
32
|
+
],
|
|
33
|
+
expectedBehaviour: "Removing the header button removed the only call to `startTour`. Nothing else should be able to begin a tour, on any route or for any role.",
|
|
34
|
+
expectedUiState: "No tour popover, spotlight, highlight ring or step counter appears at any point, signed in or out. A tour appearing means something still calls startTour and the removal is incomplete — record which page it fired on.",
|
|
35
|
+
endResult: "No tour state is written. Reloading any of the pages produces no tour either.",
|
|
36
|
+
},
|
|
37
|
+
"checklist-design-ux-header-controls-and-messages-messages-icon-visible-when-signed-in": {
|
|
38
|
+
roles: ["buyer"],
|
|
39
|
+
startPage: "/messages",
|
|
40
|
+
steps: [
|
|
41
|
+
"Sign in as any buyer.",
|
|
42
|
+
"At a window 1024px or wider, look at the header beside the notification bell.",
|
|
43
|
+
"Click the messages icon.",
|
|
44
|
+
],
|
|
45
|
+
expectedBehaviour: "The header renders the messages control only when `siteSettings.chat.enabled` is true — and renders nothing at all when it is not, rather than a disabled icon. The flag is now on, so the control should exist and route to the inbox.",
|
|
46
|
+
expectedUiState: "A messages/inbox icon is present beside the notification bell and carries an unread badge when there are unread messages. Clicking it navigates to /messages, which loads — not a 404 and not /unauthorized.",
|
|
47
|
+
endResult: "Navigation only; nothing is written. Going back returns to the previous page with the icon still present.",
|
|
48
|
+
},
|
|
49
|
+
"checklist-design-ux-header-controls-and-messages-messages-icon-on-mobile-row": {
|
|
50
|
+
roles: ["buyer"],
|
|
51
|
+
startPage: "/messages",
|
|
52
|
+
steps: [
|
|
53
|
+
"Still signed in, narrow the window to 320px.",
|
|
54
|
+
"Look at BOTH header rows — the main row and the secondary row beneath it.",
|
|
55
|
+
"Tap the messages icon.",
|
|
56
|
+
],
|
|
57
|
+
expectedBehaviour: "Several signed-in icons are hidden from the main row below 1024px and carried on a secondary row instead, so the control moves rather than disappearing.",
|
|
58
|
+
expectedUiState: "The messages icon is present and fully visible in the secondary header row at 320px — not clipped at the edge and not absent. Tapping it opens /messages.",
|
|
59
|
+
endResult: "Nothing persists. Widening back past 1024px moves the icon to the main row again.",
|
|
60
|
+
},
|
|
61
|
+
"checklist-design-ux-header-controls-and-messages-messages-absent-for-guests": {
|
|
62
|
+
roles: ["guest"],
|
|
63
|
+
startPage: "/products",
|
|
64
|
+
steps: [
|
|
65
|
+
"Sign out completely.",
|
|
66
|
+
"Reload /products.",
|
|
67
|
+
"Inspect both header rows at a desktop width and again at 320px.",
|
|
68
|
+
],
|
|
69
|
+
expectedBehaviour: "A message inbox is per-user, so enabling the feature site-wide must not expose the control to visitors who have no inbox.",
|
|
70
|
+
expectedUiState: "No messages icon in either header row at either width. The guest header shows search, deals, theme, hamburger and the sign-in affordances only.",
|
|
71
|
+
endResult: "No state. Signing back in brings the icon back, which also confirms the gate is on the session and not on the page.",
|
|
72
|
+
},
|
|
73
|
+
"checklist-design-ux-header-controls-and-messages-messages-inbox-loads": {
|
|
74
|
+
roles: ["buyer"],
|
|
75
|
+
startPage: "/messages",
|
|
76
|
+
steps: [
|
|
77
|
+
"Signed in, navigate directly to /messages.",
|
|
78
|
+
"Wait for the page to settle.",
|
|
79
|
+
"Reload once.",
|
|
80
|
+
],
|
|
81
|
+
expectedBehaviour: "The messaging feature has been dormant behind a disabled flag, so this is the first real exercise of the page. It must handle having no conversations as a normal state.",
|
|
82
|
+
expectedUiState: "The inbox shell renders with either a list of conversations or an explicit empty state saying there are none. It must NOT be a crash, an error boundary, a redirect to /unauthorized, or a spinner that never resolves. Note the exact empty-state wording so a later run can tell 'empty' from 'failed to load'.",
|
|
83
|
+
endResult: "Reloading shows the same state. Visiting /messages signed out should send you to sign-in rather than rendering an empty inbox.",
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Authored six-part procedures for the design-ux/nav-reachability page.
|
|
3
|
+
* WHAT: 6 case(s), keyed by full checklist id.
|
|
4
|
+
*
|
|
5
|
+
* @tag domain:tester
|
|
6
|
+
* @tag layer:seed
|
|
7
|
+
* @tag pattern:none
|
|
8
|
+
* @tag access:server-only
|
|
9
|
+
* @tag consumers:seed-data/authored/index.ts
|
|
10
|
+
* @tag sideEffects:none
|
|
11
|
+
*/
|
|
12
|
+
export const authored = {
|
|
13
|
+
"checklist-design-ux-nav-reachability-main-nav-first-items-reachable": {
|
|
14
|
+
roles: ["guest"],
|
|
15
|
+
startPage: "/products",
|
|
16
|
+
steps: [
|
|
17
|
+
"Set the browser window to roughly 1024 pixels wide.",
|
|
18
|
+
"Open /products and look at the horizontal navigation strip under the header.",
|
|
19
|
+
"Read the FIRST item at the left edge of the strip.",
|
|
20
|
+
"Scroll the strip to the right as far as it goes and read the LAST item.",
|
|
21
|
+
],
|
|
22
|
+
expectedBehaviour: "The strip centres its items while they fit and falls back to starting at the left edge as soon as they would overflow, so nothing is ever pushed past the leading edge where scrolling cannot reach it.",
|
|
23
|
+
expectedUiState: "'Home' is the first item visible at the left edge of the strip, followed by 'Products'. Scrolling right reaches 'About Us' at the far end. The prior symptom was the strip opening mid-list with 'Home' and 'Products' already cut off past the left edge and no way to bring them back.",
|
|
24
|
+
endResult: "Nothing persists. Reloading at the same width shows 'Home' first again — the strip is not remembering a scroll position, it genuinely starts at the beginning.",
|
|
25
|
+
},
|
|
26
|
+
"checklist-design-ux-nav-reachability-main-nav-left-chevron-works": {
|
|
27
|
+
roles: ["guest"],
|
|
28
|
+
startPage: "/products",
|
|
29
|
+
steps: [
|
|
30
|
+
"At roughly 1024 pixels wide, scroll the navigation strip to the right (drag it, or use the right chevron).",
|
|
31
|
+
"Look at the left end of the strip for a left-pointing chevron.",
|
|
32
|
+
"Click the left chevron.",
|
|
33
|
+
],
|
|
34
|
+
expectedBehaviour: "The left chevron is shown when the strip has been scrolled away from its start. That condition could never be true while the centring pinned the scroll position at 0, so the control existed and never appeared.",
|
|
35
|
+
expectedUiState: "After scrolling right, a left chevron is visible at the left end of the strip. Clicking it scrolls the strip back towards the start, and continuing to click eventually brings 'Home' into view and the chevron disappears again.",
|
|
36
|
+
endResult: "The strip can be scrolled to either end and back as many times as wanted. No state is saved.",
|
|
37
|
+
},
|
|
38
|
+
"checklist-design-ux-nav-reachability-header-hamburger-reachable-at-320": {
|
|
39
|
+
roles: ["guest"],
|
|
40
|
+
startPage: "/products",
|
|
41
|
+
steps: [
|
|
42
|
+
"Set the browser window to 320 pixels wide, or choose an iPhone SE device profile.",
|
|
43
|
+
"Look at the right-hand end of the top header row.",
|
|
44
|
+
"Tap the hamburger (menu) control.",
|
|
45
|
+
],
|
|
46
|
+
expectedBehaviour: "The header row must fit inside the viewport at the narrowest supported width. The app shell clips horizontal overflow rather than scrolling it, so anything past the edge is unreachable — not merely out of sight.",
|
|
47
|
+
expectedUiState: "The hamburger is FULLY visible at the end of the header row, not half-cut and not missing, and tapping it opens the menu. The theme toggle immediately before it is also whole. The prior symptom was the hamburger sitting entirely beyond the right edge and the theme toggle sliced in half.",
|
|
48
|
+
endResult: "The menu opens and closes normally. Nothing persists.",
|
|
49
|
+
},
|
|
50
|
+
"checklist-design-ux-nav-reachability-header-demotes-optional-controls": {
|
|
51
|
+
roles: ["guest"],
|
|
52
|
+
startPage: "/products",
|
|
53
|
+
steps: [
|
|
54
|
+
"At 320 pixels wide, read every icon in the top header row.",
|
|
55
|
+
"Widen the window past about 640 pixels.",
|
|
56
|
+
"Read the header row again.",
|
|
57
|
+
"At 320 pixels again, open the hamburger menu and look for the deals and tour entries.",
|
|
58
|
+
],
|
|
59
|
+
expectedBehaviour: "Room for the hamburger is made by demoting the two most discretionary controls at the narrowest width, rather than by letting the most important one fall off the end. Neither is removed from the product — only from the header row at that width.",
|
|
60
|
+
expectedUiState: "At 320px the 'Today's Deals' and product-tour icons are NOT in the header. Above ~640px both are back. At 320px their destinations are still reachable from the menu, so nothing has become unavailable.",
|
|
61
|
+
endResult: "Purely responsive; no state is written. Resizing back and forth shows them disappear and reappear at the same point each time.",
|
|
62
|
+
},
|
|
63
|
+
"checklist-design-ux-nav-reachability-no-horizontal-page-scroll": {
|
|
64
|
+
roles: ["guest"],
|
|
65
|
+
startPage: "/",
|
|
66
|
+
steps: [
|
|
67
|
+
"Set the browser window to 320 pixels wide.",
|
|
68
|
+
"Visit each of: /, /products, any product detail page, /categories, /cart.",
|
|
69
|
+
"On each page, try to drag the page sideways and look for a horizontal scrollbar at the bottom of the window.",
|
|
70
|
+
],
|
|
71
|
+
expectedBehaviour: "No layout should be wider than the viewport at the narrowest supported width. A sideways-scrolling page is the clearest single symptom of a broken responsive layout.",
|
|
72
|
+
expectedUiState: "On all five pages the content does not move horizontally and no horizontal scrollbar appears. Report the page and the element if any one of them scrolls — this case is per-page, so a single failure is still a finding even if the other four pass.",
|
|
73
|
+
endResult: "Nothing persists. This is a pure layout check at one width.",
|
|
74
|
+
},
|
|
75
|
+
"checklist-design-ux-nav-reachability-lightbox-thumbnails-reachable": {
|
|
76
|
+
roles: ["guest"],
|
|
77
|
+
startPage: "/products",
|
|
78
|
+
steps: [
|
|
79
|
+
"Open a product that has several images — the Beyblade X UX-03 WizardRod and the video-demo fixtures all carry multiple.",
|
|
80
|
+
"Click the main product image to open the lightbox.",
|
|
81
|
+
"Look at the thumbnail strip below the enlarged image.",
|
|
82
|
+
"Scroll the strip to both ends.",
|
|
83
|
+
],
|
|
84
|
+
expectedBehaviour: "The thumbnail strip is a horizontal scroller and had the same centring defect as the navbar, so with enough images its first thumbnails were off the leading edge with no scroll position that could reach them.",
|
|
85
|
+
expectedUiState: "The strip begins at the FIRST image, and every thumbnail can be reached by scrolling. No thumbnail is stranded past the left edge. Clicking any thumbnail switches the enlarged image to it.",
|
|
86
|
+
endResult: "Closing the lightbox returns to the product page with no change. Reopening starts the strip at the first image again.",
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -88,6 +88,8 @@ import { authored as a_cta_layout__product_bottom_bar } from "./cta-layout__prod
|
|
|
88
88
|
import { authored as a_design_ux__back_to_top_button } from "./design-ux__back-to-top-button.js";
|
|
89
89
|
import { authored as a_design_ux__rich_text_readability } from "./design-ux__rich-text-readability.js";
|
|
90
90
|
import { authored as a_design_ux__action_bar_placement } from "./design-ux__action-bar-placement.js";
|
|
91
|
+
import { authored as a_design_ux__nav_reachability } from "./design-ux__nav-reachability.js";
|
|
92
|
+
import { authored as a_design_ux__header_controls_and_messages } from "./design-ux__header-controls-and-messages.js";
|
|
91
93
|
import { authored as a_design_ux__feature_badge_picker } from "./design-ux__feature-badge-picker.js";
|
|
92
94
|
import { authored as a_design_ux__dashboard_layout } from "./design-ux__dashboard-layout.js";
|
|
93
95
|
import { authored as a_design_ux__footer_theme } from "./design-ux__footer-theme.js";
|
|
@@ -213,6 +215,8 @@ export const AUTHORED_CASES = {
|
|
|
213
215
|
...a_design_ux__back_to_top_button,
|
|
214
216
|
...a_design_ux__rich_text_readability,
|
|
215
217
|
...a_design_ux__action_bar_placement,
|
|
218
|
+
...a_design_ux__nav_reachability,
|
|
219
|
+
...a_design_ux__header_controls_and_messages,
|
|
216
220
|
...a_design_ux__feature_badge_picker,
|
|
217
221
|
...a_design_ux__dashboard_layout,
|
|
218
222
|
...a_design_ux__footer_theme,
|
|
@@ -3892,6 +3892,92 @@ const rawTesterChecklistItems = [
|
|
|
3892
3892
|
},
|
|
3893
3893
|
],
|
|
3894
3894
|
},
|
|
3895
|
+
{
|
|
3896
|
+
pageKey: "header-controls-and-messages",
|
|
3897
|
+
pageLabel: "Header Controls — Tour Removed, Messages Enabled",
|
|
3898
|
+
href: "/products",
|
|
3899
|
+
cases: [
|
|
3900
|
+
{
|
|
3901
|
+
key: "no-help-or-tour-icon",
|
|
3902
|
+
label: "The header has NO '?' help / product-tour icon at any window width",
|
|
3903
|
+
description: "Removed 2026-10-15. BEFORE: a '?' icon sat between the theme toggle and the hamburger; it started the product tour (aria-label 'Start product tour'). It was one control, not two — the help glyph WAS the tour button. AFTER: check the header at 320px, 768px, 1024px and 1440px. There must be no '?' icon at any of them. The remaining right-hand controls are search, Today's Deals (sm and up), theme and hamburger — plus the signed-in icons on lg and up.",
|
|
3904
|
+
href: "/products",
|
|
3905
|
+
},
|
|
3906
|
+
{
|
|
3907
|
+
key: "tour-does-not-autostart",
|
|
3908
|
+
label: "No product tour starts on its own, and no tour overlay can be triggered from the header",
|
|
3909
|
+
description: "Removing the button removed the only way to start a tour. Load several pages signed out and signed in and confirm no tour popover, spotlight or step overlay ever appears. If a tour does appear, something still calls startTour and the removal was incomplete.",
|
|
3910
|
+
href: "/",
|
|
3911
|
+
},
|
|
3912
|
+
{
|
|
3913
|
+
key: "messages-icon-visible-when-signed-in",
|
|
3914
|
+
label: "A signed-in user sees a messages/inbox icon in the header that opens the inbox",
|
|
3915
|
+
description: "Enabled 2026-10-15. BEFORE: siteSettings.chat.enabled was absent, and the header renders nothing at all without it — not a disabled icon, nothing — so there was no way into the inbox. AFTER: sign in and look at the header on a desktop width (lg and up). A messages icon must be present beside the notification bell, and clicking it must open /messages, not 404.",
|
|
3916
|
+
href: "/messages",
|
|
3917
|
+
},
|
|
3918
|
+
{
|
|
3919
|
+
key: "messages-icon-on-mobile-row",
|
|
3920
|
+
label: "On mobile the messages icon appears in the secondary header row, not only on desktop",
|
|
3921
|
+
description: "The header hides several signed-in icons below lg and carries them on a second row instead. Signed in at 320–768px, confirm the messages icon is reachable in that secondary row and opens the inbox.",
|
|
3922
|
+
href: "/messages",
|
|
3923
|
+
},
|
|
3924
|
+
{
|
|
3925
|
+
key: "messages-absent-for-guests",
|
|
3926
|
+
label: "A signed-OUT visitor does not see a messages icon",
|
|
3927
|
+
description: "Messages are per-user, so the control should not appear for a guest even with chat enabled. Sign out, reload, and confirm no messages icon is present in either header row.",
|
|
3928
|
+
href: "/products",
|
|
3929
|
+
},
|
|
3930
|
+
{
|
|
3931
|
+
key: "messages-inbox-loads",
|
|
3932
|
+
label: "The /messages inbox page loads for a signed-in user and shows either conversations or a proper empty state",
|
|
3933
|
+
description: "The feature was dormant behind a flag, so this is the first real exercise of the page. Open /messages signed in. It must render the inbox shell with either a conversation list or an explicit empty state — never a crash, a redirect to /unauthorized, or an endless spinner.",
|
|
3934
|
+
href: "/messages",
|
|
3935
|
+
},
|
|
3936
|
+
],
|
|
3937
|
+
},
|
|
3938
|
+
{
|
|
3939
|
+
pageKey: "nav-reachability",
|
|
3940
|
+
pageLabel: "Navigation — Every Item Reachable at Every Width",
|
|
3941
|
+
href: "/products",
|
|
3942
|
+
cases: [
|
|
3943
|
+
{
|
|
3944
|
+
key: "main-nav-first-items-reachable",
|
|
3945
|
+
label: "At a ~1024px window the main navbar still reaches 'Home' and 'Products' — they are not cut off past the left edge",
|
|
3946
|
+
description: "Fixed 2026-10-15. BEFORE: the nav strip was a horizontal scroller centred with `justify-center`. When flex content overflows a centred container the overflow splits across BOTH ends, and the leading half cannot be scrolled to because scrollLeft stops at 0. Measured at 1024px: 14 items needing 1245px in a 990px strip put 'Home' at x=-238 and 'Products' at x=-148, with scrollLeft already 0 — permanently unreachable. AFTER: set the window to roughly 1024px wide and look at the navbar. 'Home' must be the first visible item at the left edge. Scroll the strip right and confirm 'About Us' is reachable too.",
|
|
3947
|
+
href: "/products",
|
|
3948
|
+
},
|
|
3949
|
+
{
|
|
3950
|
+
key: "main-nav-left-chevron-works",
|
|
3951
|
+
label: "After scrolling the navbar right, the left chevron appears and scrolls it back to 'Home'",
|
|
3952
|
+
description: "The chevron existed before the fix but could never appear: it is shown when scrollLeft > 0, and scrollLeft was pinned at 0 by the centring — so the control built for exactly this problem was disabled by the problem. Scroll the nav strip to the right, confirm a left chevron appears, click it, and confirm the strip scrolls back and 'Home' becomes visible.",
|
|
3953
|
+
href: "/products",
|
|
3954
|
+
},
|
|
3955
|
+
{
|
|
3956
|
+
key: "header-hamburger-reachable-at-320",
|
|
3957
|
+
label: "At a 320px-wide window the hamburger (Open menu) is fully visible and tappable in the header",
|
|
3958
|
+
description: "Fixed 2026-10-15. BEFORE: the header action row ran 64px past a 320px viewport and the app shell clips horizontal overflow, so the excess was not scrollable — it was gone. 'Open menu' sat entirely off-screen and the theme toggle was sliced in half. AFTER: narrow the window to 320px (or use an iPhone SE profile). The hamburger must be fully visible at the end of the header row and must open the menu when tapped. The theme toggle beside it must also be whole, not clipped.",
|
|
3959
|
+
href: "/products",
|
|
3960
|
+
},
|
|
3961
|
+
{
|
|
3962
|
+
key: "header-demotes-optional-controls",
|
|
3963
|
+
label: "At 320px the 'Today's Deals' and product-tour icons are hidden from the header, and both reappear on a wider screen",
|
|
3964
|
+
description: "This is how the hamburger was given room — the two most discretionary controls step aside at the narrowest width rather than the most important one falling off. At 320px confirm neither icon is in the header. Widen past ~640px and confirm both come back. Neither is lost at 320px: both remain reachable from the menu the hamburger opens.",
|
|
3965
|
+
href: "/products",
|
|
3966
|
+
},
|
|
3967
|
+
{
|
|
3968
|
+
key: "no-horizontal-page-scroll",
|
|
3969
|
+
label: "No page scrolls sideways at 320px — the whole layout fits the viewport width",
|
|
3970
|
+
description: "A horizontal scrollbar on a phone is the single clearest sign of a broken responsive layout. At 320px, visit the homepage, /products, a product detail page, /categories and /cart. On each, try to drag the page sideways: nothing should move, and no horizontal scrollbar should appear at the bottom of the window.",
|
|
3971
|
+
href: "/",
|
|
3972
|
+
},
|
|
3973
|
+
{
|
|
3974
|
+
key: "lightbox-thumbnails-reachable",
|
|
3975
|
+
label: "In the image lightbox, the FIRST thumbnail is reachable when a gallery has many images",
|
|
3976
|
+
description: "Same defect as the navbar, same fix — the thumbnail strip was a centred flex scroller, so with enough images the leading thumbnails were off the left edge with no way to scroll to them. Open a product with several images, click the main image to open the lightbox, and confirm the thumbnail strip starts at its first image and that every thumbnail can be reached by scrolling the strip.",
|
|
3977
|
+
href: "/products",
|
|
3978
|
+
},
|
|
3979
|
+
],
|
|
3980
|
+
},
|
|
3895
3981
|
{
|
|
3896
3982
|
pageKey: "action-bar-placement",
|
|
3897
3983
|
pageLabel: "Action Bar & Back-to-Top — Desktop vs Mobile",
|