diff --git a/beango/css/beango.css b/beango/css/beango.css index 32f7cfb..8998cea 100644 --- a/beango/css/beango.css +++ b/beango/css/beango.css @@ -53,9 +53,10 @@ body { /* Style for permanently marked cells */ .bingo-cell.marked { - background-color: #fde047; /* Tailwind yellow-300 */ + /* background-color: #fde047; */ /* REMOVED - Handled by JS */ /* Optional: Add other persistent styles like a stronger border */ /* border-color: #ca8a04; */ /* Tailwind yellow-600 */ + /* Styles like background-image, background-size, background-position, background-repeat, and opacity will be added by JS */ } /* Style for when a MARKED cell is actively being clicked */ @@ -98,3 +99,56 @@ body { /* Basic styling for the board */ #bingo-board-container { } + +/* Styling for config pane sections */ +.config-section { + border-bottom: 1px solid #e5e7eb; /* Tailwind gray-200 */ + padding-bottom: 0.75rem; /* pb-3 */ + margin-bottom: 0.75rem; /* mb-3 */ +} + +.config-section:last-of-type { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 0; +} + +.config-summary { + cursor: pointer; + font-weight: 600; /* semibold */ + color: #1f2937; /* gray-800 */ + padding: 0.5rem 0; /* py-2 */ + list-style: none; /* Remove default marker */ + position: relative; + padding-left: 1.5rem; /* Make space for custom marker */ +} + +/* Custom arrow marker */ +.config-summary::before { + content: ''; + border-width: 0 2px 2px 0; + border-style: solid; + border-color: #6b7280; /* gray-500 */ + display: inline-block; + padding: 3px; + transform: rotate(45deg); + position: absolute; + left: 0.25rem; + top: 0.85rem; /* Adjust vertical alignment */ + transition: transform 0.2s ease-in-out; +} + +/* Rotate arrow when section is open */ +details[open] > .config-summary::before { + transform: rotate(-135deg); + top: 1rem; /* Adjust vertical alignment for open state */ +} + +/* Hide default
marker */ +.config-summary::-webkit-details-marker { + display: none; +} + +.config-content { + padding-left: 1.5rem; /* Indent content */ +} diff --git a/beango/index.html b/beango/index.html index de4ea93..d9d0d78 100644 --- a/beango/index.html +++ b/beango/index.html @@ -58,90 +58,130 @@

Configuration

-
- - -
-
- - -
-
- - -
- -
- -
- - -
-
- - -

The bean image will appear next to the text.

-
- -
- +
+ Board Settings +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+
-
- -
- - +
+ Header Customization +
+
+ + +
+
+ + +

The bean image will appear next to the text.

+
+
-
-
- - -
- - - +
+ +
+ Background Style +
+
+ + +
+
+ + +
+ +
+
+ +
+ Marked Cell Style +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+ + + diff --git a/beango/js/beango.js b/beango/js/beango.js index 0076fc2..d9590af 100644 --- a/beango/js/beango.js +++ b/beango/js/beango.js @@ -16,6 +16,10 @@ const LS_GRADIENT_DIRECTION = 'beango_gradientDirection'; const LS_ORIGINAL_ITEMS = 'beango_originalItems'; // User's raw input const LS_HEADER_TYPE = 'beango_headerType'; // 'text' or 'image' const LS_HEADER_CONTENT = 'beango_headerContent'; // Text string or Image URL +const LS_MARKED_STYLE_TYPE = 'beango_markedStyleType'; // 'color' or 'image' +const LS_MARKED_COLOR = 'beango_markedColor'; +const LS_MARKED_IMAGE_URL = 'beango_markedImageUrl'; +const LS_MARKED_OPACITY = 'beango_markedOpacity'; // Stored as 0-100 // --- Default Values --- const DEFAULT_SOLID_COLOR = '#ff7e5f'; // Default to first color of gradient @@ -25,6 +29,10 @@ const DEFAULT_GRADIENT_DIRECTION = '135deg'; // From main page gradient const DEFAULT_HEADER_TYPE = 'text'; const DEFAULT_HEADER_TEXT = 'Beango!'; const DEFAULT_HEADER_IMAGE_URL = ''; // No default image +const DEFAULT_MARKED_STYLE_TYPE = 'color'; +const DEFAULT_MARKED_COLOR = '#fde047'; // Default yellow +const DEFAULT_MARKED_IMAGE_URL = ''; +const DEFAULT_MARKED_OPACITY = 80; // Default 80% // --- Notification Function --- function showNotification(message, type = 'info', duration = 3000) { @@ -173,6 +181,92 @@ function getItemsFromInput() { return text ? text.split('\n').map(item => item.trim()).filter(item => item) : []; } +// --- Save current marked style settings to localStorage --- +function saveMarkedStyleSettings() { + const styleType = document.querySelector('input[name="marked-style-type"]:checked').value; + localStorage.setItem(LS_MARKED_STYLE_TYPE, styleType); + + if (styleType === 'color') { + localStorage.setItem(LS_MARKED_COLOR, document.getElementById('marked-color-picker').value); + // Optionally remove image URL if switching to color + // localStorage.removeItem(LS_MARKED_IMAGE_URL); + } else { // image + localStorage.setItem(LS_MARKED_IMAGE_URL, document.getElementById('marked-image-url-input').value); + // Optionally remove color if switching to image + // localStorage.removeItem(LS_MARKED_COLOR); + } + // Always save opacity + let opacity = parseInt(document.getElementById('marked-opacity-input').value, 10); + if (isNaN(opacity) || opacity < 0) opacity = 0; + if (opacity > 100) opacity = 100; + localStorage.setItem(LS_MARKED_OPACITY, opacity); +} + +// --- Function to manage visibility of marked style controls --- +function toggleMarkedStyleInputs() { + const styleType = document.querySelector('input[name="marked-style-type"]:checked').value; + const colorSettings = document.getElementById('marked-color-settings'); + const imageSettings = document.getElementById('marked-image-settings'); + + if (styleType === 'color') { + colorSettings.style.display = 'block'; + imageSettings.style.display = 'none'; + } else { // image + colorSettings.style.display = 'none'; + imageSettings.style.display = 'block'; + } + // Opacity input is always visible +} + +// --- Apply saved marked styles to a cell --- +function applyMarkedCellStyle(cell) { + if (!cell) return; + + const isMarked = cell.classList.contains('marked'); + const styleType = localStorage.getItem(LS_MARKED_STYLE_TYPE) || DEFAULT_MARKED_STYLE_TYPE; + const opacityValue = (parseInt(localStorage.getItem(LS_MARKED_OPACITY), 10) || DEFAULT_MARKED_OPACITY) / 100; + + // Reset styles first + cell.style.backgroundColor = ''; // Use CSS default + cell.style.backgroundImage = ''; + cell.style.backgroundSize = ''; + cell.style.backgroundPosition = ''; + cell.style.backgroundRepeat = ''; + cell.style.opacity = ''; // Use CSS default + + if (isMarked) { + cell.style.opacity = opacityValue; + + if (styleType === 'color') { + const color = localStorage.getItem(LS_MARKED_COLOR) || DEFAULT_MARKED_COLOR; + cell.style.backgroundColor = color; + } else { // image + const imageUrl = localStorage.getItem(LS_MARKED_IMAGE_URL) || DEFAULT_MARKED_IMAGE_URL; + if (imageUrl) { + cell.style.backgroundImage = `url('${imageUrl}')`; + cell.style.backgroundSize = 'cover'; // Or 'contain', 'auto', etc. + cell.style.backgroundPosition = 'center center'; + cell.style.backgroundRepeat = 'no-repeat'; + // Set a fallback background color in case the image fails to load or is transparent + cell.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; // Slightly visible white + } else { + // Fallback to default color if image URL is empty + cell.style.backgroundColor = DEFAULT_MARKED_COLOR; + } + } + } else { + // If unmarked, ensure all styles are fully reset (already done above) + } +} + +// --- Re-apply styles to all currently marked cells --- +function refreshMarkedCellStyles() { + const markedCells = document.querySelectorAll('#bingo-board .bingo-cell.marked'); + markedCells.forEach(cell => { + applyMarkedCellStyle(cell); // Re-apply based on current settings + }); +} + // Helper to save board state function saveBoardState() { const sizeInput = document.getElementById('board-size'); @@ -187,6 +281,7 @@ function saveBoardState() { localStorage.setItem(LS_ORIGINAL_ITEMS, JSON.stringify(originalUserItems)); // Save user's raw input from textarea saveBackgroundSettings(); // Save background settings from inputs saveHeaderSettings(); // Save header settings from inputs + saveMarkedStyleSettings(); // Save marked cell style settings } function generateBoard() { @@ -246,7 +341,7 @@ function generateBoard() { board.appendChild(cell); }); - clearMarks(false); // Clear previous marks visually but don't save yet + clearMarks(false); // Clear previous marks visually AND their styles, but don't save yet equalizeCellSizes(); // Add this call // saveBoardState(); // REMOVED first call - Save the newly generated board state (size, items, displayed, marks) @@ -267,7 +362,7 @@ function generateBoard() { showNotification(notificationMessage, notificationType); // *** Save the complete final state just before reload *** - saveBoardState(); // Includes size, items, displayed, marks, original items, background, header + saveBoardState(); // Includes size, items, displayed, marks, original items, background, header, marked style // Reload the page to ensure correct rendering based on saved state location.reload(); @@ -296,6 +391,14 @@ function generateBoard() { toggleHeaderInputs(); // Ensure correct header inputs are visible updateHeaderDisplay(); // Apply default header display + // Reset marked style inputs to defaults + document.querySelector('input[name="marked-style-type"][value="color"]').checked = true; + document.getElementById('marked-color-picker').value = DEFAULT_MARKED_COLOR; + document.getElementById('marked-image-url-input').value = DEFAULT_MARKED_IMAGE_URL; + document.getElementById('marked-opacity-input').value = DEFAULT_MARKED_OPACITY; + toggleMarkedStyleInputs(); // Ensure correct marked style inputs are visible + refreshMarkedCellStyles(); // Apply default styles (which is none, effectively) + // Clear the board display const boardHeader = document.getElementById('board-header'); if (boardHeader) boardHeader.style.maxWidth = ''; @@ -357,13 +460,19 @@ function randomizeBoard() { function selectCell(cell) { cell.classList.toggle('marked'); // Toggle the dedicated 'marked' class + applyMarkedCellStyle(cell); // Apply/remove styles based on new state and settings saveBoardState(); // Save updated marks immediately after click } function clearMarks(save = true) { const cells = document.querySelectorAll('#bingo-board .bingo-cell'); cells.forEach(cell => { - cell.classList.remove('marked', 'ring-2', 'ring-blue-500'); // Use 'marked' class + if (cell.classList.contains('marked')) { + cell.classList.remove('marked'); + applyMarkedCellStyle(cell); // Reset styles for this cell + } + // Ensure styles are reset even if class was somehow missing + applyMarkedCellStyle(cell); }); if (save) { saveBoardState(); // Save cleared marks @@ -385,61 +494,62 @@ function shuffleArray(array) { // Function to reset settings and clear board function resetSettings() { - // Clear localStorage relevant to the board + // Clear localStorage relevant ONLY to the board structure/content localStorage.removeItem(LS_BOARD_SIZE); - localStorage.removeItem(LS_CELL_ITEMS); - localStorage.removeItem(LS_DISPLAYED_ITEMS); + localStorage.removeItem(LS_CELL_ITEMS); // The items configured for the board (padded/sliced) + localStorage.removeItem(LS_DISPLAYED_ITEMS); // The current layout localStorage.removeItem(LS_MARKED_INDICES); - localStorage.removeItem(LS_ORIGINAL_ITEMS); // Clear saved raw input - // Optionally reset config pane state too? - // localStorage.removeItem(LS_CONFIG_OPEN); - localStorage.removeItem(LS_BACKGROUND_TYPE); - localStorage.removeItem(LS_SOLID_COLOR); - localStorage.removeItem(LS_GRADIENT_COLOR_1); - localStorage.removeItem(LS_GRADIENT_COLOR_2); - localStorage.removeItem(LS_GRADIENT_DIRECTION); + localStorage.removeItem(LS_ORIGINAL_ITEMS); // User's raw input - // Reset global variables + // DO NOT clear these: + // localStorage.removeItem(LS_CONFIG_OPEN); + // localStorage.removeItem(LS_BACKGROUND_TYPE); + // localStorage.removeItem(LS_SOLID_COLOR); + // localStorage.removeItem(LS_GRADIENT_COLOR_1); + // localStorage.removeItem(LS_GRADIENT_COLOR_2); + // localStorage.removeItem(LS_GRADIENT_DIRECTION); + // localStorage.removeItem(LS_HEADER_TYPE); + // localStorage.removeItem(LS_HEADER_CONTENT); + // localStorage.removeItem(LS_MARKED_STYLE_TYPE); + // localStorage.removeItem(LS_MARKED_COLOR); + // localStorage.removeItem(LS_MARKED_IMAGE_URL); + // localStorage.removeItem(LS_MARKED_OPACITY); + + // Reset global variables for board content currentItems = []; displayedItems = []; - // Reset form inputs + // Reset ONLY board-related form inputs document.getElementById('board-size').value = 5; // Default size document.getElementById('cell-contents').value = ''; document.getElementById('file-input').value = ''; // Clear file input - // Reset background inputs to defaults - document.querySelector('input[name="background-type"][value="gradient"]').checked = true; // Default to gradient - document.getElementById('background-color-picker').value = DEFAULT_SOLID_COLOR; - document.getElementById('gradient-color-1').value = DEFAULT_GRADIENT_COLOR_1; - document.getElementById('gradient-color-2').value = DEFAULT_GRADIENT_COLOR_2; - document.getElementById('gradient-direction').value = DEFAULT_GRADIENT_DIRECTION; - toggleBackgroundControls(); // Ensure correct controls are visible - - // Reset header inputs to defaults - document.querySelector('input[name="header-type"][value="text"]').checked = true; - document.getElementById('header-text-input').value = DEFAULT_HEADER_TEXT; - document.getElementById('header-image-url-input').value = DEFAULT_HEADER_IMAGE_URL; - toggleHeaderInputs(); // Ensure correct header inputs are visible - updateHeaderDisplay(); // Apply default header display + // DO NOT reset background inputs + // DO NOT reset header inputs + // DO NOT reset marked style inputs // Clear the board display const board = document.getElementById('bingo-board'); - board.innerHTML = '
Settings Reset. Generate a new board!
'; + board.innerHTML = '
Board Reset. Generate a new board!
'; board.style.gridTemplateColumns = ''; // Clear grid style board.style.gridAutoRows = ''; // Also clear gridAutoRows - // Reset container width to default - REMOVED - // updateBoardContainerMaxWidth(5); // Assuming 5 is the default size - // updateBoardContainerMaxWidth(5, '#board-header'); - // Also clear any inline max-width set by equalizeCellSizes + // Reset container width to default const boardContainer = document.getElementById('bingo-board-container'); const boardHeader = document.getElementById('board-header'); - if (boardContainer) boardContainer.style.maxWidth = ''; - if (boardHeader) boardHeader.style.maxWidth = ''; + if (boardContainer) boardContainer.style.maxWidth = ''; // Let it resize naturally or via equalize + if (boardHeader) boardHeader.style.maxWidth = ''; // Let it resize naturally or via equalize + // Call equalize after potentially changing board content/size + equalizeCellSizes(); - setBackground(); // Apply default background - showNotification('Settings and board reset.', 'success'); + // DO NOT reset background - keep current style + // setBackground(); + // DO NOT reset header display - keep current style + // updateHeaderDisplay(); + // DO NOT refresh marked styles - keep current style + // refreshMarkedCellStyles(); + + showNotification('Board settings reset. Generate a new board to apply.', 'success'); } // --- Helper to get indices of marked cells --- @@ -470,6 +580,10 @@ function loadFromLocalStorage() { const savedOriginalItemsText = localStorage.getItem(LS_ORIGINAL_ITEMS); const savedHeaderType = localStorage.getItem(LS_HEADER_TYPE) || DEFAULT_HEADER_TYPE; const savedHeaderContent = localStorage.getItem(LS_HEADER_CONTENT) || (savedHeaderType === 'text' ? DEFAULT_HEADER_TEXT : DEFAULT_HEADER_IMAGE_URL); + const savedMarkedStyleType = localStorage.getItem(LS_MARKED_STYLE_TYPE) || DEFAULT_MARKED_STYLE_TYPE; + const savedMarkedColor = localStorage.getItem(LS_MARKED_COLOR) || DEFAULT_MARKED_COLOR; + const savedMarkedImageUrl = localStorage.getItem(LS_MARKED_IMAGE_URL) || DEFAULT_MARKED_IMAGE_URL; + const savedMarkedOpacity = localStorage.getItem(LS_MARKED_OPACITY) || DEFAULT_MARKED_OPACITY; // Restore Config Pane State const pane = document.getElementById('config-pane'); @@ -523,6 +637,8 @@ function loadFromLocalStorage() { cell.onclick = () => selectCell(cell); if (markedIndices.includes(index)) { cell.classList.add('marked'); // Apply saved mark using 'marked' class + // Apply dynamic styles AFTER adding the class + applyMarkedCellStyle(cell); } board.appendChild(cell); }); @@ -568,6 +684,15 @@ function loadFromLocalStorage() { updateHeaderDisplay(); // Apply the loaded header // --- End Restore Header Settings --- + // --- Restore Marked Style Settings --- + document.querySelector(`input[name="marked-style-type"][value="${savedMarkedStyleType}"]`).checked = true; + document.getElementById('marked-color-picker').value = savedMarkedColor; + document.getElementById('marked-image-url-input').value = savedMarkedImageUrl; + document.getElementById('marked-opacity-input').value = savedMarkedOpacity; + toggleMarkedStyleInputs(); // Show/hide correct inputs + // Styles are applied during board creation loop above + // --- End Restore Marked Style Settings --- + // Restore the textarea with the original user input if available if (savedOriginalItemsText) { try { @@ -753,11 +878,12 @@ function explodeBeans() { } } -// --- Event Listener for Page Load & Color Picker --- +// --- Consolidated Event Listeners --- document.addEventListener('DOMContentLoaded', () => { + // Load state first thing loadFromLocalStorage(); - // Add listeners for background controls + // --- Add Event Listeners for Background Controls --- document.querySelectorAll('input[name="background-type"]').forEach(radio => { radio.addEventListener('change', () => { toggleBackgroundControls(); @@ -765,48 +891,62 @@ document.addEventListener('DOMContentLoaded', () => { saveBackgroundSettings(); }); }); - document.getElementById('background-color-picker').addEventListener('input', () => { setBackground(); saveBackgroundSettings(); }); - document.getElementById('gradient-color-1').addEventListener('input', () => { setBackground(); saveBackgroundSettings(); }); - - document.getElementById('gradient-color-2').addEventListener('input', () => { + document.getElementById('gradient-color-2').addEventListener('input', () => { + setBackground(); + saveBackgroundSettings(); + }); + document.getElementById('gradient-direction').addEventListener('change', () => { setBackground(); saveBackgroundSettings(); }); - document.getElementById('gradient-direction').addEventListener('change', () => { - setBackground(); - saveBackgroundSettings(); - }); - - // Header listeners + // --- Add Event Listeners for Header Controls --- document.querySelectorAll('input[name="header-type"]').forEach(radio => { radio.addEventListener('change', () => { toggleHeaderInputs(); - // Update display immediately on type change - Order changed saveHeaderSettings(); // Save the new type first updateHeaderDisplay(); // Then update display from saved state }); }); document.getElementById('header-text-input').addEventListener('input', () => { - // Order changed saveHeaderSettings(); // Save the new text first updateHeaderDisplay(); // Then update display from saved state }); document.getElementById('header-image-url-input').addEventListener('input', () => { - // Order changed saveHeaderSettings(); // Save the new URL first updateHeaderDisplay(); // Then update display from saved state }); - // Call equalizeCellSizes on resize and load + // --- Add Event Listeners for Marked Style Controls --- + document.querySelectorAll('input[name="marked-style-type"]').forEach(radio => { + radio.addEventListener('change', () => { + toggleMarkedStyleInputs(); + saveMarkedStyleSettings(); // Save the new setting + refreshMarkedCellStyles(); // Update existing marked cells + }); + }); + document.getElementById('marked-color-picker').addEventListener('input', () => { + saveMarkedStyleSettings(); + refreshMarkedCellStyles(); + }); + document.getElementById('marked-image-url-input').addEventListener('input', () => { + saveMarkedStyleSettings(); + refreshMarkedCellStyles(); + }); + document.getElementById('marked-opacity-input').addEventListener('input', () => { + saveMarkedStyleSettings(); + refreshMarkedCellStyles(); + }); + + // --- Other Listeners --- window.addEventListener('resize', equalizeCellSizes); equalizeCellSizes(); // Initial call after load });