CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Three things, from the screenshots.
Banner repositioning, which was a real hole. A banner was always cropped dead
centre, and a GIF could not go through the still-image cropper at all — so
there was no way to frame one. The crop is now two percentages and a zoom
stored in the profile (`banner: {x, y, zoom}`), and the banner renders as an
<img> with object-position plus a scale rather than a CSS background: those two
compose into pan-and-zoom, where `background-size: cover` cannot be zoomed
without knowing the file's dimensions. The editor drags the live element, so a
GIF keeps animating while you frame it, and nothing is ever re-encoded.
The editor now has the reference's shape: a rail of cards on the left, each
showing what it currently holds, and the live profile beside it. A card opens a
dialog with the options on one side and a preview on the other. That is the
part both earlier attempts got wrong — the first put a 360px preview inside a
640px column, the second made you leave the preview to open a section. Here
they are on screen together at full size.
The dialog edits a copy: Cancel discards it, Apply commits to the draft, and
the draft is only persisted by Save. So backing out of a dialog cannot cost
unrelated work, and neither can the per-section reset in its header.
"Surprise me" randomises the look and nothing else. Fonts, effects, decoration,
theme and nameplate change; pronouns, tag, links, board and the banner crop are
the user's own words and framing, and rolling dice over those would be
vandalism. A test asserts exactly that over 40 rolls, and that every roll lands
on ids the normaliser accepts.
Five more nameplates in the y2k direction — holo, chrome, vapor, matrix, ember
— since the existing ten were all plain gradients.
Verified: shared build, web build, server typecheck, 523 web tests (5 new:
banner clamping and round-trip, randomiser bounds).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
38 lines
1.2 KiB
TypeScript
38 lines
1.2 KiB
TypeScript
import React from 'react';
|
|
import type { ProfileStyle } from '@backspace/shared/src/profile';
|
|
import { bannerImageStyle, themeGradient } from '../../utils/profileStyle';
|
|
import { ProfileEffectLayer } from './ProfileEffectLayer';
|
|
|
|
interface ProfileBannerProps {
|
|
src: string | null;
|
|
style: ProfileStyle;
|
|
/** Gradient used when there is no image and no theme. */
|
|
fallback: string;
|
|
className?: string;
|
|
effectDensity?: number;
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
/**
|
|
* The banner strip: image (framed by the user's crop), theme gradient, or the
|
|
* identity gradient — in that order — with the profile effect over it.
|
|
*/
|
|
export function ProfileBanner({ src, style, fallback, className = '', effectDensity = 1, children }: ProfileBannerProps) {
|
|
const theme = themeGradient(style);
|
|
return (
|
|
<div className={`relative overflow-hidden ${className}`} style={src ? undefined : { background: theme ?? fallback }}>
|
|
{src && (
|
|
<img
|
|
src={src}
|
|
alt=""
|
|
draggable={false}
|
|
className="absolute inset-0 w-full h-full object-cover select-none"
|
|
style={bannerImageStyle(style.banner)}
|
|
/>
|
|
)}
|
|
<ProfileEffectLayer effect={style.effect} density={effectDensity} />
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|