beggingings of file export, improve button and input events

This commit is contained in:
CodedSakura 2026-07-12 15:03:32 +03:00
parent 3e2a17f5cd
commit b69bcacb78
12 changed files with 421 additions and 123 deletions

File diff suppressed because one or more lines are too long

View File

@ -9,21 +9,24 @@
<body>
<nav id="top-menu" class="menu-bar">
<div data-section="File" data-alt="f">
<button data-alt="n" data-button="new-file">New</button>
<button data-alt="o" data-button="open-file">Open</button>
<button data-alt="n" data-button="file:new">New</button>
<button data-alt="o" data-button="file:load">Open</button>
<button data-alt="s" data-button="file:save">Save</button>
<div data-section="Recent Files" data-alt="r">
<button disabled>-</button>
</div>
</div>
<div data-section="Edit" data-alt="e">
<button data-alt="u" data-button="undo">Undo</button>
<button data-alt="r" data-button="redo" disabled>Redo</button>
<button data-alt="u" data-button="edit:undo">Undo</button>
<button data-alt="r" data-button="edit:redo" disabled>Redo</button>
</div>
<div data-section="Select" data-alt="s">
<button data-alt="i">Invert</button>
</div>
<div data-section="View" data-alt="v">
<button data-alt="c" data-toggle data-toggled="false" data-button="show-casing">Show casing</button>
<button data-alt="i" data-button="planner:zoom:+">Zoom in</button>
<button data-alt="o" data-button="planner:zoom:-">Zoom out</button>
<button data-alt="c" data-toggle="planner:toggle-casing" data-toggled="false">Show casing</button>
</div>
</nav>
<aside id="left-menu" data-state="planning">
@ -31,9 +34,9 @@
<section id="block-picker">
<label>
Filter:
<input data-input="block-filter">
<input data-input="block-picker:filter">
</label>
<div class="list" id="block-list" data-retain-tooltip-hover>
<div class="list" id="block-picker-list" data-retain-tooltip-hover>
<template>
<div class="m1"></div>
</template>
@ -47,6 +50,21 @@
<input type="file" data-input="configuration" accept=".ncpf.json" />
</div>
<div data-state="planning">
<header class="status-bar">
<div>
<!--left-->
</div>
<div>
<!--center-->
</div>
<div>
<!--right-->
<label title="Planner zoom level" class="m1">
Zoom:
<select data-input="planner:zoom"></select>
</label>
</div>
</header>
<div class="list" id="planner" data-retain-tooltip-hover>
<template>
<div class="grid m2">
@ -61,9 +79,9 @@
<aside id="right-menu">
<span data-resize="right-menu"></span>
</aside>
<footer id="bottom-menu">
<aside id="bottom-menu">
<span data-resize="bottom-menu"></span>
</footer>
</aside>
<div class="dialog hidden" data-dialog="fileLoad:chooseDesign">
<section>
<header>Choose a design to open</header>

View File

