diff --git a/README.md b/README.md index a4024cf..b2430da 100644 --- a/README.md +++ b/README.md @@ -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 @@ -7,7 +7,7 @@ Effortlessly turn single images or exported map data into ready‑to‑play Foun ### 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 @@ -15,8 +15,8 @@ Setting up scenes manually can be slow: uploading backgrounds, measuring grid si ### Inside foundry -image -image +image +image @@ -38,7 +38,7 @@ Setting up scenes manually can be slow: uploading backgrounds, measuring grid si ## Compatibility -- Foundry VTT compatibility: minimum 10, verified 12 +- Foundry VTT compatibility: minimum 12, verified 13 ## Installation @@ -50,7 +50,7 @@ Setting up scenes manually can be slow: uploading backgrounds, measuring grid si 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 @@ -79,7 +79,3 @@ The module uploads the media to your world, applies grid settings (auto-detected ## Credits - Author: Myxelium (https://github.com/Myxelium) - -## License - -MIT diff --git a/images/image1.png b/images/image1.png new file mode 100644 index 0000000..50d3fe4 Binary files /dev/null and b/images/image1.png differ diff --git a/images/importer.png b/images/importer.png new file mode 100644 index 0000000..94545e9 Binary files /dev/null and b/images/importer.png differ diff --git a/languages/en.json b/languages/en.json index 8139838..0570ebf 100644 --- a/languages/en.json +++ b/languages/en.json @@ -1,8 +1,8 @@ { "QUICKBATTLEMAP": { - "Ready": "Quick Battlemap module is ready", - "DropAreaTitle": "Quick Battlemap Importer", - "DropInstructions": "Drop background images/videos and optionally JSON files with walls", + "Ready": "Myxeliums Battlemap Importer is ready", + "DropAreaTitle": "Myxeliums Battlemap Importer", + "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.", "BackgroundStatus": "Background Media", "WallDataStatus": "Wall Data", @@ -15,7 +15,7 @@ "SceneCreationFailed": "Failed to create scene", "InvalidJSON": "The JSON file could not be parsed", "DefaultSceneName": "New Battlemap", - "ControlTitle": "Quick Battlemap Importer", + "ControlTitle": "Myxeliums Battlemap Importer", "Options": "Options", "NoGridLabel": "Create scene with no grid", "NoGridHint": "Skips applying or detecting a grid. Useful for maps without visible grid lines.", diff --git a/module.json b/module.json index 4fdc2a9..2e6c05c 100644 --- a/module.json +++ b/module.json @@ -1,8 +1,8 @@ { "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", - "version": "1.5.4", + "version": "1.5.10", "compatibility": { "minimum": "10", "verified": "13" @@ -13,8 +13,8 @@ "url": "https://github.com/Myxelium" } ], - "esmodules": ["scripts/quick-battlemap.js"], - "styles": ["styles/quick-battlemap.css"], + "esmodules": ["scripts/myxeliums-battlemap.js"], + "styles": ["styles/myxeliums-battlemap.css"], "languages": [ { "lang": "en", @@ -23,6 +23,6 @@ } ], "url": "https://github.com/Myxelium/FoundryVTT-Quick-Import", - "manifest": "https://github.com/Myxelium/FoundryVTT-Quick-Import-Dev/releases/download/1.5.4/module.json", - "download": "https://github.com/Myxelium/FoundryVTT-Quick-Import-Dev/releases/download/1.5.4/quick-battlemap-importer.zip" + "manifest": "https://github.com/Myxelium/FoundryVTT-Quick-Import/releases/latest/download/module.json", + "download": "https://github.com/Myxelium/FoundryVTT-Quick-Import/releases/latest/download/myxeliums-battlemap-importer.zip" } diff --git a/release.sh b/release.sh index 3d64b0f..f7e1f59 100755 --- a/release.sh +++ b/release.sh @@ -1,12 +1,12 @@ #!/bin/bash -# Release script for Quick Battlemap Importer +# Release script for Myxeliums Battlemap Importer # Increments version in module.json and creates a release zip set -e MODULE_FILE="module.json" -ZIP_NAME="quick-battlemap-importer.zip" +ZIP_NAME="myxeliums-battlemap-importer.zip" # Check if module.json exists if [ ! -f "$MODULE_FILE" ]; then diff --git a/scripts/lib/file-processor.js b/scripts/lib/file-processor.js index a4e6051..c27675a 100644 --- a/scripts/lib/file-processor.js +++ b/scripts/lib/file-processor.js @@ -8,7 +8,7 @@ */ /** Module identifier for console logging */ -const MODULE_LOG_PREFIX = 'Quick Battlemap Importer'; +const MODULE_LOG_PREFIX = 'Myxeliums Battlemap Importer'; /** * @typedef {Object} ProcessedImageData diff --git a/scripts/lib/import-panel-view.js b/scripts/lib/import-panel-view.js index 60c27a6..9bb7dac 100644 --- a/scripts/lib/import-panel-view.js +++ b/scripts/lib/import-panel-view.js @@ -19,7 +19,7 @@ /** CSS selectors for frequently accessed elements */ const PANEL_SELECTORS = { - PANEL_ROOT: '#quick-battlemap-drop-area', + PANEL_ROOT: '#myxeliums-battlemap-drop-area', CREATE_BUTTON: '.create-scene-button', RESET_BUTTON: '.reset-button', CLOSE_BUTTON: '.header-button.close', @@ -35,7 +35,7 @@ const PANEL_SELECTORS = { }; /** 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. @@ -83,7 +83,7 @@ export class ImportPanelView { } // 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) { existingPanel.remove(); } @@ -138,7 +138,7 @@ export class ImportPanelView { ` : ''; return ` -
+
@@ -378,7 +378,7 @@ export class ImportPanelView { * @returns {HTMLElement|null} The panel element */ getPanelElement() { - return document.getElementById('quick-battlemap-drop-area'); + return document.getElementById('myxeliums-battlemap-drop-area'); } /** diff --git a/scripts/lib/scene-builder.js b/scripts/lib/scene-builder.js index eed4fe5..20c4725 100644 --- a/scripts/lib/scene-builder.js +++ b/scripts/lib/scene-builder.js @@ -554,7 +554,7 @@ export class SceneBuilder { /** * 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 {Array} additionalFloors - Array of floor data (excluding base floor) @@ -582,6 +582,7 @@ export class SceneBuilder { const rangeTop = elevation + floorHeight * 2 - 1; // Create tile for this floor with Levels flags + // Using overhead: false and proper Levels flags to avoid fade/zoom behavior const tileData = { texture: { src: floor.uploadedPath @@ -590,14 +591,17 @@ export class SceneBuilder { y: 0, width: baseDimensions.width || scene.width, height: baseDimensions.height || scene.height, - overhead: true, + overhead: false, // Not overhead - this is a floor tile roof: false, - occlusion: { mode: 1 }, // Levels uses occlusion mode 1 + occlusion: { mode: 0 }, // No occlusion - controlled by Levels elevation: elevation, - sort: 1000 + (i * 100), + sort: 100 + (i * 10), // Lower sort order for floor tiles flags: { 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 } } }; diff --git a/scripts/lib/scene-data-normalizer.js b/scripts/lib/scene-data-normalizer.js index bf65031..7eabb97 100644 --- a/scripts/lib/scene-data-normalizer.js +++ b/scripts/lib/scene-data-normalizer.js @@ -183,12 +183,61 @@ export class SceneDataNormalizer { /** * 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 * @returns {NormalizedWallData[]} Array of normalized wall documents */ 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) { 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 { c: this.normalizeWallCoordinates(wall.c), - door: this.ensureFiniteNumber(wall.door, 0), + door: doorType, ds: this.ensureFiniteNumber(wall.ds, 0), dir: this.ensureFiniteNumber(wall.dir, 0), - move: this.parseRestrictionValue(wall.move, restrictionTypes.NONE, restrictionTypes), - sound: this.parseRestrictionValue(wall.sound, restrictionTypes.NONE, restrictionTypes), - sight: this.parseRestrictionValue(wall.sense ?? wall.sight, restrictionTypes.NONE, restrictionTypes), - light: this.parseRestrictionValue(wall.light, restrictionTypes.NONE, restrictionTypes), + move: this.parseRestrictionValue(wall.move, defaultRestriction, restrictionTypes), + sound: this.parseRestrictionValue(wall.sound, defaultRestriction, restrictionTypes), + sight: this.parseRestrictionValue(wall.sense ?? wall.sight, defaultRestriction, restrictionTypes), + light: this.parseRestrictionValue(wall.light, defaultRestriction, restrictionTypes), flags: wall.flags ?? {} }; } diff --git a/scripts/lib/scene-import-controller.js b/scripts/lib/scene-import-controller.js index dbbb724..895ad48 100644 --- a/scripts/lib/scene-import-controller.js +++ b/scripts/lib/scene-import-controller.js @@ -738,9 +738,10 @@ export class SceneImportController { } // 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({ - 'flags.levels.sceneLevels': sceneLevels, - 'flags.levels.backgroundElevation': floorElevations[0] + 'flags.levels.sceneLevels': sceneLevels }); this.cleanupAfterMultiFloorCreation(sceneName); diff --git a/scripts/myxeliums-battlemap.js b/scripts/myxeliums-battlemap.js new file mode 100644 index 0000000..3c0ac66 --- /dev/null +++ b/scripts/myxeliums-battlemap.js @@ -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 = ' Quick import'; + + 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(); +} diff --git a/scripts/quick-battlemap.js b/scripts/quick-battlemap.js index 25b9a5e..3c0ac66 100644 --- a/scripts/quick-battlemap.js +++ b/scripts/quick-battlemap.js @@ -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. * 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 QuickBattlemap + * @module MyxeliumsBattlemap * @author Myxelium * @license MIT */ @@ -19,13 +19,13 @@ let sceneImportController = null; * Module identifier used for logging and namespacing * @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 * @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. diff --git a/styles/myxeliums-battlemap.css b/styles/myxeliums-battlemap.css new file mode 100644 index 0000000..0a4747a --- /dev/null +++ b/styles/myxeliums-battlemap.css @@ -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; +} diff --git a/styles/quick-battlemap.css b/styles/quick-battlemap.css index b87c8d8..0a4747a 100644 --- a/styles/quick-battlemap.css +++ b/styles/quick-battlemap.css @@ -1,10 +1,13 @@ /* ========================================================================== - Quick Battlemap Importer - Modern Panel Styles + Myxeliums Battlemap Importer - Modern Panel Styles ========================================================================== */ /* Quick Import Button (Foundry sidebar) - Keep original styling */ -button.quick-battlemap-quick-import { +button.myxeliums-battlemap-quick-import { flex: 0 0 auto; + margin-left: auto; + margin-right: auto; + display: block; } /* CSS Custom Properties */ @@ -362,7 +365,7 @@ button.quick-battlemap-quick-import { .qbi-footer { display: flex; gap: 10px; - padding-top: 8px; + padding: 12px 0px; border-top: 1px solid var(--qbi-border); margin-top: 4px; } @@ -380,6 +383,7 @@ button.quick-battlemap-quick-import { cursor: pointer; transition: all var(--qbi-transition); line-height: 1; + height: 50px; } .qbi-btn i { @@ -387,6 +391,7 @@ button.quick-battlemap-quick-import { } .qbi-btn-secondary { + flex: 0 0 25%; background: var(--qbi-bg-elevated); color: var(--qbi-text-muted); border: 1px solid var(--qbi-border-light); @@ -399,7 +404,7 @@ button.quick-battlemap-quick-import { } .qbi-btn-primary { - flex: 1; + flex: 1 1 75%; background: linear-gradient( 135deg, var(--qbi-primary) 0%,