vojo/src/app/features/room/MediaViewerBody.css.ts

143 lines
4.4 KiB
TypeScript

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 <img>. `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,
});