@ -11,7 +11,7 @@ export class DomList {
clear() {
this.list.querySelectorAll("[data-tooltip]").forEach(e => deleteTooltip(Number(e.dataset.tooltip)))
this.list.replaceChildren(...this.list.querySelectorAll("template, [data-keep]"));
this.list.replaceChildren(...this.list.querySelectorAll(":scope > template, :scope > [data-keep]"));
}
makeItem() {

View File

@ -1,3 +1,5 @@
import { emitEvent } from "./events.js";
export function getElements(dataType, dataValue, {
additionalDataFilters = [],
additionalPropertyFilters = [],
@ -25,6 +27,10 @@ export function addEventToButton(dataValue, event, callback, props = {}) {
addEventToElement("button", dataValue, event, callback, props);
}
export function emitOnButtonClick(dataValue, eventName, props = {}) {
addEventToButton(dataValue, "click", e => emitEvent(eventName, e), props);
}
export function setText(dataValue, text, props = {}) {
getElements("text", dataValue, props)
.forEach(e => e.innerText = text);

View File

@ -7,7 +7,13 @@ export function emitEvent(name, data = null) {
}
export function listenTo(name, callback) {
window.addEventListener(`ncp:${name}`, callback);
if (!Array.isArray(name)) {
name = [name];
}
for (const n of name) {
window.addEventListener(`ncp:${n}`, callback);
}
}
export function awaitEvent(name) {

View File

@ -1,6 +1,14 @@
import { awaitEvent, emitEvent } from "./events.js";
import { clearPlanner, loadPlanner } from "./planner.js";
import { getAtlasIndexFromMetadata } from "./atlas.js";
import { awaitEvent, emitEvent, listenTo } from "./events.js";
import { findFullEntry, stripEntryToMinimums } from "./ncpf-utils.js";
import { getPlannerMultiblockSize, getPlannerMultiblockType, loadPlanner, readPlannerTiles } from "./planner.js";
import { getAtlasIndexFromMetadata, getAtlasMetadata } from "./atlas.js";
import { resetSolver } from "./solver.js";
export function initializeFileEvents() {
listenTo("file:load", () => showOpenFileDialog());
listenTo("file:new", () => {});
listenTo("file:save", () => saveFile("test"));
}
export function showOpenFileDialog() {
const input = document.createElement("input");
@ -26,6 +34,7 @@ export async function loadExampleFile(filename) {
}
export async function loadFile(data) {
console.log(data);
if (!data.designs) throw new Error("invalid file");
if (data.designs.length === 0) throw new Error("no designs");
@ -45,4 +54,64 @@ export async function loadFile(data) {
const designWithAtlas = design.design.map(a => a.map(b => b.map(c => c === -1 ? -1 : blocks[c])));
loadPlanner(design.dimensions, design.type, designWithAtlas);
resetSolver(design);
}
export async function saveFile(name) {
const multiblockType = getPlannerMultiblockType();
const designAsAtlas = readPlannerTiles();
const designBlocksAtlasMap = Object.fromEntries(
[...new Set(designAsAtlas.flat(3))]
.filter(v => v !== -1)
.sort((a, b) => a - b)
.map((ai, di) => [ ai, { ...getAtlasMetadata(ai), designIndex: di } ])
);
const designAsBlocks = designAsAtlas
.map(xPlane => xPlane
.map(yRow => yRow.map(atlasIndex => designBlocksAtlasMap[atlasIndex]))
);
const designAsBlockIndices = designAsBlocks
.map(xPlane => xPlane.map(yRow => yRow.map(b => b?.designIndex ?? -1)));
const designBlocksWithRecipes = designAsBlocks
.map(xPlane => xPlane
.map(yRow => yRow
.map(block => {
if (!block) return;
return findFullEntry("blocks", block, { configurationHint: multiblockType });
})
.filter(b => b && b.modules && "ncpf:block_recipes" in b.modules)
)
.filter(r => r.length > 0)
)
.filter(p => p.length > 0);
console.log("designBlocksWithRecipes", designBlocksWithRecipes)
const minimizedBlocks = Object.entries(designBlocksAtlasMap)
.sort(([a], [b]) => a - b)
.map(([, m]) => stripEntryToMinimums(m));
const data = {
addons: [],
configurations: {
[multiblockType]: {
blocks: minimizedBlocks,
coolant_recipes: [], // FIXME
},
},
designs: [
{
block_recipes: [], // FIXME
coolant_recipe: 0, // FIXME
design: designAsBlockIndices,
dimensions: getPlannerMultiblockSize(),
type: multiblockType,
},
],
version: 1,
};
console.log(designBlocksAtlasMap);
console.log(data);
}

17
scripts/js-utils.js Normal file
View File

@ -0,0 +1,17 @@
export function initialize3dArray(x, y, z, initialValue = undefined) {
return Array.from({ length: x })
.fill(null)
.map(() => Array.from({ length: y })
.fill(null)
.map(() => Array.from({ length: z }).fill(initialValue))
);
}
export function objectKeepKeys(object, ...keys) {
if (Array.isArray(keys[0])) keys = keys[0];
return Object.fromEntries(
keys
.filter(k => k in object)
.map(k => [ k, object[k] ])
);
}

View File

@ -2,7 +2,7 @@ import { loadConfiguration } from "./configuration.js";
import { initializeDialogs } from "./dialogs.js";
import { addEventToButton, addEventToInput, debounce, getElements } from "./dom-utils.js";
import { awaitEvent, emitEvent, listenTo } from "./events.js";
import { loadExampleFile, showOpenFileDialog } from "./files.js";
import { initializeFileEvents, loadExampleFile, saveFile, showOpenFileDialog } from "./files.js";
import { initializePlanner } from "./planner.js";
import { initializeMenuResizing } from "./resize-menus.js";
import { getState, setState, STATE } from "./state.js";
@ -19,12 +19,31 @@ window.addEventListener("load", () => {
}
});
getElements("button", "*").forEach(elem => {
elem.addEventListener("click", e => {
emitEvent(elem.dataset.button, e);
});
});
getElements("toggle", "*").forEach(elem => {
elem.addEventListener("click", e => {
const wasChecked = e.currentTarget.dataset.toggled === "true";
const newState = !wasChecked;
e.currentTarget.dataset.toggled = newState.toString();
emitEvent(elem.dataset.toggle, { state: newState, event: e });
});
});
getElements("input", "*").forEach(elem => {
elem.addEventListener("keydown", e => e.stopPropagation());
elem.addEventListener("input", debounce(e => {
emitEvent(elem.dataset.input + ":input", e);
}));
elem.addEventListener("change", e => {
emitEvent(elem.dataset.input + ":change", e);
});
});
addEventToInput("configuration", "input", loadConfiguration);
addEventToButton("load-configuration", "click", loadConfiguration);
addEventToInput("block-filter", "input", debounce(e => {
emitEvent("block-picker:filter", e.target.value);
}));
addEventToButton("open-file", "click", showOpenFileDialog);
listenTo("state", () => {
console.log("State changed", getState());
@ -38,6 +57,7 @@ window.addEventListener("load", () => {
listenTo("config:end", () => setState(STATE.planning));
initializeFileEvents();
initializePlanner();
initializeMenuResizing();
initializeTooltips();
@ -50,4 +70,5 @@ window.addEventListener("load", () => {
awaitEvent("config:end").then(() => loadExampleFile("tiny reactor"));
// awaitEvent("config:end").then(() => loadExampleFile("multi-reactor"));
awaitEvent("solver:reset:done").then(() => emitEvent("file:save"));
});

View File

@ -1,4 +1,5 @@
import { configuration } from "./configuration.js";
import { objectKeepKeys } from "./js-utils.js";
export function checkEquivalence(a, b) {
if (!a.type || !b.type || a.type !== b.type) return false;
@ -17,6 +18,18 @@ export function checkEquivalence(a, b) {
return false;
}
export function stripEntryToMinimums(entry) {
if (!entry.type) throw new Error("must have a type");
switch (entry.type) {
case "legacy_block":
return objectKeepKeys(entry, "type", "name", "metadata", "blockstate");
case "oredict":
return objectKeepKeys(entry, "type", "oredict");
}
throw new Error(`unknown entry type ${entry.type}`);
}
export function findFullEntry(type, entry, { configurationHint }) {
const possibleEntries = [
...Object.entries(configuration.configuration),

View File

@ -1,18 +1,181 @@
import { atlases, makeAtlasImage } from "./atlas.js";
import { DomList } from "./dom-list.js";
import { getElements } from "./dom-utils.js";
import { emitEvent, listenTo } from "./events.js";
import { Filter } from "./filter.js";
import { initialize3dArray } from "./js-utils.js";
import { recall, save } from "./storage.js";
import { createTooltip, tooltipDataToPlain } from "./tooltip.js";
import { lookupFromMetadata, resolveBlockName } from "./ncpf-utils.js";
import { addEventToButton } from "./dom-utils.js";
const zoomOptions = [ 0.5, 1, 2, 3, 4, 5 ];
const zoomDefault = 2;
const zoomStorageKey = "zoom-size";
let suppressHover = false;
let multiblockSize = [9, 7, 9]; // width (screen x), height (xy grids), depth (screen y)
let multiblockType = "nuclearcraft:overhaul_sfr";
let activeItem = -1;
export function initializePlanner() {
listenTo("config:end", buildBlockPicker);
listenTo("config:end", buildPlanner);
listenTo("block-picker:filter:input", buildBlockPicker);
listenTo("planner:toggle-casing", e => {
const planner = document.getElementById("planner");
planner.dataset.casing = e.detail.state ? "show" : "hide";
});
const zoomLevel = recall(zoomStorageKey, zoomDefault);
document.getElementById("planner").style
.setProperty("--scale-factor", zoomLevel);
document.querySelectorAll('select[data-input="planner:zoom"]').forEach(elem => {
for (const op of zoomOptions) {
const option = document.createElement("option");
option.value = op.toString(10);
option.innerText = `${op * 100}%`;
option.selected = zoomLevel === op;
elem.appendChild(option);
}
});
listenTo("planner:zoom:change", e => {
const value = Number(e.detail.currentTarget.value);
if (zoomOptions.includes(value)) {
emitEvent("planner:zoom:set", value);
} else {
console.warn("bad planner zoom value", e);
}
});
listenTo(["planner:zoom:+", "planner:zoom:-"], e => {
const zoomIn = e.type === "ncp:planner:zoom:+";
const currentScale = Number(document.getElementById("planner").style
.getPropertyValue("--scale-factor") ?? zoomDefault);
const currentIndex = zoomOptions.indexOf(currentScale);
if (zoomIn && currentIndex < zoomOptions.length - 1) {
emitEvent("planner:zoom:set", zoomOptions[currentIndex + 1]);
}
if (!zoomIn && currentIndex > 0) {
emitEvent("planner:zoom:set", zoomOptions[currentIndex - 1]);
}
});
listenTo("planner:zoom:set", e => {
const scale = e.detail;
const scaleIndex = zoomOptions.indexOf(scale);
document.getElementById("planner").style
.setProperty("--scale-factor", scale.toString(10));
document.querySelectorAll('select[data-input="planner:zoom"]')
.forEach(elem => elem.value = scale);
getElements("button", "planner:zoom:+")
.forEach(e => e.disabled = scaleIndex >= zoomOptions.length - 1);
getElements("button", "planner:zoom:-")
.forEach(e => e.disabled = scaleIndex <= 0);
save(zoomStorageKey, scale);
});
listenTo("block-picker:selection", clearSelectedCells);
document.addEventListener("keydown", e => {
if (e.key === "H") {
suppressHover = !suppressHover;
console.log("Suppressing hover:", suppressHover);
}
});
}
function buildBlockPicker() {
const filter = Filter.fromInput("block-picker:filter");
const grid = new DomList(document.getElementById("block-picker-list"));
grid.clear();
atlases.flatMap(a => a.metadata)
.filter(m => m.configuration === multiblockType && m.configurationList === "blocks")
.forEach(m => {
const item = grid.makeItem();
item.dataset.index = m.index.toString(10);
const tooltipData = [...getBlockTooltip(m)];
item.dataset.tooltip = createTooltip(tooltipData).toString(10);
if (!filter.test(...tooltipDataToPlain(tooltipData))) {
item.classList.add("hidden");
}
item.appendChild(makeAtlasImage(m.index));
item.addEventListener("click", listClickEvent);
grid.addItem(item);
});
}
function listClickEvent(e) {
const items = document.querySelectorAll("#block-picker-list [data-index]");
if (e.currentTarget.dataset.active !== undefined) {
items.forEach(i => delete i.dataset.active);
activeItem = -1;
delete e.currentTarget.dataset.active;
emitEvent("block-picker:selection", activeItem);
emitEvent("block-picker:selection:clear");
return;
}
items.forEach(i => delete i.dataset.active);
activeItem = Number(e.currentTarget.dataset.index);
e.currentTarget.dataset.active = "";
emitEvent("block-picker:selection", activeItem);
}
function buildPlanner(textures) {
// handle case when 1st argument is event
if (!Array.isArray(textures)) textures = undefined;
const planner = document.getElementById("planner");
planner.dataset.casing = "hide";
const list = new DomList(planner);
list.clear();
for (let y = 0; y < multiblockSize[1]; y++) {
const item = list.makeItem();
item.style.setProperty("--width", multiblockSize[0]);
item.dataset.y = y.toString(10);
if (y === 0 || y === multiblockSize[1] - 1) {
item.dataset.casing = "";
}
const grid = new DomList(item);
grid.clear();
for (let z = 0; z < multiblockSize[2]; z++) {
for (let x = 0; x < multiblockSize[0]; x++) {
const texture = textures ? textures[x][y][z] : -1;
if (textures && !texture) {
console.log(x, y, z, textures);
}
const cell = grid.makeItem();
cell.dataset.x = x.toString(10);
cell.dataset.y = y.toString(10);
cell.dataset.z = z.toString(10);
cell.dataset.tile = texture.toString(10);
if (x === 0 || x === multiblockSize[0] - 1
|| y === 0 || y === multiblockSize[1] - 1
|| z === 0 || z === multiblockSize[2] - 1) {
cell.dataset.casing = "";
}
cell.appendChild(makeAtlasImage(texture));
cell.addEventListener("mouseenter", cellHoverEvent);
cell.addEventListener("mouseleave", cellHoverEvent);
cell.addEventListener("mousedown", cellClickEvent);
cell.addEventListener("mouseenter", cellClickEvent);
cell.addEventListener("mouseleave", cellClickEvent);
cell.addEventListener("contextmenu", cellClickEvent);
grid.addItem(cell);
}
}
list.addItem(item);
}
}
function cellHoverEvent(e) {
if (suppressHover) return;
if (activeItem === -1) return;
const cells = document.querySelectorAll("#planner [data-x]");
cells.forEach(c => {
@ -50,11 +213,15 @@ function cellClickEvent(e) {
return;
}
if (e.buttons === 1 && activeItem === -1) return;
if (activeItem === -1) {
cellSelectEvent(e);
return;
}
if (e.type === "mousedown") {
window.addEventListener("mouseup", cellClickEvent);
emitEvent("planner:drag:start");
clearSelectedCells();
}
if (e.type === "mouseup") {
window.removeEventListener("mouseup", cellClickEvent);
@ -97,105 +264,35 @@ function cellClickEvent(e) {
e.target.dataset.preview = activeItem.toString(10);
e.target.replaceChildren(makeAtlasImage(activeItem));
}
function listClickEvent(e) {
const items = document.querySelectorAll("#block-list [data-index]");
items.forEach(i => delete i.dataset.active);
activeItem = Number(e.currentTarget.dataset.index);
e.currentTarget.dataset.active = "";
}
export function initializePlanner() {
listenTo("block-picker:filter", buildBlockPicker);
listenTo("config:end", buildBlockPicker);
listenTo("config:end", buildPlanner);
addEventToButton("show-casing", "click", e => {
const wasChecked = e.currentTarget.dataset.toggled === "true";
const show = !wasChecked;
e.currentTarget.dataset.toggled = show.toString();
const planner = document.querySelector("#planner");
planner.dataset.casing = show ? "show" : "hide";
});
document.addEventListener("keydown", e => {
if (e.key === "H") {
suppressHover = !suppressHover;
function cellSelectEvent(e) {
if (e.type === "mousedown" && !e.ctrlKey && e.buttons !== 2) {
clearSelectedCells();
}
if (e.buttons === 2) {
// remove
const wasSelected = "selected" in e.target.dataset;
delete e.target.dataset.selected;
if (wasSelected) {
emitEvent("planner:selection:removed", e.target);
}
} else if (e.buttons === 1) {
// add
const wasSelected = "selected" in e.target.dataset;
e.target.dataset.selected = "";
if (!wasSelected) {
emitEvent("planner:selection:added", e.target);
}
}
}
function clearSelectedCells() {
getSelectedCells()
.forEach(cell => {
delete cell.dataset.selected;
emitEvent("planner:selection:removed", cell);
});
}
function buildBlockPicker() {
const filter = Filter.fromInput("block-filter");
const grid = new DomList(document.querySelector("#block-list"));
grid.clear();
atlases.flatMap(a => a.metadata)
.filter(m => m.configuration === multiblockType && m.configurationList === "blocks")
.forEach(m => {
const item = grid.makeItem();
item.dataset.index = m.index.toString(10);
const tooltipData = [...getBlockTooltip(m)];
item.dataset.tooltip = createTooltip(tooltipData).toString(10);
if (!filter.test(...tooltipDataToPlain(tooltipData))) {
item.classList.add("hidden");
}
item.appendChild(makeAtlasImage(m.index));
item.addEventListener("click", listClickEvent);
grid.addItem(item);
});
}
function buildPlanner(textures) {
// handle case when 1st argument is event
if (!Array.isArray(textures)) textures = undefined;
const planner = document.querySelector("#planner");
planner.dataset.casing = "hide";
const list = new DomList(planner);
list.clear();
for (let y = 0; y < multiblockSize[1]; y++) {
const item = list.makeItem();
item.style.setProperty("--width", multiblockSize[0]);
if (y === 0 || y === multiblockSize[1] - 1) {
item.dataset.casing = "";
}
const grid = new DomList(item);
grid.clear();
for (let z = 0; z < multiblockSize[2]; z++) {
for (let x = 0; x < multiblockSize[0]; x++) {
const texture = textures ? textures[x][y][z] : -1;
if (textures && !texture) {
console.log(x, y, z, textures);
}
const cell = grid.makeItem();
cell.dataset.x = x.toString(10);
cell.dataset.y = y.toString(10);
cell.dataset.z = z.toString(10);
cell.dataset.tile = texture.toString(10);
if (x === 0 || x === multiblockSize[0] - 1
|| y === 0 || y === multiblockSize[1] - 1
|| z === 0 || z === multiblockSize[2] - 1) {
cell.dataset.casing = "";
}
cell.appendChild(makeAtlasImage(texture));
cell.addEventListener("mouseenter", cellHoverEvent);
cell.addEventListener("mouseleave", cellHoverEvent);
cell.addEventListener("mousedown", cellClickEvent);
cell.addEventListener("mouseenter", cellClickEvent);
cell.addEventListener("mouseleave", cellClickEvent);
cell.addEventListener("contextmenu", cellClickEvent);
grid.addItem(cell);
}
}
list.addItem(item);
}
export function getSelectedCells() {
return document.querySelectorAll("#planner [data-selected]");
}
export function clearPlanner(size, type) {
@ -209,6 +306,25 @@ export function loadPlanner(size, type, textures) {
buildPlanner(textures);
}
export function readPlannerTiles() {
const planner = document.getElementById("planner");
const tiles = initialize3dArray(...multiblockSize, -1);
const grids = [...planner.children].filter(e => e.dataset.y);
for (let y = 0; y < multiblockSize[1]; y++) {
const grid = grids[y];
const gridTiles = [...grid.children].filter(e => e.dataset.tile);
for (let z = 0; z < multiblockSize[2]; z++) {
for (let x = 0; x < multiblockSize[0]; x++) {
tiles[x][y][z] = Number(gridTiles[x + z * multiblockSize[0]].dataset.tile);
}
}
}
return tiles;
}
export const getPlannerMultiblockType = () => multiblockType;
export const getPlannerMultiblockSize = () => multiblockSize;
function* getBlockTooltip(metadata) {
const data = lookupFromMetadata(metadata);
@ -336,7 +452,7 @@ function parseRuleAsTooltip(rule, props) {
}
if (rule.min === rule.max) {
return `exactly ${rule.min} axial ${block}`;
} else if (rule.max === 6) {
} else if (rule.max === 3) {
return `at least ${rule.min} axial ${block}`;
} else if (rule.min === 0) {
return `no more than ${rule.max} axial ${block}`;

7
scripts/solver.js Normal file
View File

@ -0,0 +1,7 @@
import { configuration } from "./configuration.js";
import { emitEvent } from "./events.js";
export function resetSolver(design = undefined) {
console.log("solver", configuration, design);
emitEvent("solver:reset:done");
}

View File

@ -85,6 +85,14 @@ main {
border-top: 1px solid var(--menu-border-color)
}
.status-bar {
border-bottom: 1px solid var(--status-bar-color, #4444);
}
.status-bar > :nth-child(3) {
display: flex;
justify-content: flex-end;
}
#left-menu, #right-menu, #bottom-menu {
position: relative;
display: flex;
@ -173,11 +181,13 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
width: calc(var(--scale-factor) * var(--size-x, var(--default-size)));
height: calc(var(--scale-factor) * var(--size-y, var(--default-size)));
outline: calc(var(--scale-factor) * 0.5px) solid var(--cell-empty-outline-color, #ccc);
outline-offset: calc(var(--scale-factor) * -0.5px);
opacity: var(--atlas-opacity, 1);
}
.atlas-air:hover, .atlas-texture:hover {
outline: calc(var(--scale-factor) * 1px) solid var(--cell-hover-outline-color, #8888);
outline-offset: calc(var(--scale-factor) * -1px);
}
[data-preview], [data-preview-remove] {
--atlas-opacity: var(--atlas-preview-opacity, 0.5);
@ -222,16 +232,24 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
[data-casing="hide"] [data-casing] {
display: none;
}
[data-selected] {
outline: calc(var(--scale-factor) * 1px) dashed var(--cell-selected-outline, #f80);
outline-offset: calc(var(--scale-factor) * -1px);
}
[data-selected] .atlas-air {
outline: none;
}
#block-list [data-active] {
#block-picker-list [data-active] {
outline: calc(var(--scale-factor) * 1px) solid var(--block-active-outline-color, #000);
}
#block-list [data-index]:hover:not([data-active]) {
#block-picker-list [data-index]:hover:not([data-active]) {
outline: calc(var(--scale-factor) * 1px) solid var(--block-hover-outline-color, #6666);
}
[data-tooltip] {
position: relative;
user-select: none;
}
[data-tooltip] > .tooltip {
position: fixed;
@ -287,14 +305,20 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
.menu-bar .alt-key {
text-decoration: underline;
}
.menu-bar [data-section]:has(> [data-toggle]) > button:not([data-toggle]) {
padding-left: 1em;
.menu-bar [data-section]:has(> [data-toggle]) > button {
display: grid;
grid-template-columns: 1.25em 1fr;
}
.menu-bar [data-section]:has(> [data-toggle]) > button::before {
content: '';
inline-size: 1.25em;
text-align: center;
}
.menu-bar button[data-toggle][data-toggled="false"]::before {
content: '[ ] ';
content: '';
}
.menu-bar button[data-toggle][data-toggled="true"]::before {
content: '[x] ';
content: '';
}
.dialog.hidden {