import { style } from '@vanilla-extract/css'; import { color, config, toRem } from 'folds'; // Root layout — header strip → image stage → action row. `position: // relative` so the prev/next chevrons can absolute-position over the // stage on desktop. export const root = style({ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0, backgroundColor: color.Background.Container, color: color.Background.OnContainer, }); // `PageHeader` (folds `Header size="600"`) provides the strip // height; we only override the left gutter so the title sits at // the same x-offset as the profile side pane's title — keeps both // right-pane variants visually consistent. export const header = style({ paddingLeft: config.space.S300, }); export const title = style({ minWidth: 0, flex: '0 1 auto', }); // Vertical hairline between the action cluster and the close // button — visually separates "do something with this image" from // "close the viewer", which are semantically different actions. // Rendered for image kind only (no zoom controls for video → only // download + close, which doesn't need a separator). export const headerSeparator = style({ flexShrink: 0, width: '1px', height: toRem(20), margin: `0 ${config.space.S100}`, backgroundColor: color.SurfaceVariant.Container, }); // Image stage — fills remaining vertical space, centres the image // with object-fit-style logic on the wrapper. `overflow: hidden` so // a zoomed-in pan can't bleed past the stage. `touch-action: none` // blocks browser-native gestures (pinch-zoom, scroll, refresh-pull) // — we own the touch surface here for swipe + pan. export const stage = style({ flex: 1, position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', touchAction: 'none', userSelect: 'none', }); // The actual . `max-*: 100%` keeps it within the stage at // zoom=1; transforms (scale, translate) layer above without // re-flowing. `will-change` hints the compositor for smooth pan / // swipe / zoom animations. export const image = style({ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', willChange: 'transform', // Sharp pixel-ratio handling on dense screens — keeps phone // photos crisp at zoom=1, smooths only when zoomed beyond // intrinsic resolution. imageRendering: 'auto', }); // Side chevrons — visibility logic: // • Coarse pointer (touch devices, mobile): always visible at // 0.7 opacity so users have a tappable nav affordance without // needing the hover state that touch lacks. Swipe still works // but the buttons are the discoverability path. // • Fine pointer (desktop): hidden until the stage is hovered, // fading in to full opacity. Keeps the image surface clean // when the cursor isn't on it. // • Boundary (first/last item): the chevron is dimmed (0.25 // opacity, pointer-events disabled) rather than hidden, so the // user gets a visual cue that prev/next isn't available // instead of a missing button. export const chevron = style({ position: 'absolute', top: '50%', transform: 'translateY(-50%)', width: toRem(40), height: toRem(40), borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0, 0, 0, 0.55)', color: color.Background.OnContainer, cursor: 'pointer', border: 'none', outline: 'none', transition: 'opacity 150ms ease, background-color 150ms ease', opacity: 0, '@media': { '(pointer: coarse)': { opacity: 0.7, }, }, selectors: { [`${stage}:hover &`]: { opacity: 1 }, '&:hover': { backgroundColor: 'rgba(0, 0, 0, 0.75)' }, '&[aria-disabled="true"]': { opacity: 0.25, pointerEvents: 'none' }, }, }); export const chevronLeft = style({ left: config.space.S300, }); export const chevronRight = style({ right: config.space.S300, }); // Loading / error state overlay — centred inside the stage. export const stateOverlay = style({ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', }); // Index pill — "3 / 12" in the header. Subtle, matches the title // row tone. export const indexPill = style({ flexShrink: 0, padding: `${toRem(2)} ${config.space.S200}`, borderRadius: toRem(999), backgroundColor: color.SurfaceVariant.Container, color: color.SurfaceVariant.OnContainer, fontSize: toRem(12), fontWeight: 500, });