New visual style and multifloor support

This commit is contained in:
2026-01-08 05:57:56 +01:00
parent 61fa58fcc3
commit 1a1e36eabd
15 changed files with 998 additions and 49 deletions

View File

@@ -1,4 +1,4 @@
![GitHub Downloads (specific asset, all releases)](https://img.shields.io/github/downloads/Myxelium/FoundryVTT-Quick-Import/quick-battlemap-importer.zip) ![GitHub Downloads (specific asset, all releases)](https://img.shields.io/github/downloads/Myxelium/FoundryVTT-Quick-Import/myxeliums-battlemap-importer.zip)
## Myxelium's Battlemap Importer ## Myxelium's Battlemap Importer
@@ -7,7 +7,7 @@ Effortlessly turn single images or exported map data into readytoplay Foun
### What it is ### What it is
Quick Battlemap Importer is a Foundry VTT module that adds a simple "Quick import" button to the Scenes sidebar. It opens a window where you can drag and drop a background image or video and, if you have it, a JSON configuration file. The module uploads the media, applies grid settings, creates walls, lights and doors etc, and builds a new scene for you automatically. Myxeliums Battlemap Importer is a Foundry VTT module that adds a simple "Quick import" button to the Scenes sidebar. It opens a window where you can drag and drop a background image or video and, if you have it, a JSON configuration file. The module uploads the media, applies grid settings, creates walls, lights and doors etc, and builds a new scene for you automatically.
### Why it exists ### Why it exists
@@ -15,8 +15,8 @@ Setting up scenes manually can be slow: uploading backgrounds, measuring grid si
### Inside foundry ### Inside foundry
<img width="629" height="497" alt="image" src="https://github.com/user-attachments/assets/a848543f-7a96-439a-8897-4971cf8a4cb5" /> <img width="629" height="497" alt="image" src="./images/importer.png" />
<img width="538" height="422" alt="image" src="https://github.com/user-attachments/assets/d7672c2e-d241-4ced-8f6f-b5479e522287" /> <img width="538" height="422" alt="image" src="./images/image1.png" />
@@ -38,7 +38,7 @@ Setting up scenes manually can be slow: uploading backgrounds, measuring grid si
## Compatibility ## Compatibility
- Foundry VTT compatibility: minimum 10, verified 12 - Foundry VTT compatibility: minimum 12, verified 13
## Installation ## Installation
@@ -50,7 +50,7 @@ Setting up scenes manually can be slow: uploading backgrounds, measuring grid si
Manual download (optional): Manual download (optional):
- Download ZIP: https://github.com/Myxelium/FoundryVTT-Quick-Import/releases/latest/download/quick-battlemap-importer.zip - Download ZIP: https://github.com/Myxelium/FoundryVTT-Quick-Import/releases/latest/download/myxeliums-battlemap-importer.zip
## Usage ## Usage
@@ -79,7 +79,3 @@ The module uploads the media to your world, applies grid settings (auto-detected
## Credits ## Credits
- Author: Myxelium (https://github.com/Myxelium) - Author: Myxelium (https://github.com/Myxelium)
## License
MIT

BIN
images/image1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
images/importer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

View File

@@ -1,8 +1,8 @@
{ {
"QUICKBATTLEMAP": { "QUICKBATTLEMAP": {
"Ready": "Quick Battlemap module is ready", "Ready": "Myxeliums Battlemap Importer is ready",
"DropAreaTitle": "Quick Battlemap Importer", "DropAreaTitle": "Myxeliums Battlemap Importer",
"DropInstructions": "Drop background images/videos and optionally JSON files with walls", "DropInstructions": "Drop background images/videos and optionally JSON files with wall/light data here to create battlemap scenes.",
"DropInstructionsMore": "Multiple files can be dropped to create multi-floor scenes (requires Levels module). Files with matching names will be paired automatically.", "DropInstructionsMore": "Multiple files can be dropped to create multi-floor scenes (requires Levels module). Files with matching names will be paired automatically.",
"BackgroundStatus": "Background Media", "BackgroundStatus": "Background Media",
"WallDataStatus": "Wall Data", "WallDataStatus": "Wall Data",
@@ -15,7 +15,7 @@
"SceneCreationFailed": "Failed to create scene", "SceneCreationFailed": "Failed to create scene",
"InvalidJSON": "The JSON file could not be parsed", "InvalidJSON": "The JSON file could not be parsed",
"DefaultSceneName": "New Battlemap", "DefaultSceneName": "New Battlemap",
"ControlTitle": "Quick Battlemap Importer", "ControlTitle": "Myxeliums Battlemap Importer",
"Options": "Options", "Options": "Options",
"NoGridLabel": "Create scene with no grid", "NoGridLabel": "Create scene with no grid",
"NoGridHint": "Skips applying or detecting a grid. Useful for maps without visible grid lines.", "NoGridHint": "Skips applying or detecting a grid. Useful for maps without visible grid lines.",

View File

@@ -1,8 +1,8 @@
{ {
"id": "quick-battlemap-importer", "id": "quick-battlemap-importer",
"title": "Quick Battlemap Importer", "title": "Myxeliums Battlemap Importer",
"description": "Import battlemaps by simply dragging in a background image and wall/light data JSON file", "description": "Import battlemaps by simply dragging in a background image and wall/light data JSON file",
"version": "1.5.4", "version": "1.5.10",
"compatibility": { "compatibility": {
"minimum": "10", "minimum": "10",
"verified": "13" "verified": "13"
@@ -13,8 +13,8 @@
"url": "https://github.com/Myxelium" "url": "https://github.com/Myxelium"
} }
], ],
"esmodules": ["scripts/quick-battlemap.js"], "esmodules": ["scripts/myxeliums-battlemap.js"],
"styles": ["styles/quick-battlemap.css"], "styles": ["styles/myxeliums-battlemap.css"],
"languages": [ "languages": [
{ {
"lang": "en", "lang": "en",
@@ -23,6 +23,6 @@
} }
], ],
"url": "https://github.com/Myxelium/FoundryVTT-Quick-Import", "url": "https://github.com/Myxelium/FoundryVTT-Quick-Import",
"manifest": "https://github.com/Myxelium/FoundryVTT-Quick-Import-Dev/releases/download/1.5.4/module.json", "manifest": "https://github.com/Myxelium/FoundryVTT-Quick-Import/releases/latest/download/module.json",
"download": "https://github.com/Myxelium/FoundryVTT-Quick-Import-Dev/releases/download/1.5.4/quick-battlemap-importer.zip" "download": "https://github.com/Myxelium/FoundryVTT-Quick-Import/releases/latest/download/myxeliums-battlemap-importer.zip"
} }

View File

@@ -1,12 +1,12 @@
#!/bin/bash #!/bin/bash
# Release script for Quick Battlemap Importer # Release script for Myxeliums Battlemap Importer
# Increments version in module.json and creates a release zip # Increments version in module.json and creates a release zip
set -e set -e
MODULE_FILE="module.json" MODULE_FILE="module.json"
ZIP_NAME="quick-battlemap-importer.zip" ZIP_NAME="myxeliums-battlemap-importer.zip"
# Check if module.json exists # Check if module.json exists
if [ ! -f "$MODULE_FILE" ]; then if [ ! -f "$MODULE_FILE" ]; then

View File

@@ -8,7 +8,7 @@
*/ */
/** Module identifier for console logging */ /** Module identifier for console logging */
const MODULE_LOG_PREFIX = 'Quick Battlemap Importer'; const MODULE_LOG_PREFIX = 'Myxeliums Battlemap Importer';
/** /**
* @typedef {Object} ProcessedImageData * @typedef {Object} ProcessedImageData

View File

@@ -19,7 +19,7 @@
/** CSS selectors for frequently accessed elements */ /** CSS selectors for frequently accessed elements */
const PANEL_SELECTORS = { const PANEL_SELECTORS = {
PANEL_ROOT: '#quick-battlemap-drop-area', PANEL_ROOT: '#myxeliums-battlemap-drop-area',
CREATE_BUTTON: '.create-scene-button', CREATE_BUTTON: '.create-scene-button',
RESET_BUTTON: '.reset-button', RESET_BUTTON: '.reset-button',
CLOSE_BUTTON: '.header-button.close', CLOSE_BUTTON: '.header-button.close',
@@ -35,7 +35,7 @@ const PANEL_SELECTORS = {
}; };
/** LocalStorage key for persisting no-grid preference */ /** LocalStorage key for persisting no-grid preference */
const NO_GRID_STORAGE_KEY = 'quick-battlemap:no-grid'; const NO_GRID_STORAGE_KEY = 'myxeliums-battlemap:no-grid';
/** /**
* View class that manages the import panel DOM and user interactions. * View class that manages the import panel DOM and user interactions.
@@ -83,7 +83,7 @@ export class ImportPanelView {
} }
// Remove any existing panel to prevent duplicates // Remove any existing panel to prevent duplicates
const existingPanel = document.getElementById('quick-battlemap-drop-area'); const existingPanel = document.getElementById('myxeliums-battlemap-drop-area');
if (existingPanel) { if (existingPanel) {
existingPanel.remove(); existingPanel.remove();
} }
@@ -138,7 +138,7 @@ export class ImportPanelView {
` : ''; ` : '';
return ` return `
<div id="quick-battlemap-drop-area" class="qbi-panel"> <div id="myxeliums-battlemap-drop-area" class="qbi-panel">
<header class="qbi-header"> <header class="qbi-header">
<div class="qbi-header-title"> <div class="qbi-header-title">
<i class="fas fa-map qbi-header-icon"></i> <i class="fas fa-map qbi-header-icon"></i>
@@ -378,7 +378,7 @@ export class ImportPanelView {
* @returns {HTMLElement|null} The panel element * @returns {HTMLElement|null} The panel element
*/ */
getPanelElement() { getPanelElement() {
return document.getElementById('quick-battlemap-drop-area'); return document.getElementById('myxeliums-battlemap-drop-area');
} }
/** /**

View File

@@ -554,7 +554,7 @@ export class SceneBuilder {
/** /**
* Create floor tiles for multi-floor scenes using Levels module format. * Create floor tiles for multi-floor scenes using Levels module format.
* Each additional floor is created as an overhead tile with proper elevation. * Each additional floor is created as a tile with proper elevation.
* *
* @param {Scene} scene - The scene to add floor tiles to * @param {Scene} scene - The scene to add floor tiles to
* @param {Array} additionalFloors - Array of floor data (excluding base floor) * @param {Array} additionalFloors - Array of floor data (excluding base floor)
@@ -582,6 +582,7 @@ export class SceneBuilder {
const rangeTop = elevation + floorHeight * 2 - 1; const rangeTop = elevation + floorHeight * 2 - 1;
// Create tile for this floor with Levels flags // Create tile for this floor with Levels flags
// Using overhead: false and proper Levels flags to avoid fade/zoom behavior
const tileData = { const tileData = {
texture: { texture: {
src: floor.uploadedPath src: floor.uploadedPath
@@ -590,14 +591,17 @@ export class SceneBuilder {
y: 0, y: 0,
width: baseDimensions.width || scene.width, width: baseDimensions.width || scene.width,
height: baseDimensions.height || scene.height, height: baseDimensions.height || scene.height,
overhead: true, overhead: false, // Not overhead - this is a floor tile
roof: false, roof: false,
occlusion: { mode: 1 }, // Levels uses occlusion mode 1 occlusion: { mode: 0 }, // No occlusion - controlled by Levels
elevation: elevation, elevation: elevation,
sort: 1000 + (i * 100), sort: 100 + (i * 10), // Lower sort order for floor tiles
flags: { flags: {
levels: { levels: {
rangeTop: rangeTop rangeTop: rangeTop,
showIfAbove: false, // Don't show when above this floor
noCollision: true, // No 3D collision for floor tiles
noFogHide: true // Don't hide in fog
} }
} }
}; };

View File

@@ -183,12 +183,61 @@ export class SceneDataNormalizer {
/** /**
* Normalize an array of wall data to Foundry's Wall document format. * Normalize an array of wall data to Foundry's Wall document format.
* Filters out regular walls that share exact coordinates with doors to prevent
* duplicate walls blocking door functionality.
* *
* @param {Array} wallsArray - Array of raw wall data objects * @param {Array} wallsArray - Array of raw wall data objects
* @returns {NormalizedWallData[]} Array of normalized wall documents * @returns {NormalizedWallData[]} Array of normalized wall documents
*/ */
normalizeWallsData(wallsArray) { normalizeWallsData(wallsArray) {
return wallsArray.map(wall => this.normalizeWall(wall)); const normalizedWalls = wallsArray.map(wall => this.normalizeWall(wall));
return this.filterDuplicateWallsAtDoorLocations(normalizedWalls);
}
/**
* Filter out regular walls that have the same coordinates as doors.
* Some map exports include both a wall segment and a door at the same location,
* which causes the door to be blocked by the overlapping wall.
*
* @param {NormalizedWallData[]} walls - Array of normalized wall data
* @returns {NormalizedWallData[]} Filtered array with duplicate walls removed
*/
filterDuplicateWallsAtDoorLocations(walls) {
// Collect coordinates of all doors
const doorCoordinates = new Set();
for (const wall of walls) {
if (wall.door > 0 && Array.isArray(wall.c) && wall.c.length >= 4) {
// Store both forward and reverse coordinate strings to handle different orderings
const coordKey = wall.c.slice(0, 4).join(',');
const reverseKey = [wall.c[2], wall.c[3], wall.c[0], wall.c[1]].join(',');
doorCoordinates.add(coordKey);
doorCoordinates.add(reverseKey);
}
}
// If no doors, return walls as-is
if (doorCoordinates.size === 0) {
return walls;
}
// Filter out regular walls (door === 0) that share coordinates with doors
return walls.filter(wall => {
// Keep all doors
if (wall.door > 0) {
return true;
}
// Check if this regular wall overlaps with a door location
if (Array.isArray(wall.c) && wall.c.length >= 4) {
const coordKey = wall.c.slice(0, 4).join(',');
if (doorCoordinates.has(coordKey)) {
return false; // Remove this wall - there's a door here
}
}
return true;
});
} }
/** /**
@@ -199,16 +248,22 @@ export class SceneDataNormalizer {
*/ */
normalizeWall(wall) { normalizeWall(wall) {
const restrictionTypes = this.getWallRestrictionTypes(); const restrictionTypes = this.getWallRestrictionTypes();
const doorType = this.ensureFiniteNumber(wall.door, 0);
// Doors should default to NORMAL restrictions (blocking) when closed,
// while regular walls default to NONE (the source data usually specifies restrictions)
const isDoor = doorType > 0;
const defaultRestriction = isDoor ? restrictionTypes.NORMAL : restrictionTypes.NONE;
return { return {
c: this.normalizeWallCoordinates(wall.c), c: this.normalizeWallCoordinates(wall.c),
door: this.ensureFiniteNumber(wall.door, 0), door: doorType,
ds: this.ensureFiniteNumber(wall.ds, 0), ds: this.ensureFiniteNumber(wall.ds, 0),
dir: this.ensureFiniteNumber(wall.dir, 0), dir: this.ensureFiniteNumber(wall.dir, 0),
move: this.parseRestrictionValue(wall.move, restrictionTypes.NONE, restrictionTypes), move: this.parseRestrictionValue(wall.move, defaultRestriction, restrictionTypes),
sound: this.parseRestrictionValue(wall.sound, restrictionTypes.NONE, restrictionTypes), sound: this.parseRestrictionValue(wall.sound, defaultRestriction, restrictionTypes),
sight: this.parseRestrictionValue(wall.sense ?? wall.sight, restrictionTypes.NONE, restrictionTypes), sight: this.parseRestrictionValue(wall.sense ?? wall.sight, defaultRestriction, restrictionTypes),
light: this.parseRestrictionValue(wall.light, restrictionTypes.NONE, restrictionTypes), light: this.parseRestrictionValue(wall.light, defaultRestriction, restrictionTypes),
flags: wall.flags ?? {} flags: wall.flags ?? {}
}; };
} }

View File

@@ -738,9 +738,10 @@ export class SceneImportController {
} }
// Set Levels module scene flags for floor definitions // Set Levels module scene flags for floor definitions
// Only set sceneLevels - let backgroundElevation default to 0
// This prevents the background from incorrectly hiding/showing
await createdScene.update({ await createdScene.update({
'flags.levels.sceneLevels': sceneLevels, 'flags.levels.sceneLevels': sceneLevels
'flags.levels.backgroundElevation': floorElevations[0]
}); });
this.cleanupAfterMultiFloorCreation(sceneName); this.cleanupAfterMultiFloorCreation(sceneName);

View File

@@ -0,0 +1,139 @@
/**
* Myxeliums Battlemap Importer - Main Entry Point
*
* This module provides a streamlined way to import battlemaps into Foundry VTT.
* It adds a "Quick import" button to the Scenes sidebar that opens a drag-and-drop
* panel for creating scenes from images/videos and optional JSON configuration files.
*
* @module MyxeliumsBattlemap
* @author Myxelium
* @license MIT
*/
import { SceneImportController } from './lib/scene-import-controller.js';
/** @type {SceneImportController|null} Singleton instance of the import controller */
let sceneImportController = null;
/**
* Module identifier used for logging and namespacing
* @constant {string}
*/
const MODULE_ID = 'Myxeliums Battlemap Importer';
/**
* CSS class name for the quick import button to prevent duplicate insertion
* @constant {string}
*/
const QUICK_IMPORT_BUTTON_CLASS = 'myxeliums-battlemap-quick-import';
/**
* Initialize the module when Foundry is ready.
* Sets up the import controller and registers necessary handlers.
*/
Hooks.once('init', async function () {
console.log(`${MODULE_ID} | Initializing module`);
});
/**
* Complete module setup after Foundry is fully loaded.
* Creates the controller instance and displays a ready notification.
*/
Hooks.once('ready', async function () {
console.log(`${MODULE_ID} | Module ready`);
sceneImportController = new SceneImportController();
sceneImportController.initialize();
ui.notifications.info(game.i18n.localize("QUICKBATTLEMAP.Ready"));
});
/**
* Add the "Quick import" button to the Scenes directory header.
* This hook fires whenever the SceneDirectory is rendered.
*
* @param {Application} _app - The SceneDirectory application instance (unused)
* @param {jQuery|HTMLElement} html - The rendered HTML element
*/
Hooks.on('renderSceneDirectory', (_app, html) => {
// Only GMs can use the quick import feature
if (!game.user?.isGM) {
return;
}
// Handle different HTML element formats across Foundry versions
const rootElement = html?.[0] || html?.element || html;
if (!(rootElement instanceof HTMLElement)) {
return;
}
// Prevent adding duplicate buttons
const existingButton = rootElement.querySelector(`button.${QUICK_IMPORT_BUTTON_CLASS}`);
if (existingButton) {
return;
}
// Find a suitable container for the button
const buttonContainer = findButtonContainer(rootElement);
if (!buttonContainer) {
return;
}
// Create and append the quick import button
const quickImportButton = createQuickImportButton();
buttonContainer.appendChild(quickImportButton);
});
/**
* Find a suitable container element for the quick import button.
* Tries multiple selectors for compatibility across Foundry versions.
*
* @param {HTMLElement} rootElement - The root element to search within
* @returns {HTMLElement|null} The container element or null if not found
*/
function findButtonContainer(rootElement) {
const containerSelectors = [
'.header-actions',
'.action-buttons',
'.directory-header'
];
for (const selector of containerSelectors) {
const container = rootElement.querySelector(selector);
if (container) {
return container;
}
}
return null;
}
/**
* Create the quick import button element with icon and click handler.
*
* @returns {HTMLButtonElement} The configured button element
*/
function createQuickImportButton() {
const button = document.createElement('button');
button.type = 'button';
button.className = QUICK_IMPORT_BUTTON_CLASS;
button.innerHTML = '<i class="fas fa-map"></i> <span>Quick import</span>';
button.addEventListener('click', handleQuickImportClick);
return button;
}
/**
* Handle click events on the quick import button.
* Creates the controller if needed and shows the import panel.
*/
function handleQuickImportClick() {
if (!sceneImportController) {
sceneImportController = new SceneImportController();
sceneImportController.initialize();
}
sceneImportController.showImportPanel();
}

View File

@@ -1,11 +1,11 @@
/** /**
* Quick Battlemap Importer - Main Entry Point * Myxeliums Battlemap Importer - Main Entry Point
* *
* This module provides a streamlined way to import battlemaps into Foundry VTT. * This module provides a streamlined way to import battlemaps into Foundry VTT.
* It adds a "Quick import" button to the Scenes sidebar that opens a drag-and-drop * It adds a "Quick import" button to the Scenes sidebar that opens a drag-and-drop
* panel for creating scenes from images/videos and optional JSON configuration files. * panel for creating scenes from images/videos and optional JSON configuration files.
* *
* @module QuickBattlemap * @module MyxeliumsBattlemap
* @author Myxelium * @author Myxelium
* @license MIT * @license MIT
*/ */
@@ -19,13 +19,13 @@ let sceneImportController = null;
* Module identifier used for logging and namespacing * Module identifier used for logging and namespacing
* @constant {string} * @constant {string}
*/ */
const MODULE_ID = 'Quick Battlemap'; const MODULE_ID = 'Myxeliums Battlemap Importer';
/** /**
* CSS class name for the quick import button to prevent duplicate insertion * CSS class name for the quick import button to prevent duplicate insertion
* @constant {string} * @constant {string}
*/ */
const QUICK_IMPORT_BUTTON_CLASS = 'quick-battlemap-quick-import'; const QUICK_IMPORT_BUTTON_CLASS = 'myxeliums-battlemap-quick-import';
/** /**
* Initialize the module when Foundry is ready. * Initialize the module when Foundry is ready.

View File

@@ -0,0 +1,749 @@
/* ==========================================================================
Myxeliums Battlemap Importer - Modern Panel Styles
========================================================================== */
/* Quick Import Button (Foundry sidebar) - Keep original styling */
button.myxeliums-battlemap-quick-import {
flex: 0 0 auto;
margin-left: auto;
margin-right: auto;
display: block;
}
/* CSS Custom Properties */
:root {
--qbi-primary: #7c3aed;
--qbi-primary-hover: #6d28d9;
--qbi-primary-light: rgba(124, 58, 237, 0.1);
--qbi-success: #10b981;
--qbi-success-bg: rgba(16, 185, 129, 0.1);
--qbi-error: #ef4444;
--qbi-error-bg: rgba(239, 68, 68, 0.1);
--qbi-warning: #f59e0b;
--qbi-bg-dark: #1a1a2e;
--qbi-bg-card: #16213e;
--qbi-bg-elevated: #1f2b4d;
--qbi-border: rgba(255, 255, 255, 0.08);
--qbi-border-light: rgba(255, 255, 255, 0.12);
--qbi-text: #e2e8f0;
--qbi-text-muted: #94a3b8;
--qbi-text-dim: #64748b;
--qbi-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
--qbi-shadow-sm: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
--qbi-radius: 12px;
--qbi-radius-sm: 8px;
--qbi-radius-xs: 6px;
--qbi-transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Panel Container */
.qbi-panel {
position: absolute;
left: 72px;
top: 80px;
z-index: 100;
display: none;
width: 420px;
background: var(--qbi-bg-dark);
border-radius: var(--qbi-radius);
box-shadow: var(--qbi-shadow);
border: 1px solid var(--qbi-border);
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
overflow: hidden;
}
/* Header */
.qbi-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: linear-gradient(
135deg,
var(--qbi-bg-elevated) 0%,
var(--qbi-bg-card) 100%
);
border-bottom: 1px solid var(--qbi-border);
cursor: move;
user-select: none;
}
.qbi-header-title {
display: flex;
align-items: center;
gap: 10px;
}
.qbi-header-icon {
font-size: 18px;
color: var(--qbi-primary);
}
.qbi-header h4 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--qbi-text);
letter-spacing: -0.01em;
}
.qbi-close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
background: rgba(255, 255, 255, 0.05);
border-radius: var(--qbi-radius-xs);
color: var(--qbi-text-muted);
cursor: pointer;
transition: all var(--qbi-transition);
}
.qbi-close-btn:hover {
background: rgba(239, 68, 68, 0.2);
color: var(--qbi-error);
}
/* Content */
.qbi-content {
padding: 20px;
}
.qbi-content form {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Instructions */
.qbi-instructions {
margin: 0;
font-size: 13px;
color: var(--qbi-text-muted);
line-height: 1.5;
}
.qbi-instructions-secondary {
font-size: 12px;
color: var(--qbi-text-dim);
padding-top: 4px;
border-top: 1px solid var(--qbi-border);
}
/* Drop Zone */
.qbi-dropzone {
position: relative;
border: 2px dashed var(--qbi-border-light);
border-radius: var(--qbi-radius-sm);
background: linear-gradient(
135deg,
rgba(124, 58, 237, 0.03) 0%,
rgba(16, 213, 238, 0.03) 100%
);
transition: all var(--qbi-transition);
}
.qbi-dropzone:hover {
border-color: var(--qbi-primary);
background: var(--qbi-primary-light);
}
.qbi-dropzone-inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 20px;
gap: 8px;
}
.qbi-dropzone-icon {
font-size: 32px;
color: var(--qbi-primary);
opacity: 0.8;
transition: all var(--qbi-transition);
}
.qbi-dropzone:hover .qbi-dropzone-icon {
opacity: 1;
transform: translateY(-2px);
}
.qbi-dropzone-text {
font-size: 15px;
font-weight: 500;
color: var(--qbi-text);
}
.qbi-dropzone-hint {
font-size: 12px;
color: var(--qbi-text-dim);
}
/* Highlight state when dragging */
.qbi-panel.highlight .qbi-dropzone {
border-color: var(--qbi-primary);
background: var(--qbi-primary-light);
box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.15);
}
.qbi-panel.highlight .qbi-dropzone-icon {
color: var(--qbi-primary);
opacity: 1;
animation: qbi-bounce 0.6s ease infinite;
}
@keyframes qbi-bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}
/* Status Grid */
.qbi-status-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.qbi-status-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: var(--qbi-bg-card);
border-radius: var(--qbi-radius-sm);
border: 1px solid var(--qbi-border);
transition: all var(--qbi-transition);
}
.qbi-status-indicator {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--qbi-error-bg);
transition: all var(--qbi-transition);
}
.qbi-status-icon {
font-size: 16px;
line-height: 1;
}
.qbi-status-item:has(.status-value[data-status="complete"])
.qbi-status-indicator {
background: var(--qbi-success-bg);
}
.qbi-status-label {
font-size: 12px;
font-weight: 500;
color: var(--qbi-text-muted);
line-height: 1.3;
}
/* Options / Checkbox */
.qbi-options {
padding: 4px 0;
}
.qbi-checkbox {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
.qbi-checkbox-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.qbi-checkbox-mark {
position: relative;
width: 20px;
height: 20px;
background: var(--qbi-bg-card);
border: 2px solid var(--qbi-border-light);
border-radius: var(--qbi-radius-xs);
transition: all var(--qbi-transition);
}
.qbi-checkbox-mark::after {
content: "";
position: absolute;
top: 2px;
left: 6px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg) scale(0);
transition: transform var(--qbi-transition);
}
.qbi-checkbox-input:checked + .qbi-checkbox-mark {
background: var(--qbi-primary);
border-color: var(--qbi-primary);
}
.qbi-checkbox-input:checked + .qbi-checkbox-mark::after {
transform: rotate(45deg) scale(1);
}
.qbi-checkbox-input:focus + .qbi-checkbox-mark {
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.3);
}
.qbi-checkbox-label {
font-size: 13px;
color: var(--qbi-text-muted);
}
/* Progress Section */
.qbi-progress {
display: none;
padding: 14px 16px;
background: linear-gradient(
135deg,
rgba(124, 58, 237, 0.08) 0%,
rgba(124, 58, 237, 0.04) 100%
);
border-radius: var(--qbi-radius-sm);
border: 1px solid rgba(124, 58, 237, 0.2);
}
.qbi-progress-content {
display: flex;
align-items: flex-start;
gap: 12px;
}
.qbi-spinner {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: var(--qbi-primary);
font-size: 18px;
}
.qbi-progress-info {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.qbi-progress-text {
font-size: 13px;
font-weight: 500;
color: var(--qbi-text);
}
.qbi-progress-note {
font-size: 11px;
color: var(--qbi-text-dim);
margin: 0;
}
/* Footer / Buttons */
.qbi-footer {
display: flex;
gap: 10px;
padding: 12px 0px;
border-top: 1px solid var(--qbi-border);
margin-top: 4px;
}
.qbi-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 18px;
font-size: 13px;
font-weight: 500;
border-radius: var(--qbi-radius-sm);
border: none;
cursor: pointer;
transition: all var(--qbi-transition);
line-height: 1;
height: 50px;
}
.qbi-btn i {
font-size: 12px;
}
.qbi-btn-secondary {
flex: 0 0 25%;
background: var(--qbi-bg-elevated);
color: var(--qbi-text-muted);
border: 1px solid var(--qbi-border-light);
}
.qbi-btn-secondary:hover {
background: var(--qbi-bg-card);
color: var(--qbi-text);
border-color: var(--qbi-border-light);
}
.qbi-btn-primary {
flex: 1 1 75%;
background: linear-gradient(
135deg,
var(--qbi-primary) 0%,
var(--qbi-primary-hover) 100%
);
color: white;
box-shadow: 0 4px 14px rgba(124, 58, 237, 0.35);
}
.qbi-btn-primary:hover:not(:disabled) {
background: linear-gradient(
135deg,
var(--qbi-primary-hover) 0%,
#5b21b6 100%
);
box-shadow: 0 6px 20px rgba(124, 58, 237, 0.45);
transform: translateY(-1px);
}
.qbi-btn-primary:disabled {
background: var(--qbi-bg-elevated);
color: var(--qbi-text-dim);
box-shadow: none;
cursor: not-allowed;
opacity: 0.6;
}
/* Status update animations */
.qbi-status-icon {
transition: transform 0.3s ease;
}
.qbi-status-item:has(.status-value:not([data-status="pending"])) {
border-color: rgba(16, 185, 129, 0.3);
}
/* ==========================================================================
Floor Management Section
========================================================================== */
.qbi-floor-section {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 4px;
}
.qbi-floor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2px;
}
.qbi-floor-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 500;
color: var(--qbi-text);
}
.qbi-floor-title i {
color: var(--qbi-primary);
font-size: 14px;
}
.qbi-floor-count {
font-size: 11px;
color: var(--qbi-text-dim);
background: var(--qbi-bg-card);
padding: 3px 8px;
border-radius: var(--qbi-radius-xs);
}
.qbi-floor-list {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 200px;
overflow-y: auto;
padding: 2px;
}
.qbi-floor-list::-webkit-scrollbar {
width: 6px;
}
.qbi-floor-list::-webkit-scrollbar-track {
background: var(--qbi-bg-card);
border-radius: 3px;
}
.qbi-floor-list::-webkit-scrollbar-thumb {
background: var(--qbi-border-light);
border-radius: 3px;
}
.qbi-floor-empty {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 20px;
color: var(--qbi-text-dim);
font-size: 12px;
background: var(--qbi-bg-card);
border-radius: var(--qbi-radius-sm);
border: 1px dashed var(--qbi-border);
}
.qbi-floor-empty i {
font-size: 14px;
opacity: 0.7;
}
/* Floor Item */
.qbi-floor-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--qbi-bg-card);
border-radius: var(--qbi-radius-sm);
border: 1px solid var(--qbi-border);
transition: all var(--qbi-transition);
cursor: grab;
}
.qbi-floor-item:hover {
border-color: var(--qbi-border-light);
background: var(--qbi-bg-elevated);
}
.qbi-floor-item.qbi-floor-dragging {
opacity: 0.5;
cursor: grabbing;
}
.qbi-floor-item.qbi-floor-drag-over {
border-color: var(--qbi-primary);
background: var(--qbi-primary-light);
}
.qbi-floor-drag-handle {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
color: var(--qbi-text-dim);
cursor: grab;
}
.qbi-floor-drag-handle:active {
cursor: grabbing;
}
.qbi-floor-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.qbi-floor-name {
display: flex;
align-items: center;
gap: 8px;
}
.qbi-floor-level {
font-size: 11px;
font-weight: 600;
color: var(--qbi-primary);
background: var(--qbi-primary-light);
padding: 2px 6px;
border-radius: 4px;
white-space: nowrap;
}
.qbi-floor-filename {
font-size: 12px;
color: var(--qbi-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.qbi-floor-json {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
}
.qbi-floor-json.has-json {
color: var(--qbi-success);
}
.qbi-floor-json.no-json {
color: var(--qbi-text-dim);
}
.qbi-floor-json i {
font-size: 10px;
}
.qbi-floor-actions {
display: flex;
gap: 4px;
}
.qbi-floor-btn {
display: flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: none;
background: var(--qbi-bg-elevated);
border-radius: var(--qbi-radius-xs);
color: var(--qbi-text-muted);
cursor: pointer;
transition: all var(--qbi-transition);
font-size: 11px;
}
.qbi-floor-btn:hover:not(:disabled) {
background: var(--qbi-border-light);
color: var(--qbi-text);
}
.qbi-floor-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.qbi-floor-btn.qbi-floor-remove:hover {
background: var(--qbi-error-bg);
color: var(--qbi-error);
}
/* Unmatched Files Section */
.qbi-unmatched-files {
margin-top: 8px;
padding: 12px;
background: rgba(245, 158, 11, 0.08);
border-radius: var(--qbi-radius-sm);
border: 1px solid rgba(245, 158, 11, 0.2);
}
.qbi-unmatched-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
color: var(--qbi-warning);
font-size: 12px;
font-weight: 500;
}
.qbi-unmatched-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.qbi-unmatched-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.qbi-unmatched-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--qbi-text-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.qbi-unmatched-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
background: var(--qbi-bg-card);
border-radius: var(--qbi-radius-xs);
}
.qbi-unmatched-name {
font-size: 12px;
color: var(--qbi-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
margin-right: 8px;
}
.qbi-unmatched-assign {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
background: var(--qbi-primary-light);
border-radius: var(--qbi-radius-xs);
color: var(--qbi-primary);
cursor: pointer;
transition: all var(--qbi-transition);
}
.qbi-unmatched-assign:hover {
background: var(--qbi-primary);
color: white;
}
/* File Match Dialog Styles */
.qbi-match-dialog-content {
padding: 10px 0;
}
.qbi-match-dialog-content p {
margin-bottom: 12px;
color: var(--qbi-text);
}
.qbi-floor-select {
width: 100%;
padding: 8px 12px;
background: var(--qbi-bg-card);
border: 1px solid var(--qbi-border-light);
border-radius: var(--qbi-radius-xs);
color: var(--qbi-text);
font-size: 13px;
}

View File

@@ -1,10 +1,13 @@
/* ========================================================================== /* ==========================================================================
Quick Battlemap Importer - Modern Panel Styles Myxeliums Battlemap Importer - Modern Panel Styles
========================================================================== */ ========================================================================== */
/* Quick Import Button (Foundry sidebar) - Keep original styling */ /* Quick Import Button (Foundry sidebar) - Keep original styling */
button.quick-battlemap-quick-import { button.myxeliums-battlemap-quick-import {
flex: 0 0 auto; flex: 0 0 auto;
margin-left: auto;
margin-right: auto;
display: block;
} }
/* CSS Custom Properties */ /* CSS Custom Properties */
@@ -362,7 +365,7 @@ button.quick-battlemap-quick-import {
.qbi-footer { .qbi-footer {
display: flex; display: flex;
gap: 10px; gap: 10px;
padding-top: 8px; padding: 12px 0px;
border-top: 1px solid var(--qbi-border); border-top: 1px solid var(--qbi-border);
margin-top: 4px; margin-top: 4px;
} }
@@ -380,6 +383,7 @@ button.quick-battlemap-quick-import {
cursor: pointer; cursor: pointer;
transition: all var(--qbi-transition); transition: all var(--qbi-transition);
line-height: 1; line-height: 1;
height: 50px;
} }
.qbi-btn i { .qbi-btn i {
@@ -387,6 +391,7 @@ button.quick-battlemap-quick-import {
} }
.qbi-btn-secondary { .qbi-btn-secondary {
flex: 0 0 25%;
background: var(--qbi-bg-elevated); background: var(--qbi-bg-elevated);
color: var(--qbi-text-muted); color: var(--qbi-text-muted);
border: 1px solid var(--qbi-border-light); border: 1px solid var(--qbi-border-light);
@@ -399,7 +404,7 @@ button.quick-battlemap-quick-import {
} }
.qbi-btn-primary { .qbi-btn-primary {
flex: 1; flex: 1 1 75%;
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
var(--qbi-primary) 0%, var(--qbi-primary) 0%,