more words
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { sound } from '../engine/audio.js';
|
||||
|
||||
export function createTileGrid({ onTileSelected }) {
|
||||
export function createTileGrid({ onTileSelected, onTogglePin }) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'board-container';
|
||||
|
||||
@@ -13,15 +13,27 @@ export function createTileGrid({ onTileSelected }) {
|
||||
.map(tile => {
|
||||
const isSelected = activeSet.has(tile.id);
|
||||
const isLocked = tile.isLocked;
|
||||
const isPinned = !!tile.isPinned;
|
||||
|
||||
return `
|
||||
<button
|
||||
class="board-tile ${isSelected ? 'selected' : ''} ${isLocked ? 'locked' : ''}"
|
||||
<div
|
||||
class="board-tile ${isSelected ? 'selected' : ''} ${isLocked ? 'locked' : ''} ${isPinned ? 'pinned' : ''}"
|
||||
data-id="${tile.id}"
|
||||
aria-label="Tile ${tile.text}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Tile ${tile.text}${isPinned ? ', pinned' : ''}"
|
||||
>
|
||||
${tile.text}
|
||||
</button>
|
||||
<span class="tile-letter-text">${tile.text}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="tile-pin-btn ${isPinned ? 'is-pinned' : ''}"
|
||||
data-pin-id="${tile.id}"
|
||||
aria-label="${isPinned ? 'Unpin tile' : 'Pin tile in place'}"
|
||||
title="${isPinned ? 'Unpin tile (Click or right-click to pin/unpin)' : 'Pin tile to current spot (keeps it from moving on shuffle)'}"
|
||||
>
|
||||
<span class="pin-icon">${isPinned ? '📌' : '📍'}</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
@@ -29,12 +41,48 @@ export function createTileGrid({ onTileSelected }) {
|
||||
`;
|
||||
|
||||
const tiles = container.querySelectorAll('.board-tile');
|
||||
tiles.forEach(tileBtn => {
|
||||
const id = parseInt(tileBtn.getAttribute('data-id'), 10);
|
||||
tileBtn.addEventListener('click', () => {
|
||||
tiles.forEach(tileEl => {
|
||||
const id = parseInt(tileEl.getAttribute('data-id'), 10);
|
||||
|
||||
// Main tile click to add/remove from tray
|
||||
tileEl.addEventListener('click', e => {
|
||||
// If clicking pin button, don't trigger tile selection
|
||||
if (e.target.closest('.tile-pin-btn')) return;
|
||||
sound.playTileClick();
|
||||
onTileSelected(id);
|
||||
});
|
||||
|
||||
// Keyboard accessibility
|
||||
tileEl.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
sound.playTileClick();
|
||||
onTileSelected(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Right-click to toggle pin
|
||||
tileEl.addEventListener('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
if (onTogglePin) {
|
||||
sound.playTileClick();
|
||||
onTogglePin(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Pin button click listener
|
||||
const pinBtns = container.querySelectorAll('.tile-pin-btn');
|
||||
pinBtns.forEach(btn => {
|
||||
const pinId = parseInt(btn.getAttribute('data-pin-id'), 10);
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (onTogglePin) {
|
||||
sound.playTileClick();
|
||||
onTogglePin(pinId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
11955
src/data/puzzles.js
11955
src/data/puzzles.js
File diff suppressed because it is too large
Load Diff
@@ -4,22 +4,91 @@ import { solveBoard } from './solver.js';
|
||||
/**
|
||||
* Generates a random valid Quartiles puzzle from the target word pool
|
||||
*/
|
||||
/**
|
||||
* Generates a random valid Quartiles puzzle from the target word pool
|
||||
* Guarantees:
|
||||
* - 20 strictly unique tiles (no duplicates anywhere on board)
|
||||
* - All tiles are at least 2 characters (never 1 letter)
|
||||
* - Anti-repetition: tracks recently used words in localStorage across games
|
||||
* - Highly varied and random puzzles
|
||||
*/
|
||||
export function generateRandomPuzzle(seed) {
|
||||
const pool = [...TARGET_WORDS_POOL];
|
||||
// Shuffle pool
|
||||
for (let i = pool.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[pool[i], pool[j]] = [pool[j], pool[i]];
|
||||
// Load recently used target words to prevent repetition across games
|
||||
let recentWords = [];
|
||||
try {
|
||||
const rawRecent = localStorage.getItem('quartiles_recent_target_words');
|
||||
if (rawRecent) {
|
||||
recentWords = JSON.parse(rawRecent);
|
||||
if (!Array.isArray(recentWords)) recentWords = [];
|
||||
}
|
||||
} catch (e) {
|
||||
recentWords = [];
|
||||
}
|
||||
const recentSet = new Set(recentWords.map(w => w.toLowerCase()));
|
||||
|
||||
// Filter pool to prioritize words not played recently
|
||||
const nonRecentPool = TARGET_WORDS_POOL.filter(w => !recentSet.has(w.word.toLowerCase()));
|
||||
// If non-recent pool is too small, use full pool
|
||||
const poolCandidate = nonRecentPool.length >= 25 ? nonRecentPool : [...TARGET_WORDS_POOL];
|
||||
|
||||
// Helper to shuffle an array
|
||||
function shuffle(arr) {
|
||||
const a = [...arr];
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
// Pick 5 words that don't repeat letters/roots too excessively
|
||||
const selectedWords = pool.slice(0, 5);
|
||||
const quartets = selectedWords.map(w => w.tiles);
|
||||
// Attempt to find 5 words with 0 tile collisions (strictly 20 unique tiles)
|
||||
let selectedWords = null;
|
||||
const attemptsPools = [shuffle(poolCandidate), shuffle(TARGET_WORDS_POOL), shuffle(TARGET_WORDS_POOL)];
|
||||
|
||||
const solved = solveBoard(quartets);
|
||||
for (const pool of attemptsPools) {
|
||||
const picked = [];
|
||||
const usedTiles = new Set();
|
||||
|
||||
for (const item of pool) {
|
||||
if (!item.tiles || item.tiles.length !== 4) continue;
|
||||
// Ensure no 1-letter tiles
|
||||
if (!item.tiles.every(t => t && t.length >= 2)) continue;
|
||||
|
||||
// Check collision with already used tiles
|
||||
const hasCollision = item.tiles.some(t => usedTiles.has(t.toLowerCase()));
|
||||
if (!hasCollision) {
|
||||
picked.push(item);
|
||||
item.tiles.forEach(t => usedTiles.add(t.toLowerCase()));
|
||||
if (picked.length === 5) {
|
||||
selectedWords = picked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedWords && selectedWords.length === 5) break;
|
||||
}
|
||||
|
||||
// Fallback if extreme constraints somehow failed
|
||||
if (!selectedWords || selectedWords.length < 5) {
|
||||
selectedWords = shuffle(TARGET_WORDS_POOL).slice(0, 5);
|
||||
}
|
||||
|
||||
// Save selected words into recent words history (keep up to last 50 words)
|
||||
try {
|
||||
const updatedRecent = [
|
||||
...selectedWords.map(w => w.word.toLowerCase()),
|
||||
...recentWords.filter(w => !selectedWords.some(sw => sw.word.toLowerCase() === w))
|
||||
].slice(0, 50);
|
||||
localStorage.setItem('quartiles_recent_target_words', JSON.stringify(updatedRecent));
|
||||
} catch (e) {
|
||||
// Non-fatal if localStorage unavailable
|
||||
}
|
||||
|
||||
const quartets = selectedWords.map(w => w.tiles.map(t => t.toLowerCase().trim()));
|
||||
const solved = solveBoard(quartets, 30);
|
||||
|
||||
const id = 'custom-' + Date.now();
|
||||
const theme = selectedWords[0].theme ? `${selectedWords[0].theme.toUpperCase()} & MORE` : 'Unlimited Challenge';
|
||||
const theme = selectedWords[0].theme ? `${selectedWords[0].theme.toUpperCase()} & BEYOND` : 'Unlimited Challenge';
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -44,12 +113,17 @@ export function generateRandomPuzzle(seed) {
|
||||
/**
|
||||
* Validates and creates a custom puzzle from user-provided quartets
|
||||
* Expects array of 5 quartets: [[t1,t2,t3,t4], [t5,t6,t7,t8], ...]
|
||||
* Enforces:
|
||||
* - Exactly 20 tiles total
|
||||
* - All 20 tiles must be unique (no duplicate tiles on board)
|
||||
* - Every tile must be at least 2 letters (no 1-letter tiles)
|
||||
*/
|
||||
export function createCustomPuzzle(quartets, customTitle = 'Custom Community Puzzle') {
|
||||
if (!Array.isArray(quartets) || quartets.length !== 5) {
|
||||
throw new Error('Puzzle must have exactly 5 quartets (4-tile words)');
|
||||
}
|
||||
|
||||
const allTiles = [];
|
||||
for (let i = 0; i < quartets.length; i++) {
|
||||
const q = quartets[i];
|
||||
if (!Array.isArray(q) || q.length !== 4) {
|
||||
@@ -57,14 +131,21 @@ export function createCustomPuzzle(quartets, customTitle = 'Custom Community Puz
|
||||
}
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const tile = q[j].toLowerCase().trim();
|
||||
if (!/^[a-z]{2,4}$/.test(tile)) {
|
||||
throw new Error(`Tile "${tile}" in quartet #${i + 1} must be 2 to 4 letters`);
|
||||
if (!/^[a-z]{2,8}$/.test(tile)) {
|
||||
throw new Error(`Tile "${tile}" in quartet #${i + 1} must be at least 2 letters`);
|
||||
}
|
||||
allTiles.push(tile);
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce no duplicate tiles across the board
|
||||
const uniqueTileSet = new Set(allTiles);
|
||||
if (uniqueTileSet.size !== 20) {
|
||||
throw new Error('All 20 tiles on the board must be unique. Duplicate tiles are not allowed.');
|
||||
}
|
||||
|
||||
const normalizedQuartets = quartets.map(q => q.map(t => t.toLowerCase().trim()));
|
||||
const solved = solveBoard(normalizedQuartets);
|
||||
const solved = solveBoard(normalizedQuartets, 30);
|
||||
|
||||
return {
|
||||
id: 'user-' + btoa(normalizedQuartets.map(q => q.join('-')).join('|')).slice(0, 16),
|
||||
|
||||
@@ -37,6 +37,13 @@ export function solveBoard(quartets, maxWords = 30) {
|
||||
const quartilesList = quartets.map(q => q.join('').toLowerCase());
|
||||
const quartileSet = new Set(quartilesList);
|
||||
|
||||
// Target quartiles are always valid, even if missing from the dictionary
|
||||
quartilesList.forEach((word, qIdx) => {
|
||||
if (!foundMap.has(word)) {
|
||||
foundMap.set(word, [qIdx * 4, qIdx * 4 + 1, qIdx * 4 + 2, qIdx * 4 + 3]);
|
||||
}
|
||||
});
|
||||
|
||||
let words = [];
|
||||
|
||||
for (const [word, path] of foundMap.entries()) {
|
||||
|
||||
@@ -145,6 +145,9 @@ async function initApp() {
|
||||
const tileGrid = createTileGrid({
|
||||
onTileSelected: tileId => {
|
||||
gameState.onTileClicked(tileId);
|
||||
},
|
||||
onTogglePin: tileId => {
|
||||
gameState.togglePinTile(tileId);
|
||||
}
|
||||
});
|
||||
appContainer.appendChild(tileGrid.element);
|
||||
|
||||
@@ -4,8 +4,9 @@ import { getRankInfo, QUARTILE_COMPLETION_BONUS } from '../engine/constants.js';
|
||||
class GameStateManager {
|
||||
constructor() {
|
||||
this.currentPuzzle = null;
|
||||
this.boardTiles = []; // Array of { id, text, isQuartileSolved, isLocked }
|
||||
this.boardTiles = []; // Array of { id, text, originalText, isQuartileSolved, isLocked, isPinned }
|
||||
this.activeTileIds = []; // Array of tile IDs in construction tray
|
||||
this.userPinnedTileIds = new Set(); // Set of tile IDs manually pinned by user
|
||||
this.foundWords = new Map(); // word -> wordData
|
||||
this.score = 0;
|
||||
this.quartilesBonusAwarded = false;
|
||||
@@ -48,6 +49,7 @@ class GameStateManager {
|
||||
this.currentPuzzle = puzzle;
|
||||
this.saveActivePuzzle();
|
||||
this.activeTileIds = [];
|
||||
this.userPinnedTileIds = new Set();
|
||||
this.foundWords = new Map();
|
||||
this.score = 0;
|
||||
this.quartilesBonusAwarded = false;
|
||||
@@ -60,7 +62,8 @@ class GameStateManager {
|
||||
text: text.toUpperCase(),
|
||||
originalText: text,
|
||||
isQuartileSolved: false,
|
||||
isLocked: false
|
||||
isLocked: false,
|
||||
isPinned: false
|
||||
}));
|
||||
|
||||
// Restore saved state from LocalStorage if available
|
||||
@@ -136,6 +139,8 @@ class GameStateManager {
|
||||
score: this.score,
|
||||
bonus: this.quartilesBonusAwarded,
|
||||
tileOrder: this.boardTiles.map(t => t.id),
|
||||
pinnedTileIds: Array.from(this.userPinnedTileIds),
|
||||
lockQuartiles: this.lockQuartiles,
|
||||
activeTileIds: this.activeTileIds,
|
||||
isRevealed: this.isRevealed,
|
||||
hintIndex: this.hintIndex,
|
||||
@@ -143,6 +148,11 @@ class GameStateManager {
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(saveKey, JSON.stringify(payload));
|
||||
// Maintain persistent list of all played puzzle IDs
|
||||
const rawAll = localStorage.getItem('quartiles_all_saved_puzzle_ids');
|
||||
const allSaved = new Set(rawAll ? JSON.parse(rawAll) : []);
|
||||
allSaved.add(this.currentPuzzle.id);
|
||||
localStorage.setItem('quartiles_all_saved_puzzle_ids', JSON.stringify(Array.from(allSaved)));
|
||||
} catch (err) {
|
||||
console.warn('LocalStorage save failed:', err);
|
||||
}
|
||||
@@ -182,9 +192,16 @@ class GameStateManager {
|
||||
}
|
||||
this.isRevealed = !!parsed.isRevealed;
|
||||
this.hintIndex = parsed.hintIndex || 0;
|
||||
if (parsed.lockQuartiles !== undefined) {
|
||||
this.lockQuartiles = parsed.lockQuartiles;
|
||||
}
|
||||
|
||||
// Re-mark solved quartiles
|
||||
this.updateSolvedQuartileTiles();
|
||||
// Restore pinned tiles
|
||||
if (Array.isArray(parsed.pinnedTileIds)) {
|
||||
this.userPinnedTileIds = new Set(parsed.pinnedTileIds.filter(id => id >= 0 && id < 20));
|
||||
} else {
|
||||
this.userPinnedTileIds = new Set();
|
||||
}
|
||||
|
||||
// Restore active tray tiles if any
|
||||
if (Array.isArray(parsed.activeTileIds)) {
|
||||
@@ -194,16 +211,18 @@ class GameStateManager {
|
||||
// Restore tile order if saved
|
||||
if (Array.isArray(parsed.tileOrder) && parsed.tileOrder.length === 20) {
|
||||
const isSequential = parsed.tileOrder.every((id, idx) => id === idx);
|
||||
if (!isSequential || this.foundWords.size > 0) {
|
||||
if (!isSequential || this.foundWords.size > 0 || this.userPinnedTileIds.size > 0) {
|
||||
const tileMap = new Map(this.boardTiles.map(t => [t.id, t]));
|
||||
const reordered = parsed.tileOrder.map(id => tileMap.get(id)).filter(Boolean);
|
||||
if (reordered.length === 20) {
|
||||
this.boardTiles = reordered;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.foundWords.size > 0;
|
||||
|
||||
// Re-mark solved quartiles and user-pinned tiles
|
||||
this.updateSolvedQuartileTiles();
|
||||
return this.foundWords.size > 0 || this.userPinnedTileIds.size > 0;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -212,7 +231,7 @@ class GameStateManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if a tile belongs to an already found Quartile
|
||||
// Check if a tile belongs to an already found Quartile or is user-pinned
|
||||
updateSolvedQuartileTiles() {
|
||||
const solvedQuartileWords = new Set();
|
||||
for (const [w, data] of this.foundWords.entries()) {
|
||||
@@ -221,10 +240,11 @@ class GameStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
// For each target quartet, check if solved
|
||||
// Initialize all tiles with their pin state
|
||||
this.boardTiles.forEach(tile => {
|
||||
tile.isQuartileSolved = false;
|
||||
tile.isLocked = false;
|
||||
tile.isPinned = this.userPinnedTileIds.has(tile.id);
|
||||
tile.isLocked = tile.isPinned;
|
||||
});
|
||||
|
||||
for (let qIdx = 0; qIdx < this.currentPuzzle.quartets.length; qIdx++) {
|
||||
@@ -237,7 +257,7 @@ class GameStateManager {
|
||||
const tile = this.boardTiles.find(t => t.id === i);
|
||||
if (tile) {
|
||||
tile.isQuartileSolved = true;
|
||||
if (this.lockQuartiles) {
|
||||
if (this.lockQuartiles || tile.isPinned) {
|
||||
tile.isLocked = true;
|
||||
}
|
||||
}
|
||||
@@ -246,6 +266,25 @@ class GameStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle user pin on an individual tile
|
||||
togglePinTile(tileId) {
|
||||
if (this.userPinnedTileIds.has(tileId)) {
|
||||
this.userPinnedTileIds.delete(tileId);
|
||||
} else {
|
||||
this.userPinnedTileIds.add(tileId);
|
||||
}
|
||||
this.updateSolvedQuartileTiles();
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
const tile = this.boardTiles.find(t => t.id === tileId);
|
||||
const isPinned = this.userPinnedTileIds.has(tileId);
|
||||
this.notifyToast(
|
||||
isPinned ? `📌 Pinned tile "${tile?.text || ''}" in place` : `🔓 Unpinned tile "${tile?.text || ''}"`,
|
||||
'info',
|
||||
1600
|
||||
);
|
||||
}
|
||||
|
||||
// Reorganizes the board: locked solved quartiles go to top rows
|
||||
reorganizeBoard() {
|
||||
this.updateSolvedQuartileTiles();
|
||||
@@ -347,30 +386,56 @@ class GameStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle unlocked tiles
|
||||
// Shuffle unlocked & unpinned tiles
|
||||
shuffle() {
|
||||
const lockedTiles = this.lockQuartiles ? this.boardTiles.filter(t => t.isLocked) : [];
|
||||
const unlockedTiles = this.lockQuartiles ? this.boardTiles.filter(t => !t.isLocked) : [...this.boardTiles];
|
||||
const unpinnedPositions = [];
|
||||
const unpinnedTiles = [];
|
||||
|
||||
this.boardTiles.forEach((tile, idx) => {
|
||||
const isFixed = tile.isPinned || (this.lockQuartiles && tile.isLocked);
|
||||
if (!isFixed) {
|
||||
unpinnedPositions.push(idx);
|
||||
unpinnedTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
// Fisher-Yates shuffle
|
||||
for (let i = unlockedTiles.length - 1; i > 0; i--) {
|
||||
for (let i = unpinnedTiles.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[unlockedTiles[i], unlockedTiles[j]] = [unlockedTiles[j], unlockedTiles[i]];
|
||||
[unpinnedTiles[i], unpinnedTiles[j]] = [unpinnedTiles[j], unpinnedTiles[i]];
|
||||
}
|
||||
|
||||
this.boardTiles = [...lockedTiles, ...unlockedTiles];
|
||||
const newBoard = [...this.boardTiles];
|
||||
unpinnedPositions.forEach((pos, idx) => {
|
||||
newBoard[pos] = unpinnedTiles[idx];
|
||||
});
|
||||
|
||||
this.boardTiles = newBoard;
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
}
|
||||
|
||||
// Sort unlocked tiles alphabetically
|
||||
// Sort unlocked & unpinned tiles alphabetically
|
||||
sortAlphabetical() {
|
||||
const lockedTiles = this.lockQuartiles ? this.boardTiles.filter(t => t.isLocked) : [];
|
||||
const unlockedTiles = this.lockQuartiles ? this.boardTiles.filter(t => !t.isLocked) : [...this.boardTiles];
|
||||
const unpinnedPositions = [];
|
||||
const unpinnedTiles = [];
|
||||
|
||||
unlockedTiles.sort((a, b) => a.text.localeCompare(b.text));
|
||||
this.boardTiles.forEach((tile, idx) => {
|
||||
const isFixed = tile.isPinned || (this.lockQuartiles && tile.isLocked);
|
||||
if (!isFixed) {
|
||||
unpinnedPositions.push(idx);
|
||||
unpinnedTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
this.boardTiles = [...lockedTiles, ...unlockedTiles];
|
||||
unpinnedTiles.sort((a, b) => a.text.localeCompare(b.text));
|
||||
|
||||
const newBoard = [...this.boardTiles];
|
||||
unpinnedPositions.forEach((pos, idx) => {
|
||||
newBoard[pos] = unpinnedTiles[idx];
|
||||
});
|
||||
|
||||
this.boardTiles = newBoard;
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
}
|
||||
@@ -385,13 +450,16 @@ class GameStateManager {
|
||||
const constructedWord = this.activeTileIds
|
||||
.map(id => {
|
||||
const tile = this.boardTiles.find(t => t.id === id);
|
||||
return tile ? tile.text.toLowerCase() : '';
|
||||
return tile ? tile.text.toLowerCase().trim() : '';
|
||||
})
|
||||
.join('');
|
||||
|
||||
// Check if already found
|
||||
if (this.foundWords.has(constructedWord)) {
|
||||
this.notifyToast(`"${constructedWord.toUpperCase()}" already found!`, 'warning');
|
||||
this.activeTileIds = [];
|
||||
this.saveProgress();
|
||||
this.notify();
|
||||
return { success: false, reason: 'duplicate', word: constructedWord };
|
||||
}
|
||||
|
||||
|
||||
@@ -338,6 +338,46 @@
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* User-pinned tile state */
|
||||
.board-tile.pinned {
|
||||
border-color: #f59e0b !important;
|
||||
box-shadow: 0 0 0 1.5px rgba(245, 158, 11, 0.45), var(--tile-shadow) !important;
|
||||
}
|
||||
|
||||
.tile-pin-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
opacity: 0.22;
|
||||
transition: opacity var(--transition-fast), transform var(--transition-fast);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.board-tile:hover .tile-pin-btn,
|
||||
.tile-pin-btn.is-pinned {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.tile-pin-btn .pin-icon {
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tile-pin-btn.is-pinned {
|
||||
transform: scale(1.12);
|
||||
filter: drop-shadow(0 1px 2px rgba(245, 158, 11, 0.4));
|
||||
}
|
||||
|
||||
/* Board tile shuffle animation */
|
||||
.board-tile.flipping {
|
||||
animation: tileFlip 0.4s var(--ease-smooth);
|
||||
|
||||
Reference in New Issue
Block a user