From df97413e1eece279fbe2c3eeb828428513114e25 Mon Sep 17 00:00:00 2001 From: dmnktoe Date: Thu, 18 Dec 2025 21:58:57 +0100 Subject: [PATCH] fix: zindexes --- .../components/ui/preview-notification.tsx | 2 +- .../components/widgets/now-playing-widget.tsx | 49 +++-- apps/web/components/widgets/psn-card.tsx | 2 +- .../consent/src/components/cookie-banner.tsx | 3 +- .../now-playing/src/now-playing-loading.tsx | 1 - packages/now-playing/src/now-playing.tsx | 1 - .../src/components/work-list/SbWorkList.tsx | 185 +++++++++++++----- packages/tokens/src/index.ts | 3 + packages/tokens/src/z-index.ts | 142 ++++++++++++++ packages/ui/panda.config.ts | 2 + .../custom-cursor/custom-cursor.tsx | 7 +- packages/ui/src/components/header/header.tsx | 2 +- .../components/header/mobile-menu-button.tsx | 2 +- .../components/header/mobile-menu-content.tsx | 20 +- .../image-preview/image-preview.tsx | 2 +- .../components/mouse-trail/mouse-trail.tsx | 2 +- .../ui/src/components/nav-link/nav-link.tsx | 9 +- .../ui/src/components/work-card/work-card.tsx | 4 +- .../ui/src/components/work-list/work-list.tsx | 6 +- packages/ui/styles.css | 109 ++++++++--- 20 files changed, 446 insertions(+), 107 deletions(-) create mode 100644 packages/tokens/src/z-index.ts diff --git a/apps/web/components/ui/preview-notification.tsx b/apps/web/components/ui/preview-notification.tsx index 4507f7bf..792e2c89 100644 --- a/apps/web/components/ui/preview-notification.tsx +++ b/apps/web/components/ui/preview-notification.tsx @@ -35,7 +35,7 @@ function PreviewNotificationContent() { bottom: 0, left: 0, right: 0, - zIndex: 9999, + zIndex: "preview", backgroundColor: "warning.200", color: "black", padding: 3, diff --git a/apps/web/components/widgets/now-playing-widget.tsx b/apps/web/components/widgets/now-playing-widget.tsx index e6231e21..698176f9 100644 --- a/apps/web/components/widgets/now-playing-widget.tsx +++ b/apps/web/components/widgets/now-playing-widget.tsx @@ -2,6 +2,7 @@ import { trackNowPlayingClick } from "@httpjpg/analytics"; import { NowPlaying, useNowPlaying } from "@httpjpg/now-playing"; +import { zIndex } from "@httpjpg/tokens"; import dynamic from "next/dynamic"; import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; @@ -31,24 +32,36 @@ function NowPlayingWidgetComponent() { }; const content = ( - ); diff --git a/apps/web/components/widgets/psn-card.tsx b/apps/web/components/widgets/psn-card.tsx index e64daae6..62da9d46 100644 --- a/apps/web/components/widgets/psn-card.tsx +++ b/apps/web/components/widgets/psn-card.tsx @@ -38,7 +38,7 @@ export const PSNCard = ({ username }: PSNCardProps) => { position: "fixed", top: 0, left: 0, - zIndex: 40, + zIndex: "widget", width: "250px", display: "none", lg: { diff --git a/packages/consent/src/components/cookie-banner.tsx b/packages/consent/src/components/cookie-banner.tsx index 811fe133..988fa3da 100644 --- a/packages/consent/src/components/cookie-banner.tsx +++ b/packages/consent/src/components/cookie-banner.tsx @@ -1,5 +1,6 @@ "use client"; +import { zIndex } from "@httpjpg/tokens"; import { Box, Button } from "@httpjpg/ui"; import { useEffect, useState } from "react"; import { getConsent, hasConsent, setConsent } from "../consent"; @@ -117,7 +118,7 @@ export function CookieBanner({ bottom: 0, left: 0, right: 0, - zIndex: 99999, + zIndex: zIndex.cookieBanner, backgroundColor: "white", color: "black", padding: "24px 16px", diff --git a/packages/now-playing/src/now-playing-loading.tsx b/packages/now-playing/src/now-playing-loading.tsx index 5faa2055..7cda66cb 100644 --- a/packages/now-playing/src/now-playing-loading.tsx +++ b/packages/now-playing/src/now-playing-loading.tsx @@ -89,7 +89,6 @@ export const NowPlayingLoading = ({ width: config.width, height: config.height, cursor: "grab", - zIndex: 9999, touchAction: "none", userSelect: "none", ...style, diff --git a/packages/now-playing/src/now-playing.tsx b/packages/now-playing/src/now-playing.tsx index 3ca9bd13..2c60e765 100644 --- a/packages/now-playing/src/now-playing.tsx +++ b/packages/now-playing/src/now-playing.tsx @@ -285,7 +285,6 @@ export const NowPlaying = ({ width: config.width, height: config.height, cursor: "grab", - zIndex: 9999, touchAction: "none", userSelect: "none", }} diff --git a/packages/storyblok-ui/src/components/work-list/SbWorkList.tsx b/packages/storyblok-ui/src/components/work-list/SbWorkList.tsx index 12a3741f..9715b9f7 100644 --- a/packages/storyblok-ui/src/components/work-list/SbWorkList.tsx +++ b/packages/storyblok-ui/src/components/work-list/SbWorkList.tsx @@ -1,13 +1,9 @@ "use client"; -// TODO: Fix Storyblok component registration -// This component exists in code but throws "Component work-list doesn't exist" in CMS -// Need to run: pnpm --filter @httpjpg/storyblok-sync run sync -// Or manually create component in Storyblok dashboard - import { renderStoryblokRichText } from "@httpjpg/storyblok-richtext"; import { WorkList } from "@httpjpg/ui"; -import { memo } from "react"; +import { memo, type ReactNode, useRef } from "react"; +import { mapColorToToken } from "../../lib/token-mapping"; import { useStoryblokEditable } from "../../lib/use-storyblok-editable"; export interface SbWorkListProps { @@ -39,6 +35,11 @@ export interface SbWorkListProps { }; } >; // Array of story UUIDs or resolved stories + gap?: number; + showDividers?: boolean; + dividerPattern?: string; + dividerColor?: string; + dividerSpacing?: string; }; /** * Base URL for work links @@ -56,63 +57,147 @@ export const SbWorkList = memo(function SbWorkList({ baseUrl = "/work", }: SbWorkListProps) { const editableProps = useStoryblokEditable(blok); - const { work } = blok; + const lastWorkItemsRef = useRef([]); + + const { + work, + gap, + showDividers, + dividerPattern, + dividerColor, + dividerSpacing, + } = blok || {}; + const showDividersValue = showDividers === true; - // Filter out UUIDs and only keep resolved stories (objects) + // Filter out UUIDs and only keep resolved stories const workStories = (work || []).filter( - (item): item is Exclude => typeof item === "object", + (item): item is Exclude => + item != null && typeof item === "object" && "slug" in item, ); + // Use cached items during Storyblok reload (Visual Editor stability) if (!workStories || workStories.length === 0) { - return null; + if (lastWorkItemsRef.current.length > 0 && work && work.length > 0) { + return ( +
+ +
+ ); + } + + return ( +
+
+ No work items selected. Add work items in Storyblok. +
+
+ ); } // Transform Storyblok work data to WorkList format - const workItems = workStories.map((item) => { - // Use date_end if exists (for events/exhibitions with timespan), - // otherwise use date field, fallback to first_published_at - const dateString = - item.content?.date_end || - item.content?.date || - item.first_published_at || - item.published_at || - item.created_at; + const workItems = workStories + .map((item) => { + if (!item.slug) return null; - // Render rich text description if available - const description = item.content?.description - ? renderStoryblokRichText(item.content.description) - : undefined; + const dateString = + item.content?.date_end || + item.content?.date || + item.first_published_at || + item.published_at || + item.created_at || + new Date().toISOString(); + + let description: ReactNode; + if (item.content?.description) { + try { + description = renderStoryblokRichText(item.content.description); + } catch { + // Silently fail - description is optional + } + } - return { - id: item.slug, - slug: item.slug, - title: item.content?.title || item.name, - description, - images: (item.content?.images || []).map((img) => { - // Check content_type first (for Storyblok assets) - const hasVideoContentType = img.content_type?.startsWith("video/"); - // Fallback: Check file extension (for external URLs) - const hasVideoExtension = /\.(mp4|webm|ogg|mov|avi|mkv)(\?|$)/i.test( - img.filename || "", - ); - const isVideo = hasVideoContentType || hasVideoExtension; + return { + id: item.slug, + slug: item.slug, + title: item.content?.title || item.name || "Untitled", + description, + images: (item.content?.images || []) + .filter((img) => img?.filename) + .map((img) => { + const hasVideoContentType = img.content_type?.startsWith("video/"); + const hasVideoExtension = + /\.(mp4|webm|ogg|mov|avi|mkv)(\?|$)/i.test(img.filename || ""); + const isVideo = hasVideoContentType || hasVideoExtension; - return { - url: isVideo ? "" : img.filename || "", - alt: img.alt || item.content?.title || item.name, - copyright: img.copyright, - focus: img.focus, - videoUrl: isVideo ? img.filename : undefined, - }; - }), - date: dateString, - baseUrl, - }; - }); + return { + url: isVideo ? "" : img.filename || "", + alt: img.alt || item.content?.title || item.name || "Image", + copyright: img.copyright, + focus: img.focus, + videoUrl: isVideo ? img.filename : undefined, + }; + }), + date: dateString, + baseUrl, + }; + }) + .filter((item): item is NonNullable => item !== null); + + if (workItems.length === 0) { + return ( +
+
+ ⚠️ Error loading work items. +
+
+ ); + } + + lastWorkItemsRef.current = workItems; return ( -
- +
+
); }); diff --git a/packages/tokens/src/index.ts b/packages/tokens/src/index.ts index a2439de0..7a5bf5a2 100644 --- a/packages/tokens/src/index.ts +++ b/packages/tokens/src/index.ts @@ -22,6 +22,7 @@ export { remToPx, responsiveSpacing, } from "./utils"; +export { type ZIndexKey, type ZIndexValue, zIndex } from "./z-index"; import { borderRadius } from "./border-radius"; import { colors } from "./colors"; @@ -31,6 +32,7 @@ import { sizes } from "./sizes"; import { spacing } from "./spacing"; import { transitions } from "./transitions"; import { typography } from "./typography"; +import { zIndex } from "./z-index"; /** * Complete design tokens object @@ -50,6 +52,7 @@ export const tokens = { opacity, transitions, sizes, + zIndex, } as const; export type Tokens = typeof tokens; diff --git a/packages/tokens/src/z-index.ts b/packages/tokens/src/z-index.ts new file mode 100644 index 00000000..0e03b5b3 --- /dev/null +++ b/packages/tokens/src/z-index.ts @@ -0,0 +1,142 @@ +/** + * Z-Index Scale + * Consistent stacking order for all UI elements + * Uses 20-step increments for clear hierarchy + * + * Usage: + * ```tsx + * import { zIndex } from '@httpjpg/tokens'; + * + * ``` + */ + +export const zIndex = { + /** + * Background layers (-1) + * For decorative elements that should appear behind content + */ + hide: -1, + + /** + * Base level (0) + * Default stacking context + */ + base: 0, + + /** + * Docked content (20) + * Video overlays, work cards, copyright labels, grid overlays + */ + docked: 20, + + /** + * Slideshow controls (40) + * Navigation arrows, indicators + */ + slideshow: 40, + + /** + * Widget cards (60) + * PSN card and similar widgets + */ + widget: 60, + + /** + * Header / sticky navigation (80) + * Site header, navigation bars + */ + header: 80, + + /** + * Floating widgets (10) + * Now Playing widget, persistent UI elements - kept low to stay under overlays + */ + floating: 10, + + /** + * Dropdown menus (100) + * Context menus, select dropdowns + */ + dropdown: 100, + + /** + * Sticky positioned elements (120) + * Elements that stick during scroll + */ + sticky: 120, + + /** + * Mobile menu overlay (300) + * Full-screen mobile navigation - MUST be above floating widgets + */ + mobileMenu: 300, + + /** + * Mobile menu button (320) + * Toggle button for mobile menu (must be above menu) + */ + mobileMenuButton: 320, + + /** + * Banner notifications (240) + * Cookie banner, important announcements + */ + banner: 240, + + /** + * Overlay backgrounds (220) + * Modal backdrops, drawer backgrounds + */ + overlay: 220, + + /** + * Modal dialogs (240) + * Dialog boxes, lightboxes + */ + modal: 240, + + /** + * Popovers (260) + * Contextual popups, tooltips with interactions + */ + popover: 260, + + /** + * Toast notifications (280) + * Temporary notification messages + */ + toast: 280, + + /** + * Tooltips (300) + * Hover tooltips, simple info popups + */ + tooltip: 300, + + /** + * Mouse effects (320) + * Mouse trail, cursor followers (below cursor itself) + */ + mouseEffects: 320, + + /** + * Image preview overlay (340) + * Full-screen image previews, preview notifications + */ + preview: 340, + + /** + * Cookie consent banner (360) + * Legal requirement - must be above everything except cursor + */ + cookieBanner: 360, + + /** + * Custom cursor (380) + * Always on top - custom mouse cursor + */ + cursor: 380, +} as const; + +export type ZIndexKey = keyof typeof zIndex; +export type ZIndexValue = (typeof zIndex)[ZIndexKey]; diff --git a/packages/ui/panda.config.ts b/packages/ui/panda.config.ts index 7ddcbd72..118a9a18 100644 --- a/packages/ui/panda.config.ts +++ b/packages/ui/panda.config.ts @@ -18,6 +18,7 @@ import { spacing, transitions, typography, + zIndex, } from "@httpjpg/tokens"; import { defineConfig } from "@pandacss/dev"; import { hexToRgba, linearGradient } from "./src/lib/color-utils"; @@ -250,6 +251,7 @@ export default defineConfig({ sizes: toPandaTokens(sizes), durations: toPandaTokens(transitions.duration), easings: toPandaTokens(transitions.easing), + zIndex: toPandaTokens(zIndex), }, breakpoints: { sm: "640px", diff --git a/packages/ui/src/components/custom-cursor/custom-cursor.tsx b/packages/ui/src/components/custom-cursor/custom-cursor.tsx index 840363c3..484d06ba 100644 --- a/packages/ui/src/components/custom-cursor/custom-cursor.tsx +++ b/packages/ui/src/components/custom-cursor/custom-cursor.tsx @@ -1,5 +1,6 @@ "use client"; +import { zIndex } from "@httpjpg/tokens"; import { useEffect, useState } from "react"; import type { SystemStyleObject } from "styled-system/types"; import { Box } from "../box/box"; @@ -199,8 +200,9 @@ export function CustomCursor({ top: 0, left: 0, pointerEvents: "none", - zIndex: 999999, + zIndex: zIndex.cursor, transform: `translate3d(${cursorPos.x}px, ${cursorPos.y}px, 0) translate(-50%, -50%)`, + fontSize: isDragging ? `${size * 1.2}px` : `${size}px`, color, fontWeight: "bold", @@ -224,8 +226,9 @@ export function CustomCursor({ top: 0, left: 0, pointerEvents: "none", - zIndex: 999999, + zIndex: zIndex.cursor, transform: `translate3d(${cursorPos.x}px, ${cursorPos.y}px, 0) translate(-50%, calc(-100% - 30px))`, + willChange: "transform", // Force hardware acceleration backfaceVisibility: "hidden", diff --git a/packages/ui/src/components/header/header.tsx b/packages/ui/src/components/header/header.tsx index 3adf637d..b6a2c90d 100644 --- a/packages/ui/src/components/header/header.tsx +++ b/packages/ui/src/components/header/header.tsx @@ -94,7 +94,7 @@ export const Header = ({ w: "full", bg: "transparent", color: "black", - zIndex: 50, + zIndex: "header", pointerEvents: "auto", py: 4, fontSize: "sm", diff --git a/packages/ui/src/components/header/mobile-menu-button.tsx b/packages/ui/src/components/header/mobile-menu-button.tsx index 5a39f073..99439f8c 100644 --- a/packages/ui/src/components/header/mobile-menu-button.tsx +++ b/packages/ui/src/components/header/mobile-menu-button.tsx @@ -20,7 +20,7 @@ export const MobileMenuButton = ({ position: { base: "fixed", md: "relative" }, right: { base: "2", md: "auto" }, top: { base: "3", md: "auto" }, - zIndex: 65, + zIndex: "mobileMenuButton", ml: "auto", display: { base: "block", lg: "none" }, }} diff --git a/packages/ui/src/components/header/mobile-menu-content.tsx b/packages/ui/src/components/header/mobile-menu-content.tsx index b25d2c36..6a171b76 100644 --- a/packages/ui/src/components/header/mobile-menu-content.tsx +++ b/packages/ui/src/components/header/mobile-menu-content.tsx @@ -30,7 +30,7 @@ export const MobileMenuContent = ({ css={{ position: "fixed", inset: 0, - zIndex: 50, + zIndex: "mobileMenu", display: { base: "flex", xl: "none" }, maxH: "full", w: "full", @@ -52,6 +52,7 @@ export const MobileMenuContent = ({ display: "flex", h: "calc(100vh)", w: { base: "full", md: "96" }, + maxW: "100vw", overflow: "hidden", transition: "all 200ms ease-in-out", transform: isOpen ? "translateX(0)" : "translateX(0.5rem)", @@ -70,6 +71,8 @@ export const MobileMenuContent = ({ m: { md: "6" }, borderRadius: 0, position: "relative", + overflow: "hidden", + maxW: "full", }} > @@ -101,6 +108,9 @@ export const MobileMenuContent = ({ textDecoration: "none", color: "inherit", _hover: { textDecoration: "underline" }, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", }} > {item.name} @@ -112,6 +122,7 @@ export const MobileMenuContent = ({ pattern="⋆。°✩ ・ ✦ ・ ✧ ・ ✦ ・ ✩°。⋆" color="neutral.200" spacing="3" + css={{ maxW: "full" }} /> {work.title} @@ -154,6 +168,7 @@ export const MobileMenuContent = ({ pattern="⋆。°✩ ・ ✦ ・ ✧ ・ ✦ ・ ✩°。⋆" color="neutral.200" spacing="3" + css={{ maxW: "full" }} /> {work.title} diff --git a/packages/ui/src/components/image-preview/image-preview.tsx b/packages/ui/src/components/image-preview/image-preview.tsx index 30b866cb..70fc646d 100644 --- a/packages/ui/src/components/image-preview/image-preview.tsx +++ b/packages/ui/src/components/image-preview/image-preview.tsx @@ -114,7 +114,7 @@ export function ImagePreview({ height: `${height}px`, transform: "translate(-1000px, -1000px)", pointerEvents: "none", - zIndex: 9999, + zIndex: "preview", }} > diff --git a/packages/ui/src/components/nav-link/nav-link.tsx b/packages/ui/src/components/nav-link/nav-link.tsx index 82f384fe..864ef269 100644 --- a/packages/ui/src/components/nav-link/nav-link.tsx +++ b/packages/ui/src/components/nav-link/nav-link.tsx @@ -99,6 +99,8 @@ export const NavLink = ({ href={href} className={mergedClassName} style={{ + position: "relative", + zIndex: 1, cursor: 'url(\'data:image/svg+xml;utf8,\') 10 5, pointer', }} @@ -124,7 +126,12 @@ export const NavLink = ({ // Render internal links with Next.js Link return ( - + {children} ); diff --git a/packages/ui/src/components/work-card/work-card.tsx b/packages/ui/src/components/work-card/work-card.tsx index 217e8fe2..15b9c557 100644 --- a/packages/ui/src/components/work-card/work-card.tsx +++ b/packages/ui/src/components/work-card/work-card.tsx @@ -317,7 +317,9 @@ export const WorkCard = forwardRef( {/* Content */} - + + {" "} + {/* @httpjpg/tokens zIndex.docked */} ( {header} {works.map((work, index) => ( - - + <> + {showDividers && index < works.length - 1 && ( )} - + ))} {footer} diff --git a/packages/ui/styles.css b/packages/ui/styles.css index 22516122..8064664c 100644 --- a/packages/ui/styles.css +++ b/packages/ui/styles.css @@ -780,6 +780,27 @@ --easings-ease-in: ease-in; --easings-ease-out: ease-out; --easings-ease-in-out: ease-in-out; + --z-index-hide: -1; + --z-index-base: 0; + --z-index-docked: 20; + --z-index-slideshow: 40; + --z-index-widget: 60; + --z-index-header: 80; + --z-index-floating: 10; + --z-index-dropdown: 100; + --z-index-sticky: 120; + --z-index-mobile-menu: 300; + --z-index-mobile-menu-button: 320; + --z-index-banner: 240; + --z-index-overlay: 220; + --z-index-modal: 240; + --z-index-popover: 260; + --z-index-toast: 280; + --z-index-tooltip: 300; + --z-index-mouse-effects: 320; + --z-index-preview: 340; + --z-index-cookie-banner: 360; + --z-index-cursor: 380; --breakpoints-sm: 640px; --breakpoints-md: 768px; --breakpoints-lg: 1024px; @@ -2657,8 +2678,8 @@ position: fixed; } - .z_9999 { - z-index: 9999; + .z_preview { + z-index: var(--z-index-preview); } .bg-c_warning\.200 { @@ -2677,8 +2698,8 @@ vertical-align: middle; } - .z_40 { - z-index: 40; + .z_widget { + z-index: var(--z-index-widget); } .d_none { @@ -2738,6 +2759,10 @@ user-select: none; } + .c_neutral\.300 { + color: var(--colors-neutral-300); +} + .op_0\.3 { opacity: 0.3; } @@ -2794,8 +2819,8 @@ position: sticky; } - .z_50 { - z-index: 50; + .z_header { + z-index: var(--z-index-header); } .pointer-events_auto { @@ -2806,8 +2831,8 @@ line-height: var(--line-heights-snug); } - .z_65 { - z-index: 65; + .z_mobileMenuButton { + z-index: var(--z-index-mobile-menu-button); } .ff_Impact\,_Haettenschweiler\,_sans-serif { @@ -2834,6 +2859,10 @@ opacity: var(--opacity-0); } + .z_mobileMenu { + z-index: var(--z-index-mobile-menu); +} + .trf_translateX\(0\) { transform: translateX(0); } @@ -2886,8 +2915,8 @@ will-change: transform; } - .z_9998 { - z-index: 9998; + .z_mouseEffects { + z-index: var(--z-index-mouse-effects); } .fs_24px { @@ -3532,6 +3561,46 @@ min-height: 20px; } + .bd-t-w_1px { + border-top-width: 1px; +} + + .border-top-style_dotted { + border-top-style: dotted; +} + + .border-top-style_solid { + border-top-style: solid; +} + + .border-top-style_dashed { + border-top-style: dashed; +} + + .bd-t-c_neutral\.300 { + border-top-color: var(--colors-neutral-300); +} + + .bd-l-w_1px { + border-left-width: 1px; +} + + .border-left-style_dotted { + border-left-style: dotted; +} + + .border-left-style_solid { + border-left-style: solid; +} + + .border-left-style_dashed { + border-left-style: dashed; +} + + .bd-l-c_neutral\.300 { + border-left-color: var(--colors-neutral-300); +} + .max-w_64 { max-width: var(--sizes-64); } @@ -3560,6 +3629,14 @@ height: calc(100vh); } + .max-w_100vw { + max-width: 100vw; +} + + .ov-y_auto { + overflow-y: auto; +} + .ml_2 { margin-left: var(--spacing-2); } @@ -4456,18 +4533,6 @@ padding-right: var(--spacing-96); } - .border-top-style_solid { - border-top-style: solid; -} - - .border-top-style_dashed { - border-top-style: dashed; -} - - .border-top-style_dotted { - border-top-style: dotted; -} - .border-bottom-style_solid { border-bottom-style: solid; }