ui/workshopscreen.razor.scss
// Workshop level browser. Designed in 1080-reference space (ScreenPanel AutoScreenScale =
// ConsistentHeight), living on the dark covered backdrop like the leaderboard. Header/footer match
// HighscoreScreen; the tile grid borrows the level-browser overlay's visual language.
@import "Theme.scss";
$line: rgba( 255, 255, 255, 0.12 );
$muted: rgba( 170, 180, 190, 1 );
$accent: rgba( 90, 180, 235, 1 );
$on: rgba( 60, 120, 170, 1 );
// Labels and icons are visual content, not independent mouse targets (see Theme.scss rationale).
button > * {
pointer-events: none;
}
WorkshopScreen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
padding: 16px 0 60px;
font-family: $font;
font-smooth: never;
.header {
font-size: 80px;
line-height: 0.8;
color: $white;
text-shadow: 4px 4px 0 $black;
}
// Level-editor entry in the upper-left, mirroring the character slot exactly: the picker trigger
// is a 100px box (viewport 88 + 12, see CharacterPicker.TriggerWidth) at top 8 / 512 off-centre,
// so this matches all three (size excludes the square-btn drop shadow). The 40px icon art shows
// at 1.5x — restrained inside the face, like the picker's character sprite.
// Nudged down-right by the square-btn shadow offset (6px) so the SHADOW's outer corner sits where
// the picker's frame does, visually balancing the two corners.
.editor-slot {
position: absolute;
z-index: 1000;
left: 50%;
margin-left: -506px;
top: 14px;
width: 100px;
height: 100px;
border-width: 3px; // the picker trigger's frame weight, not square-btn's 4px
.img {
width: 60px;
height: 60px;
}
}
// Character picker in the upper-right, like the daily / level-select screens.
.character-slot {
position: absolute;
z-index: 1000;
right: 50%;
margin-right: -512px;
top: 8px;
align-items: flex-end;
}
// ── search row: filter box + sort cycle, centred between the corner slots ───────────────────────
.searchbar {
flex-direction: row;
justify-content: center;
gap: 12px;
align-items: center;
width: 1050px;
margin: 10px 0 16px;
pointer-events: all;
.search-box {
position: relative;
width: 340px;
height: 46px;
flex-shrink: 0;
.search-entry {
width: 100%;
height: 100%;
padding: 6px 50px 6px 12px;
align-items: center;
background-color: rgba( 0, 0, 0, 0.35 );
border: 1px solid $line;
font-size: 24px;
color: $white;
caret-color: $accent;
white-space: nowrap;
overflow: hidden;
.content-label {
white-space: nowrap;
overflow: hidden;
}
&:focus { border-color: $accent; }
}
.search-clear {
position: absolute;
top: 0;
right: 0;
width: 46px;
height: 100%;
align-items: center;
justify-content: center;
cursor: blank-hand;
.icon {
font-family: "Material Icons";
font-size: 26px;
color: rgba( 220, 90, 90, 1 );
}
&:hover .icon { color: rgba( 255, 130, 130, 1 ); }
}
}
.sort {
background-color: rgba( 255, 255, 255, 0.06 );
border: 1px solid $line;
min-width: 130px;
padding: 8px 12px;
font-size: 22px;
align-items: center;
justify-content: center;
text-align: center;
color: $white;
cursor: blank-hand;
&:hover { background-color: rgba( 255, 255, 255, 0.14 ); }
&.on {
background-color: $on;
border-color: $accent;
}
}
}
// ── tile grid: fixed-size tiles wrapping left-to-right, vertically scrollable ───────────────────
.grid {
position: relative; // anchors the centred notice
width: 1050px;
flex-grow: 1;
flex-direction: row;
flex-wrap: wrap;
align-content: flex-start;
justify-content: center;
gap: 20px;
overflow-y: scroll;
overflow-x: hidden;
pointer-events: all;
.notice {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
label {
font-size: 38px;
letter-spacing: 3px;
color: $muted;
}
}
.tile {
position: relative; // anchors the web-link chip over the face's top-right corner
flex-direction: column;
width: 190px;
gap: 4px;
// The live LevelNodePreview (or the fallback thumbnail PNG) fills the face; clicking it
// installs + plays.
.face {
position: relative;
width: 190px;
height: 190px;
padding: 0;
overflow: hidden;
border: 3px solid rgba( 63, 124, 132, 1 );
background-color: rgba( 0, 0, 0, 0.35 );
cursor: blank-hand;
// Fallback: the uploaded workshop thumbnail, for levels too big to embed in metadata.
.img {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
pointer-events: none;
}
// Top-LEFT: the top-right corner belongs to the web-link chip.
.votes {
position: absolute;
top: 0;
left: 0;
flex-direction: row;
align-items: center;
padding: 2px 7px 3px 6px;
font-size: 18px;
color: $white;
background-color: rgba( 12, 15, 20, 0.75 );
pointer-events: none;
z-index: 2;
// Same white-on-transparent icon PNGs as the square buttons (Theme.scss).
.icon {
width: 16px;
height: 16px;
margin-right: 4px;
background-image: url( "ui/icons/icon_thumbsup.png" );
background-size: 100% 100%;
background-repeat: no-repeat;
image-rendering: pixelated;
background-tint: $white;
}
}
// Forced-character badge in the preview's lower-left, matching the level browser's
// tiles (glow-behind-sprite structure, see LevelBrowserOverlay.razor.scss).
.tile-forced-character {
position: absolute;
left: 7px;
bottom: 7px;
pointer-events: none;
z-index: 2;
.icon-glow {
position: absolute;
left: -61%;
top: -60.5%;
width: 222%;
height: 221%;
z-index: 0;
opacity: 0.3;
background-image: url( "sprites/ui/icon_glow.png" );
background-repeat: no-repeat;
background-size: 100% 100%;
}
.sprite {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
image-rendering: pixelated;
opacity: 0.95;
}
// Paired (twins) badge: partner behind up-right and slightly smaller, character
// in front down-left — same composition as the picker's twins icon.
&.paired {
.sprite.rear {
transform: translate( 11%, -9% ) scale( 0.94 );
}
.sprite.front {
transform: translate( -11%, 9% );
}
}
}
// Install / failure state, painted over the whole preview.
.busy {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
background-color: rgba( 12, 15, 20, 0.75 );
pointer-events: none;
z-index: 3;
label {
font-size: 20px;
letter-spacing: 2px;
color: $white;
}
&.failed label { color: rgba( 235, 90, 90, 1 ); }
}
}
// Play-affordance glow on the FACE's own hover only — not .tile:hover — so the web-link
// chip (and the caption/trophy row) doesn't light up the preview: the glow means
// "clicking here plays the level".
.face:hover {
border-color: #ffffff;
box-shadow: 0px 0px 18px 2px rgba( 255, 255, 255, 0.6 );
}
// Steam Workshop page link, overlaying the face's top-right corner — a SIBLING of the
// face (see the razor), inset by the face's 3px border like the votes chip. The wrapper
// owns ALL the chip visuals; the WebLinkLabel inside stays visually bare because its
// computed background gets baked into the anchor span's texture and sticks (see razor).
.web {
position: absolute;
top: 3px;
right: 3px;
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
background-color: rgba( 12, 15, 20, 0.75 );
border: 1px solid $line;
cursor: blank-hand;
z-index: 4;
&:hover { background-color: $on; border-color: $accent; }
// The anchor glyph nearly fills the chip: only the glyph itself hit-tests.
WebLinkLabel {
width: 100%;
height: 100%;
align-items: center;
text-align: center;
cursor: blank-hand;
font-family: "Material Icons";
font-size: 32px;
color: $white;
}
}
// Uploader's Steam avatar (SteamAvatar, 40px like the chips), a face SIBLING overlaying the
// face's bottom-right corner: inset by the 3px border, so top = 190 face - 3 - 40.
SteamAvatar {
position: absolute;
top: 147px;
right: 3px;
border: 1px solid $line;
z-index: 4;
&:hover { border-color: $accent; }
}
.caption {
width: 190px;
flex-direction: row;
align-items: center;
.text {
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
.name {
font-size: 20px;
line-height: 22px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: $white;
}
.author {
font-size: 16px;
line-height: 18px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: $muted;
}
}
// Per-level leaderboard. A sibling of the face — a child would bubble its click into
// the play action (see the razor).
.board {
width: 40px;
height: 40px;
flex-shrink: 0;
align-items: center;
justify-content: center;
background-color: rgba( 255, 255, 255, 0.06 );
border: 1px solid $line;
cursor: blank-hand;
&:hover { background-color: $on; border-color: $accent; }
.icon {
font-family: "Material Icons";
font-size: 24px;
color: $white;
}
}
}
}
.load-more-row {
width: 100%;
align-items: center;
justify-content: center;
padding: 8px 0 4px;
.load-more {
padding: 10px 28px;
font-size: 24px;
color: $white;
background-color: rgba( 255, 255, 255, 0.06 );
border: 1px solid $line;
cursor: blank-hand;
&:hover { background-color: rgba( 255, 255, 255, 0.14 ); }
}
}
}
.footer {
flex-direction: row;
align-items: center;
margin-top: 8px;
pointer-events: all;
.back {
padding: 10px 28px;
font-size: 30px;
color: $black;
background-color: #c98a8a;
cursor: blank-hand;
&:hover { background-color: #d99a9a; }
}
}
}