ui/levelbrowseroverlay.razor.scss
// Editor level-browser overlay. Designed in 1080-reference space (ScreenPanel AutoScreenScale =
// ConsistentHeight — same coordinate space as the editor HUD and the level-select map). Opaque,
// instant (no transitions/animations so it pops in/out immediately). CLAMPED to the central 1080²
// play-square region so it never overlaps / clips the editor's side panels (tools / inspector).
$bg: rgba( 12, 15, 20, 0.98 );
$panel: rgba( 30, 36, 46, 0.98 );
$line: rgba( 255, 255, 255, 0.12 );
$text: rgba( 235, 240, 245, 1 );
$muted: rgba( 170, 180, 190, 1 );
$accent: rgba( 90, 180, 235, 1 );
$on: rgba( 60, 120, 170, 1 );
$mapline: rgba( 79, 154, 160, 1 );
$charmatch: rgba( 130, 205, 145, 1 ); // search/Character text naming a character (keep in sync with LevelEditorHud)
$blockmatch: rgba( 220, 180, 120, 1 ); // search text naming a block type (muted amber, same L/C as $charmatch)
// Match the shared theme's button behavior: labels and icons are visual content, not independent
// mouse targets. Keeping one target prevents s&box from cancelling release after a child boundary.
button > * {
pointer-events: none;
}
.level-browser {
// Clamp to the centred 1080-wide play square (its left edge is screen-centre − 540). Full height
// so it covers the whole square; the side letterboxes (and their panels) stay outside it.
position: absolute;
top: 0;
left: 50%;
margin-left: -540px;
width: 1080px;
height: 100%;
z-index: 100;
pointer-events: all;
align-items: center;
justify-content: center;
font-family: "Roboto";
color: $text;
.backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: $bg;
cursor: blank-hand;
}
.window {
position: relative;
width: 1032px;
max-width: 100%;
height: 1000px;
max-height: 98%;
flex-direction: column;
background-color: $panel;
border: 2px solid $line;
padding: 24px;
gap: 16px;
}
.topbar {
flex-direction: row;
align-items: center;
justify-content: space-between;
.title {
font-size: 34px;
font-weight: 700;
letter-spacing: 3px;
color: $accent;
}
.modes {
flex-direction: row;
gap: 10px;
.mode {
background-color: rgba( 255, 255, 255, 0.06 );
border: 1px solid $line;
padding: 10px 22px;
font-size: 22px;
color: $text;
cursor: blank-hand;
&:hover { background-color: rgba( 255, 255, 255, 0.14 ); }
&.on {
background-color: $on;
border-color: $accent;
}
// The EDIT toggle reads amber when armed so the map's changed click behaviour
// (replace / add instead of load) is unmistakable.
&.edit.on {
background-color: rgba( 170, 110, 35, 1 );
border-color: rgba( 255, 190, 90, 1 );
}
// Back (inner edit picker) reads muted red so it's clearly "abandon this pick",
// distinct from the Other/Test pool toggles beside it.
&.back {
background-color: rgba( 105, 45, 50, 1 );
border-color: rgba( 200, 115, 115, 0.8 );
&:hover { background-color: rgba( 150, 60, 65, 1 ); }
}
}
}
.close {
width: 56px;
height: 56px;
align-items: center;
justify-content: center;
background-color: rgba( 255, 255, 255, 0.06 );
border: 1px solid $line;
cursor: blank-hand;
&:hover { background-color: rgba( 190, 70, 70, 0.8 ); }
.icon {
font-family: "Material Icons";
font-size: 34px;
color: $text;
}
}
}
// ── search row: the fixed-size filter box at the LEFT (every view), sort toggle at the RIGHT ──────
.searchbar {
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 8px;
// Fixed footprint: long text scrolls/clips INSIDE the entry, never resizes the box.
.search-box {
position: relative;
width: 340px;
height: 46px;
flex-shrink: 0;
.search-entry {
width: 100%;
height: 100%;
padding: 6px 50px 6px 12px; // right padding keeps text clear of the X button
align-items: center;
background-color: rgba( 0, 0, 0, 0.35 );
border: 1px solid $line;
font-size: 20px;
color: $text;
caret-color: $accent;
white-space: nowrap;
overflow: hidden;
.content-label {
white-space: nowrap;
overflow: hidden;
}
&:focus { border-color: $accent; }
// The text names a character exactly (id or display name) — forced-character match is live.
&.character-match { color: $charmatch; }
// The text names a block type exactly ("sad", "wind") — spawns-that-block match is live.
&.block-match { color: $blockmatch; }
}
// Red X in the box's top-right, shown only while there's text (see the razor).
.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 ); }
}
}
// Map-mode zoom bar (right side, where the grid views put their sort toggle): − / slider / +
// with a live percent readout. Scales the whole map layout via inline styles (see the razor).
.zoombar {
flex-direction: row;
align-items: center;
gap: 8px;
width: 330px;
flex-shrink: 0;
.zoom-btn {
width: 36px;
height: 36px;
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: rgba( 255, 255, 255, 0.14 ); }
.icon {
font-family: "Material Icons";
font-size: 22px;
color: $text;
}
}
.zoom-label {
width: 52px;
flex-shrink: 0;
justify-content: flex-end;
font-size: 17px;
text-align: right;
color: $muted;
}
}
.sort {
background-color: rgba( 255, 255, 255, 0.06 );
border: 1px solid $line;
min-width: 84px;
padding: 6px 10px;
font-size: 17px;
align-items: center;
justify-content: center;
text-align: center;
color: $text;
cursor: blank-hand;
&:hover { background-color: rgba( 255, 255, 255, 0.14 ); }
&.on {
background-color: $on;
border-color: $accent;
}
}
}
// ── grid mode: wrap the fixed-size tiles left-to-right / top-to-bottom, vertically scrollable ─────
.grid {
position: relative; // anchors the centred no-results notice
flex-grow: 1;
flex-direction: row;
flex-wrap: wrap;
align-content: flex-start;
justify-content: center;
gap: 20px;
overflow-y: scroll;
overflow-x: hidden;
// Centred notice when a search matches nothing in the current view.
.no-results {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
label {
font-size: 30px;
letter-spacing: 3px;
color: $muted;
}
}
.tile {
flex-direction: column;
width: 150px;
align-items: center;
cursor: blank-hand;
gap: 4px;
.face {
position: relative;
width: 150px;
height: 150px;
overflow: hidden;
border: 3px solid rgba( 63, 124, 132, 1 );
// Templates view: the template's pick weight, as a small chip in the preview's
// top-right corner (kept off the caption line below).
.weight {
position: absolute;
top: 0;
right: 0;
padding: 1px 7px 2px 7px;
font-size: 16px;
color: $text;
background-color: rgba( 12, 15, 20, 0.75 );
pointer-events: none;
z-index: 2;
}
// Templates view: swap glyph in the top-left corner of templates that may also
// spawn mirrored left-to-right (AllowMirrorX).
.mirror {
position: absolute;
top: 0;
left: 0;
padding: 1px 5px 2px 5px;
font-family: "Material Icons";
font-size: 17px;
color: $text;
background-color: rgba( 12, 15, 20, 0.75 );
pointer-events: none;
z-index: 2;
}
// Templates view: spike glyph in the bottom-right corner of templates whose spikes
// always spawn exactly as authored (AuthoredSpikes) instead of rolling per-surface
// each day.
.authored-spikes {
position: absolute;
right: 0;
bottom: 0;
padding: 1px 5px 2px 5px;
font-family: "Material Icons";
font-size: 17px;
color: $text;
background-color: rgba( 12, 15, 20, 0.75 );
pointer-events: none;
z-index: 2;
}
// Templates view: red warning in the bottom-left when the template has fewer free
// spawn slots than the biggest daily block roll (the generator clamps those rolls).
.slots-warning {
position: absolute;
left: 0;
bottom: 0;
padding: 1px 5px 2px 5px;
font-family: "Material Icons";
font-size: 18px;
color: rgba( 235, 90, 90, 1 );
background-color: rgba( 12, 15, 20, 0.75 );
pointer-events: none;
z-index: 2;
}
.tile-forced-character {
position: absolute;
left: 6px;
bottom: 6px;
pointer-events: none;
// Same pre-baked soft glow the character picker uses, so the character's dark
// outer pixels read against the level preview (see CharacterPicker.razor.scss).
.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% );
}
}
}
}
.caption {
width: 150px;
flex-direction: row;
align-items: flex-start;
justify-content: center;
.name, .tag {
font-size: 18px;
line-height: 22px;
}
.name {
flex-shrink: 1;
min-width: 0;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: $text;
}
.tag {
flex-shrink: 0;
margin-right: 6px;
color: rgba( 145, 155, 165, 1 );
}
&.tagged .name {
text-align: left;
}
}
&:hover {
.face { border-color: #ffffff; box-shadow: 0px 0px 18px 2px rgba( 255, 255, 255, 0.6 ); }
}
}
}
// ── map mode: the map canvas (sized inline) inside a scroll region; nodes/lines in map coords ────
.map-scroll {
flex-grow: 1;
overflow: scroll;
.map {
position: relative;
// Keep the canvas at its full (inline) size instead of being shrunk to the scroll viewport,
// so it can overflow HORIZONTALLY (a wide map with a long side chain) and scroll on X too.
flex-shrink: 0;
.line {
position: absolute;
background-color: $mapline;
}
// EDIT-mode + button: an empty adjacent grid cell a new side level can be added to,
// rendered near-flush against the node it hangs off.
.add-cell {
position: absolute;
align-items: center;
justify-content: center;
background-color: rgba( 255, 255, 255, 0.04 );
border: 2px solid rgba( 111, 224, 208, 0.45 );
cursor: blank-hand;
z-index: 2;
.icon {
font-family: "Material Icons";
font-size: 30px;
color: rgba( 111, 224, 208, 0.85 );
}
&:hover {
background-color: rgba( 111, 224, 208, 0.25 );
border-color: #ffffff;
.icon { color: #ffffff; }
}
// Insert-between variant: sits ON a connection line (spine or side), so its face is
// opaque to mask the line behind it (including the hover tint, pre-blended here).
&.insert {
background-color: $panel;
&:hover { background-color: rgba( 50, 83, 86, 1 ); }
}
}
// EDIT-mode X button straddling a deletable node's top-right corner.
.delete-node {
position: absolute;
align-items: center;
justify-content: center;
background-color: rgba( 140, 52, 55, 0.95 );
border: 2px solid rgba( 220, 125, 125, 0.9 );
cursor: blank-hand;
z-index: 3;
.icon {
font-family: "Material Icons";
font-size: 20px;
color: rgba( 245, 230, 230, 1 );
}
&:hover {
background-color: rgba( 200, 70, 70, 1 );
border-color: #ffffff;
.icon { color: #ffffff; }
}
}
// Positioned wrapper (left/top/size set inline).
.node-wrap {
position: absolute;
cursor: blank-hand;
.node-name {
position: absolute;
left: 0;
right: 0;
top: -29px;
height: 25px;
padding: 2px 6px;
align-items: center;
justify-content: center;
overflow: hidden;
background-color: #171b22;
pointer-events: none;
z-index: 2;
label {
width: 100%;
font-size: 18px;
line-height: 21px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: $text;
}
}
// The clipped level preview + coloured frame fills the wrapper.
.face {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
border: 4px solid rgba( 111, 224, 208, 1 );
// Mask connector lines even before a windowed preview is mounted.
background-color: $panel;
z-index: 1;
// Forced-character badge in the preview's lower-left, matching the grid tiles
// (same glow-behind-sprite structure — see the .grid .tile version above).
.tile-forced-character {
position: absolute;
left: 5px;
bottom: 5px;
pointer-events: none;
.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 — see the .grid .tile version above.
&.paired {
.sprite.rear {
transform: translate( 11%, -9% ) scale( 0.94 );
}
.sprite.front {
transform: translate( -11%, 9% );
}
}
}
// Music-override badge in the preview's lower-right (editor map only): red when the
// same track is set on more than one mapped level. Glyph size re-emitted inline per zoom.
.music-badge {
position: absolute;
right: 0;
bottom: 0;
padding: 1px 4px 2px 4px;
font-family: "Material Icons";
font-size: 18px;
color: $text;
background-color: rgba( 12, 15, 20, 0.75 );
pointer-events: none;
z-index: 2;
&.dupe { color: rgba( 235, 90, 90, 1 ); }
}
}
&:hover {
.face { border-color: #ffffff; box-shadow: 0px 0px 22px 3px rgba( 255, 255, 255, 0.85 ); }
}
}
}
}
}