ui/leveleditorhud.razor.scss
// Shared theme: pulled in for the .square-btn letterbox buttons (and its palette vars). The
// editor panel chrome below keeps its own local palette.
@import "Theme.scss";
$bg: rgba( 18, 22, 28, 0.92 );
$panel: rgba( 30, 36, 46, 0.96 );
$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 );
$danger: rgba( 190, 70, 70, 1 );
$charmatch: rgba( 130, 205, 145, 1 ); // Character/search text naming a character (keep in sync with LevelBrowserOverlay)
$musicmatch: rgba( 130, 190, 220, 1 ); // Music text naming a real song (light blue, same L/C as $charmatch)
// Must be the component name: the panel's element name is its type, so a bare `root` selector never
// matched. That was invisible while absolute children anchored past the unstyled root to the screen,
// but engine 0ff19a62e3 anchors them to the parent — unmatched, this root is a zero-width box.
LevelEditorHud {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
font-family: "Roboto";
font-size: 15px;
color: $text;
}
.canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: all;
}
.shortcut-input {
position: absolute;
width: 0;
height: 0;
pointer-events: none;
}
// Floating width×height readout anchored over the obstacle in the level (left/top set inline as a
// screen %). Fixed width + centred text + negative margins so it sits centred just above the box top
// regardless of the label's text length. Never eats pointer input.
.dim-label {
position: absolute;
width: 120px;
margin-left: -60px; // centre horizontally on the anchor
margin-top: -34px; // lift above the box top edge
pointer-events: none;
justify-content: center;
align-items: center;
color: $text;
font-size: 15px;
font-weight: 700;
text-align: center;
white-space: nowrap;
text-shadow: 0px 0px 4px rgba( 0, 0, 0, 0.9 );
}
.panel {
position: absolute;
top: 24px;
z-index: 1;
pointer-events: all;
background-color: $panel;
border: 1px solid $line;
border-radius: 0;
padding: 12px;
flex-direction: column;
gap: 6px;
.section {
width: 100%;
flex-direction: column;
gap: 6px;
background-color: rgba( 255, 255, 255, 0.025 );
padding: 2px;
}
// The daily-template controls live in their own visually distinct section (cool-tinted), so
// they read as a separate tool from the load-id / import-JSON row above.
.section.template-section {
background-color: rgba( 90, 180, 235, 0.07 );
padding: 4px 2px;
}
.heading {
color: $accent;
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
margin-top: 6px;
margin-bottom: 2px;
}
.hint {
color: $muted;
font-size: 12px;
&.tutorial {
font-size: 10px;
}
}
.shortcut {
margin-left: 4px;
color: $muted;
opacity: 0.35;
}
.error {
color: $danger;
font-size: 13px;
white-space: pre-wrap;
}
// Publish success line (shares the error label's slot under the save row).
.publish-status {
color: rgba( 130, 190, 240, 1 );
font-size: 10px;
line-height: 12px;
white-space: pre-wrap;
}
.row {
flex-direction: row;
gap: 6px;
flex-wrap: wrap;
}
button {
background-color: rgba( 255, 255, 255, 0.06 );
border: 1px solid $line;
border-radius: 0;
padding: 4px 8px;
color: $text;
font-size: 14px;
align-items: center;
justify-content: center;
&:hover {
background-color: rgba( 255, 255, 255, 0.14 );
}
&.on {
background-color: $on;
border-color: $accent;
}
&.on:hover {
background-color: rgba( 80, 150, 205, 1 );
}
&.on:active {
background-color: rgba( 105, 180, 235, 1 );
}
&.disabled {
pointer-events: none;
opacity: 0.65;
}
// Inactive action buttons (blank level id/name or template name): drop the save/danger
// tint so the greyed-out state reads at a glance.
&.save.disabled,
&.danger.disabled {
background-color: rgba( 255, 255, 255, 0.05 );
color: $muted;
}
&.danger { background-color: rgba( 150, 50, 50, 0.5 ); }
&.danger:hover { background-color: $danger; }
&.exit { background-color: rgba( 90, 70, 40, 0.5 ); }
&.save { flex-grow: 1; background-color: rgba( 45, 125, 70, 0.55 ); }
&.save:hover { background-color: rgba( 70, 170, 95, 0.95 ); }
// Workshop publish (standalone save row): steam-y blue beside the green Save.
&.publish { background-color: rgba( 45, 90, 140, 0.55 ); }
&.publish:hover { background-color: rgba( 70, 130, 195, 0.95 ); }
&.publish.disabled {
background-color: rgba( 255, 255, 255, 0.05 );
color: $muted;
}
}
}
.tools {
left: 24px;
width: 344px;
.grid-reset {
width: 28px;
height: 28px;
padding: 0;
flex-shrink: 0;
.micon {
font-family: "Material Icons";
font-size: 17px;
color: $muted;
pointer-events: none;
}
&:hover .micon {
color: $text;
}
}
.toolgrid {
flex-direction: row;
flex-wrap: wrap;
gap: 6px;
.tool {
width: 148px;
height: 40px;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 6px;
padding: 4px 8px;
font-size: 13px;
.ticon {
font-family: "Material Icons";
font-size: 18px;
color: $muted;
flex-shrink: 0;
}
> span {
white-space: nowrap;
}
&.on .ticon { color: $text; }
}
}
.sel {
color: $muted;
font-size: 13px;
margin-top: 4px;
}
}
.inspector {
right: 24px;
bottom: 24px;
width: 380px;
padding: 2px 4px 2px 4px;
overflow-y: scroll;
overflow-x: hidden;
> .section,
> .top-save-row {
flex-shrink: 0;
}
.field {
flex-direction: row;
align-items: center;
gap: 8px;
> label {
width: 90px;
color: $muted;
font-size: 13px;
}
EditorTextEntry {
flex-grow: 1;
background-color: rgba( 0, 0, 0, 0.35 );
border: 1px solid $line;
border-radius: 0;
padding: 4px 8px;
color: $text;
// Focus pop: the engine repaints a panel only when its style changes, so without a
// :focus rule a clicked box shows no caret until the first keystroke.
&:focus { border-color: $accent; }
// Save-blocking id (interior spaces) reads red before the save button is even tried.
&.invalid {
color: rgba( 235, 110, 110, 1 );
border-color: $danger;
}
// Character field: the text resolves to a character (exact id or display name) and is in effect.
&.character-match { color: $charmatch; }
// Music field: the typed name is a real song (light blue), or is a real song another mapped
// level already uses (red — same accident the picker's music badge flags).
&.music-match { color: $musicmatch; }
&.music-dupe { color: rgba( 235, 110, 110, 1 ); }
}
// Dice button beside the Music field.
.music-rand {
width: 24px;
height: 22px;
padding: 0;
flex-shrink: 0;
align-items: center;
justify-content: center;
.micon {
font-family: "Material Icons";
font-size: 15px;
color: $muted;
}
&:hover .micon { color: $text; }
}
&.wide {
flex-direction: column;
align-items: stretch;
> label {
width: auto;
}
}
EditorTextEntry.multiline {
min-height: 40px;
white-space: pre-wrap;
}
&.pattern-rows {
align-items: flex-start;
EditorTextEntry.multiline {
height: 32px;
min-height: 32px;
}
}
}
.level-fields {
flex-direction: row;
align-items: center;
gap: 8px;
.field {
flex-grow: 1;
min-width: 0;
gap: 4px;
> label {
width: auto;
flex-shrink: 0;
}
EditorTextEntry {
min-width: 0;
}
}
.delete-level-actions {
margin-left: auto;
flex-direction: row;
gap: 6px;
flex-shrink: 0;
}
}
.import-row {
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
gap: 4px;
button {
padding: 4px;
font-size: 11px;
white-space: nowrap;
flex-shrink: 0;
}
// The template row reuses the panel-wide green .save look, but stays row-sized (the
// top-save-row variant grows to fill its bar).
button.save {
flex-grow: 0;
}
EditorTextEntry {
min-width: 0;
flex-grow: 1;
background-color: rgba( 0, 0, 0, 0.35 );
border: 1px solid $line;
border-radius: 0;
padding: 4px;
color: $text;
&:focus { border-color: $accent; }
// Template name field: the typed name is already taken, so Save would overwrite that template.
&.template-dupe { color: rgba( 235, 110, 110, 1 ); }
}
EditorTextEntry.multiline {
height: 28px;
min-height: 28px;
max-height: 28px;
overflow-y: scroll;
}
.copy-json {
width: 28px;
height: 28px;
padding: 0;
justify-content: center;
.micon {
font-family: "Material Icons";
font-size: 16px;
}
}
// Small numeric template-weight box; the name entry keeps the flexible width. No text-align
// (it doesn't reach the TextEntry's inner label and squeezing the box clipped the value).
EditorTextEntry.tweight {
flex-grow: 0;
width: 48px;
min-width: 48px;
padding: 4px 6px;
}
}
.toggles .toggle {
flex-grow: 1;
}
.refreshrow {
flex-direction: row;
justify-content: flex-end;
.mini {
width: 26px;
height: 22px;
padding: 0;
flex-shrink: 0;
.micon {
font-family: "Material Icons";
font-size: 16px;
color: $muted;
}
&:hover .micon {
color: $text;
}
}
}
.count {
align-items: center;
label {
color: $muted;
}
.num {
width: 40px;
text-align: center;
color: $text;
}
button {
width: 34px;
}
}
.environment-choice {
flex-wrap: nowrap;
> label:first-child {
width: 76px;
flex-shrink: 0;
}
.num {
width: auto;
min-width: 0;
flex-grow: 1;
white-space: nowrap;
font-size: 12px;
}
}
.environment-randomizers {
justify-content: flex-end;
button {
width: 28px;
height: 26px;
padding: 0;
flex-shrink: 0;
}
.micon {
font-family: "Material Icons";
font-size: 17px;
color: $muted;
pointer-events: none;
}
button:hover .micon {
color: $text;
}
.alt-pattern-toggle {
margin-right: auto;
&.on {
background-color: $on;
border-color: $accent;
}
}
}
.alternate-choice {
background-color: rgba( 235, 170, 60, 0.07 );
}
.custom-pattern-editor {
flex-direction: column;
gap: 4px;
padding: 4px 0;
.pattern-dimension .glabel {
width: 96px;
color: $muted;
}
}
.chips {
flex-direction: row;
flex-wrap: wrap;
//gap: 2px;
.chip {
padding: 4px 8px;
font-size: 12px;
background-color: rgba( 255, 255, 255, 0.08 );
}
// Block-type buttons drawn as the baked face art (sprites/ui/blocks/*, set inline). The art is
// an opaque square, so hover/active feedback is a border tint rather than the background colour
// (which the icon would cover). Border is present-but-transparent at rest so hovering never
// shifts layout.
// 60px so a gapless row of 6 spans the section's inner width exactly (found empirically:
// the box arithmetic from the 380px panel suggested 61px, but the real rendered width is
// 360px), keeping the grid flush against both edges instead of leaving the leftover as
// fake right-side padding.
.chip.bicon {
width: 60px;
height: 60px;
padding: 0;
border: 1px solid transparent;
background-size: 100% 100%;
background-repeat: no-repeat;
image-rendering: pixelated;
justify-content: flex-end;
align-items: flex-start;
// Corner ✕ on removable (pool) chips.
.x {
font-size: 12px;
color: $text;
margin: 2px 4px 0 0;
text-shadow: 1px 1px 0px rgba( 0, 0, 0, 0.85 );
}
}
.chip.bicon:hover {
border-color: rgba( 255, 255, 255, 0.85 );
}
.chip.bicon:active {
border-color: rgba( 110, 170, 225, 0.95 );
}
&.add .chip.add {
background-color: rgba( 60, 90, 120, 0.4 );
}
&.add .chip.add:hover {
background-color: rgba( 80, 130, 180, 0.75 );
}
&.add .chip.add:active {
background-color: rgba( 110, 170, 225, 0.95 );
}
}
.blocklist {
flex-direction: column;
gap: 4px;
.blockrow {
flex-direction: row;
align-items: center;
gap: 6px;
background-color: rgba( 255, 255, 255, 0.05 );
border: 1px solid $line;
border-radius: 0;
padding: 3px 4px;
// Baked face-art stand-in for the old block-type text label (type name lives in the tooltip).
.btype-icon {
width: 34px;
height: 34px;
flex-shrink: 0;
background-size: 100% 100%;
background-repeat: no-repeat;
image-rendering: pixelated;
}
.phase {
flex-direction: row;
align-items: center;
gap: 2px;
button {
padding: 2px 6px;
font-size: 13px;
}
.num {
width: 30px;
text-align: center;
color: $muted;
font-size: 13px;
white-space: nowrap;
}
}
.sides {
flex-direction: row;
gap: 2px;
.sbtn {
width: 22px;
padding: 2px 0;
font-size: 12px;
color: $muted;
}
.sbtn.on {
color: $text;
}
}
.rm {
margin-left: auto;
padding: 2px 7px;
color: $muted;
}
}
}
.code {
background-color: rgba( 0, 0, 0, 0.5 );
border: 1px solid $line;
border-radius: 0;
padding: 8px;
font-family: "Consolas";
font-size: 12px;
color: rgba( 200, 230, 210, 1 );
white-space: pre-wrap;
max-height: 220px;
overflow-y: scroll;
}
}
// Grid controls (in the TOOLS panel). `.row.grid-row` (specificity ties `.panel .row` and comes
// later) so it can override the base row's `flex-wrap: wrap` — without nowrap the label + slider
// exceed the 240px panel and the slider wraps to a second line, leaving a tall vertical gap.
.row.grid-row {
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
gap: 4px;
.glabel {
flex-shrink: 0;
width: 96px;
color: rgba( 255, 255, 255, 1 );
}
.slider-wrap {
flex-grow: 1;
}
}
.row.grid-row.ability-row {
.glabel {
width: 122px;
}
EditorTextEntry {
min-width: 0;
flex-grow: 1;
background-color: rgba( 0, 0, 0, 0.35 );
border: 1px solid $line;
border-radius: 0;
padding: 4px 8px;
color: $text;
&:focus { border-color: $accent; }
}
}
// Square home button in the bottom-left letterbox: returns to the main menu. All the square
// buttons here get their look from the shared .square-btn (Theme.scss); these rules add ONLY
// position and the per-button accent colours.
.square-btn.home-button {
position: absolute;
z-index: 1;
bottom: 40px;
left: 40px;
border-color: #ff7b7b;
.img { background-tint: #ff9b9b; }
}
// Square back button, stacked directly ABOVE the home button (bottom row is at 40px, 72px tall +
// 12px gap): returns to the workshop screen.
.square-btn.back-button {
position: absolute;
z-index: 1;
bottom: 124px;
left: 40px;
border-color: #b79bff;
.img { background-tint: #cbb8ff; }
}
// Square clear button, immediately right of the home button (home is left:40px, 72px wide + 12px gap):
// resets the editor to a fresh default level.
.square-btn.clear-button {
position: absolute;
z-index: 1;
bottom: 40px;
left: 124px;
border-color: #e0b050;
.img { background-tint: #f0cf80; }
}
// Square level-browser button immediately left of Test Play. It shares the left-letterbox anchor: its
// right edge sits one button width plus a 12px gap further left than Test Play's 560px offset.
.square-btn.browse-button {
position: absolute;
z-index: 1;
bottom: 40px;
right: 50%;
// One button-width (72) + 12px gap left of Test Play's 580px inset.
margin-right: 664px;
border-color: #7bb8ff;
.img { background-tint: #9bcbff; }
}
// Character picker in the LEFT letterbox, sitting above the test-play button. Anchored to screen centre
// so it hugs the square edge on any aspect (same trick as the test-play button): right:50% puts its
// right edge at centre, then margin-right pushes it left past the square half-width (540) + a gap.
.character-slot {
position: absolute;
z-index: 1000;
right: 50%;
margin-right: 580px;
bottom: 130px;
align-items: flex-end;
}
// Square reload-characters button on the right side of the LEFT letterbox, directly above the character
// picker. Shares the picker's screen-centre anchor (right:50% + margin-right:560px) so its right edge
// lines up with the picker; sits above the 100px picker trigger (bottom:130px) plus a gap.
.square-btn.reload-characters-button {
position: absolute;
z-index: 1;
bottom: 242px;
right: 50%;
margin-right: 580px;
border-color: #c79bff;
.img { background-tint: #d9bcff; }
}
// Square test-play button at the bottom-right of the LEFT letterbox (just left of the centred square).
.square-btn.test-play-button {
position: absolute;
z-index: 1;
bottom: 40px;
right: 50%;
// 580 = square half-width (540) + 40, so the gap to the square edge matches the 40px bottom inset.
margin-right: 580px;
border-color: #7bff8f;
.img { background-tint: #9bff9b; }
}