diff --git a/js/app.js b/js/app.js
index 33cdd82..c9f0b9f 100644
--- a/js/app.js
+++ b/js/app.js
@@ -9,6 +9,7 @@ import {
sidebarSettings,
pwaUpdateSettings,
modalSettings,
+ keyboardShortcuts,
} from './storage.js';
import { UIRenderer } from './ui.js';
import { Player } from './player.js';
@@ -160,74 +161,114 @@ function initializeCasting(audioPlayer, castBtn) {
}
function initializeKeyboardShortcuts(player, audioPlayer) {
- document.addEventListener('keydown', (e) => {
- if (e.target.matches('input, textarea')) return;
+ const keyActionMap = {
+ playPause: () => {
+ trackKeyboardShortcut('Space');
+ player.handlePlayPause();
+ },
+ seekForward: () => {
+ trackKeyboardShortcut('Right');
+ audioPlayer.currentTime = Math.min(audioPlayer.duration, audioPlayer.currentTime + 10);
+ },
+ seekBackward: () => {
+ trackKeyboardShortcut('Left');
+ audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 10);
+ },
+ nextTrack: () => {
+ trackKeyboardShortcut('Shift+Right');
+ player.playNext();
+ },
+ previousTrack: () => {
+ trackKeyboardShortcut('Shift+Left');
+ player.playPrev();
+ },
+ volumeUp: () => {
+ trackKeyboardShortcut('Up');
+ player.setVolume(player.userVolume + 0.1);
+ },
+ volumeDown: () => {
+ trackKeyboardShortcut('Down');
+ player.setVolume(player.userVolume - 0.1);
+ },
+ mute: () => {
+ trackKeyboardShortcut('M');
+ audioPlayer.muted = !audioPlayer.muted;
+ },
+ shuffle: () => {
+ trackKeyboardShortcut('S');
+ document.getElementById('shuffle-btn')?.click();
+ },
+ repeat: () => {
+ trackKeyboardShortcut('R');
+ document.getElementById('repeat-btn')?.click();
+ },
+ queue: () => {
+ trackKeyboardShortcut('Q');
+ document.getElementById('queue-btn')?.click();
+ },
+ lyrics: () => {
+ trackKeyboardShortcut('L');
+ document.querySelector('.now-playing-bar .cover')?.click();
+ },
+ search: () => {
+ trackKeyboardShortcut('/');
+ document.getElementById('search-input')?.focus();
+ },
+ escape: () => {
+ trackKeyboardShortcut('Escape');
+ document.getElementById('search-input')?.blur();
+ sidePanelManager.close();
+ clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
+ },
+ visualizerNext: () => {
+ trackKeyboardShortcut('VisualizerNext');
+ const ui = window.monochromeUi;
+ if (ui?.visualizer?.presets?.['butterchurn']) {
+ ui.visualizer.presets['butterchurn'].nextPreset();
+ }
+ },
+ visualizerPrev: () => {
+ trackKeyboardShortcut('VisualizerPrev');
+ const ui = window.monochromeUi;
+ if (ui?.visualizer?.presets?.['butterchurn']) {
+ ui.visualizer.presets['butterchurn'].prevPreset();
+ }
+ },
+ visualizerCycle: () => {
+ trackKeyboardShortcut('VisualizerCycle');
+ const ui = window.monochromeUi;
+ if (ui?.visualizer?.presets?.['butterchurn']) {
+ ui.visualizer.presets['butterchurn'].toggleCycle();
+ }
+ },
+ };
- switch (e.key.toLowerCase()) {
- case ' ':
+ document.addEventListener('keydown', (e) => {
+ if (e.target.matches('input, textarea, [contenteditable="true"]')) return;
+
+ const shortcuts = keyboardShortcuts.getShortcuts();
+ const pressedKey = e.key.toLowerCase();
+ const hasShift = e.shiftKey;
+ const hasCtrl = e.ctrlKey || e.metaKey;
+ const hasAlt = e.altKey;
+
+ for (const [action, shortcut] of Object.entries(shortcuts)) {
+ if (!shortcut?.key) continue;
+ const shortcutKey = shortcut.key.toLowerCase();
+ const matches =
+ pressedKey === shortcutKey &&
+ shortcut.shift === hasShift &&
+ shortcut.ctrl === hasCtrl &&
+ shortcut.alt === hasAlt;
+
+ if (matches) {
e.preventDefault();
- trackKeyboardShortcut('Space');
- player.handlePlayPause();
- break;
- case 'arrowright':
- if (e.shiftKey) {
- trackKeyboardShortcut('Shift+Right');
- player.playNext();
- } else {
- trackKeyboardShortcut('Right');
- audioPlayer.currentTime = Math.min(audioPlayer.duration, audioPlayer.currentTime + 10);
+ const actionFn = keyActionMap[action];
+ if (actionFn) {
+ actionFn();
}
- break;
- case 'arrowleft':
- if (e.shiftKey) {
- trackKeyboardShortcut('Shift+Left');
- player.playPrev();
- } else {
- trackKeyboardShortcut('Left');
- audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 10);
- }
- break;
- case 'arrowup':
- e.preventDefault();
- trackKeyboardShortcut('Up');
- player.setVolume(player.userVolume + 0.1);
- break;
- case 'arrowdown':
- e.preventDefault();
- trackKeyboardShortcut('Down');
- player.setVolume(player.userVolume - 0.1);
- break;
- case 'm':
- trackKeyboardShortcut('M');
- audioPlayer.muted = !audioPlayer.muted;
- break;
- case 's':
- trackKeyboardShortcut('S');
- document.getElementById('shuffle-btn')?.click();
- break;
- case 'r':
- trackKeyboardShortcut('R');
- document.getElementById('repeat-btn')?.click();
- break;
- case 'q':
- trackKeyboardShortcut('Q');
- document.getElementById('queue-btn')?.click();
- break;
- case '/':
- e.preventDefault();
- trackKeyboardShortcut('/');
- document.getElementById('search-input')?.focus();
- break;
- case 'escape':
- trackKeyboardShortcut('Escape');
- document.getElementById('search-input')?.blur();
- sidePanelManager.close();
- clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
- break;
- case 'l':
- trackKeyboardShortcut('L');
- document.querySelector('.now-playing-bar .cover')?.click();
- break;
+ return;
+ }
}
});
}
@@ -378,6 +419,7 @@ document.addEventListener('DOMContentLoaded', async () => {
initializeCasting(audioPlayer, castBtn);
const ui = new UIRenderer(api, player);
+ window.monochromeUi = ui;
const scrobbler = new MultiScrobbler();
const lyricsManager = new LyricsManager(api);
@@ -2462,6 +2504,10 @@ document.addEventListener('DOMContentLoaded', async () => {
showKeyboardShortcuts();
});
+ document.getElementById('customize-shortcuts-btn')?.addEventListener('click', () => {
+ showCustomizeShortcutsModal();
+ });
+
// Font Settings
const fontSelect = document.getElementById('font-select');
if (fontSelect) {
@@ -2846,3 +2892,160 @@ function showKeyboardShortcuts() {
modal.addEventListener('click', handleClose);
modal.classList.add('active');
}
+
+function showCustomizeShortcutsModal() {
+ const modal = document.getElementById('customize-shortcuts-modal');
+ const shortcutsList = document.getElementById('shortcuts-list');
+ let recordingAction = null;
+ let recordingTimeout = null;
+
+ const shortcuts = keyboardShortcuts.getShortcuts();
+
+ const formatKey = (key) => {
+ if (!key) return 'none';
+ const keyMap = {
+ ' ': 'Space',
+ arrowup: '↑',
+ arrowdown: '↓',
+ arrowleft: '←',
+ arrowright: '→',
+ escape: 'Esc',
+ backspace: 'Backspace',
+ delete: 'Delete',
+ insert: 'Insert',
+ home: 'Home',
+ end: 'End',
+ pageup: 'Page Up',
+ pagedown: 'Page Down',
+ '[': '[',
+ ']': ']',
+ '\\': '\\',
+ tab: 'Tab',
+ enter: 'Enter',
+ capslock: 'Caps Lock',
+ shift: 'Shift',
+ control: 'Ctrl',
+ alt: 'Alt',
+ meta: 'Meta',
+ contextmenu: 'Context Menu',
+ };
+ return keyMap[key.toLowerCase()] || key.toUpperCase();
+ };
+
+ const renderShortcuts = () => {
+ shortcutsList.innerHTML = '';
+ const currentShortcuts = keyboardShortcuts.getShortcuts();
+
+ for (const [action, shortcut] of Object.entries(currentShortcuts || {})) {
+ const item = document.createElement('div');
+ item.className = 'customize-shortcut-item';
+ item.dataset.action = action;
+
+ const modifiers = [];
+ if (shortcut?.shift) modifiers.push('Shift');
+ if (shortcut?.ctrl) modifiers.push('Ctrl');
+ if (shortcut?.alt) modifiers.push('Alt');
+
+ const keyDisplay = [...modifiers, formatKey(shortcut?.key)].join(' + ');
+
+ item.innerHTML = `
+
${shortcut?.description || 'Unknown'}
+
+
${keyDisplay}
+
+
+ `;
+
+ const kbd = item.querySelector('kbd');
+ kbd.addEventListener('click', (e) => {
+ e.stopPropagation();
+ if (recordingAction === action) {
+ recordingAction = null;
+ clearTimeout(recordingTimeout);
+ } else {
+ recordingAction = action;
+ recordingTimeout = setTimeout(() => {
+ keyboardShortcuts.setShortcut(action, {
+ key: null,
+ shift: false,
+ ctrl: false,
+ alt: false,
+ description: shortcut?.description || 'Unknown',
+ });
+ recordingAction = null;
+ renderShortcuts();
+ }, 3000);
+ }
+ renderShortcuts();
+ });
+
+ const resetBtn = item.querySelector('.shortcut-btn');
+ resetBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const defaults = keyboardShortcuts.getDefaultShortcuts();
+ keyboardShortcuts.setShortcut(action, defaults[action]);
+ renderShortcuts();
+ });
+
+ shortcutsList.appendChild(item);
+ }
+ };
+
+ const handleKeyDown = (e) => {
+ if (!recordingAction) return;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ const key = e.key === ' ' ? ' ' : e.key;
+
+ if (['Control', 'Shift', 'Alt', 'Meta'].includes(e.key)) {
+ return;
+ }
+
+ keyboardShortcuts.setShortcut(recordingAction, {
+ key: key,
+ shift: e.shiftKey,
+ ctrl: e.ctrlKey || e.metaKey,
+ alt: e.altKey,
+ });
+
+ clearTimeout(recordingTimeout);
+ recordingAction = null;
+ renderShortcuts();
+ };
+
+ const closeModal = () => {
+ modal.classList.remove('active');
+ recordingAction = null;
+ clearTimeout(recordingTimeout);
+ document.removeEventListener('keydown', handleKeyDown);
+ modal.removeEventListener('click', handleClose);
+ };
+
+ const handleClose = (e) => {
+ if (
+ e.target === modal ||
+ e.target.classList.contains('close-customize-shortcuts') ||
+ e.target.id === 'close-customize-shortcuts-btn' ||
+ e.target.classList.contains('modal-overlay')
+ ) {
+ closeModal();
+ }
+ };
+
+ document.getElementById('reset-shortcuts-btn')?.addEventListener('click', () => {
+ keyboardShortcuts.resetShortcuts();
+ renderShortcuts();
+ });
+
+ document.addEventListener('keydown', handleKeyDown);
+ modal.addEventListener('click', handleClose);
+ renderShortcuts();
+ modal.classList.add('active');
+}
diff --git a/js/settings.js b/js/settings.js
index b6c21b0..2e25ced 100644
--- a/js/settings.js
+++ b/js/settings.js
@@ -35,6 +35,7 @@ import {
musicProviderSettings,
analyticsSettings,
modalSettings,
+ keyboardShortcuts,
} from './storage.js';
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
import { getButterchurnPresets } from './visualizers/butterchurn.js';
diff --git a/js/storage.js b/js/storage.js
index 618f40f..14f70cc 100644
--- a/js/storage.js
+++ b/js/storage.js
@@ -2527,3 +2527,71 @@ export const contentBlockingSettings = {
localStorage.removeItem(this.BLOCKED_ALBUMS_KEY);
},
};
+
+export const keyboardShortcuts = {
+ STORAGE_KEY: 'keyboard-shortcuts',
+
+ DEFAULT_SHORTCUTS: {
+ playPause: { key: ' ', shift: false, ctrl: false, alt: false, description: 'Play / Pause' },
+ seekForward: { key: 'arrowright', shift: false, ctrl: false, alt: false, description: 'Seek forward 10s' },
+ seekBackward: { key: 'arrowleft', shift: false, ctrl: false, alt: false, description: 'Seek backward 10s' },
+ nextTrack: { key: 'arrowright', shift: true, ctrl: false, alt: false, description: 'Next track' },
+ previousTrack: { key: 'arrowleft', shift: true, ctrl: false, alt: false, description: 'Previous track' },
+ volumeUp: { key: 'arrowup', shift: false, ctrl: false, alt: false, description: 'Volume up' },
+ volumeDown: { key: 'arrowdown', shift: false, ctrl: false, alt: false, description: 'Volume down' },
+ mute: { key: 'm', shift: false, ctrl: false, alt: false, description: 'Mute / Unmute' },
+ shuffle: { key: 's', shift: false, ctrl: false, alt: false, description: 'Toggle shuffle' },
+ repeat: { key: 'r', shift: false, ctrl: false, alt: false, description: 'Toggle repeat' },
+ queue: { key: 'q', shift: false, ctrl: false, alt: false, description: 'Open queue' },
+ lyrics: { key: 'l', shift: false, ctrl: false, alt: false, description: 'Toggle lyrics' },
+ search: { key: '/', shift: false, ctrl: false, alt: false, description: 'Focus search' },
+ escape: { key: 'escape', shift: false, ctrl: false, alt: false, description: 'Close modals' },
+ visualizerNext: { key: ']', shift: false, ctrl: false, alt: false, description: 'Next visualizer preset' },
+ visualizerPrev: { key: '[', shift: false, ctrl: false, alt: false, description: 'Previous visualizer preset' },
+ visualizerCycle: {
+ key: '\\',
+ shift: false,
+ ctrl: false,
+ alt: false,
+ description: 'Toggle visualizer auto-cycle',
+ },
+ },
+
+ getShortcuts() {
+ try {
+ const saved = localStorage.getItem(this.STORAGE_KEY);
+ if (saved) {
+ const parsed = JSON.parse(saved);
+ if (parsed && typeof parsed === 'object' && Object.keys(parsed).length > 0) {
+ return parsed;
+ }
+ }
+ } catch (e) {
+ console.warn('Failed to load keyboard shortcuts:', e);
+ }
+ return this.getDefaultShortcuts();
+ },
+
+ getDefaultShortcuts() {
+ return { ...this.DEFAULT_SHORTCUTS };
+ },
+
+ setShortcut(action, shortcut) {
+ const shortcuts = this.getShortcuts();
+ const defaults = this.DEFAULT_SHORTCUTS;
+ shortcuts[action] = {
+ ...(defaults[action] || {}),
+ ...shortcut,
+ };
+ localStorage.setItem(this.STORAGE_KEY, JSON.stringify(shortcuts));
+ },
+
+ resetShortcuts() {
+ localStorage.removeItem(this.STORAGE_KEY);
+ },
+
+ getShortcutForAction(action) {
+ const shortcuts = this.getShortcuts();
+ return shortcuts[action] || this.DEFAULT_SHORTCUTS[action];
+ },
+};
diff --git a/js/visualizers/butterchurn.js b/js/visualizers/butterchurn.js
index 0bd0b69..e040cd5 100644
--- a/js/visualizers/butterchurn.js
+++ b/js/visualizers/butterchurn.js
@@ -110,6 +110,15 @@ export class ButterchurnPreset {
this.presets = cachedPresets || {};
this.presetKeys = cachedPresetKeys || [];
+ // Shuffled queue for random mode
+ this.shuffledQueue = [];
+ this.shuffledIndex = 0;
+
+ // Generate shuffled queue if presets are already loaded
+ if (this.presetKeys.length > 0) {
+ this.generateShuffledQueue();
+ }
+
// Transition settings
this.blendProgress = 0;
this.blendDuration = 2.7; // seconds for preset transitions
@@ -119,6 +128,7 @@ export class ButterchurnPreset {
onButterchurnPresetsLoaded((presets, keys) => {
this.presets = presets;
this.presetKeys = keys;
+ this.generateShuffledQueue();
// Notify system that presets are ready (for settings dropdown)
window.dispatchEvent(new CustomEvent('butterchurn-presets-loaded'));
@@ -131,6 +141,44 @@ export class ButterchurnPreset {
}
}
+ /**
+ * Generate a shuffled queue of preset indices
+ */
+ generateShuffledQueue() {
+ const indices = this.presetKeys.map((_, i) => i);
+ // Fisher-Yates shuffle
+ for (let i = indices.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [indices[i], indices[j]] = [indices[j], indices[i]];
+ }
+ this.shuffledQueue = indices;
+ this.shuffledIndex = 0;
+ }
+
+ /**
+ * Get the current preset index based on mode
+ */
+ getCurrentIndex() {
+ const randomize = visualizerSettings.isButterchurnRandomizeEnabled();
+ if (randomize && this.shuffledQueue.length > 0) {
+ return this.shuffledQueue[this.shuffledIndex];
+ }
+ return this.currentPresetIndex;
+ }
+
+ /**
+ * Set the current preset index based on mode
+ */
+ setCurrentIndex(index) {
+ const randomize = visualizerSettings.isButterchurnRandomizeEnabled();
+ if (randomize && this.shuffledQueue.length > 0) {
+ this.shuffledIndex = this.shuffledQueue.indexOf(index);
+ if (this.shuffledIndex === -1) this.shuffledIndex = 0;
+ } else {
+ this.currentPresetIndex = index;
+ }
+ }
+
/**
* Get the preset cycle duration from settings (in milliseconds)
*/
@@ -218,7 +266,16 @@ export class ButterchurnPreset {
const randomize = visualizerSettings.isButterchurnRandomizeEnabled();
if (randomize) {
- this.currentPresetIndex = Math.floor(Math.random() * this.presetKeys.length);
+ if (this.shuffledQueue.length === 0) {
+ this.generateShuffledQueue();
+ }
+ this.shuffledIndex = (this.shuffledIndex + 1) % this.shuffledQueue.length;
+ if (this.shuffledIndex === 0) {
+ // Re-shuffle when we've gone through all presets
+ this.generateShuffledQueue();
+ this.shuffledIndex = 0;
+ }
+ this.currentPresetIndex = this.shuffledQueue[this.shuffledIndex];
} else {
this.currentPresetIndex = (this.currentPresetIndex + 1) % this.presetKeys.length;
}
@@ -253,7 +310,7 @@ export class ButterchurnPreset {
// Update current index if found
const index = this.presetKeys.indexOf(presetName);
if (index !== -1) {
- this.currentPresetIndex = index;
+ this.setCurrentIndex(index);
}
}
}
@@ -269,17 +326,85 @@ export class ButterchurnPreset {
* Get current preset name
*/
getCurrentPresetName() {
- return this.presetKeys[this.currentPresetIndex] || 'Unknown';
+ const index = this.getCurrentIndex();
+ return this.presetKeys[index] || 'Unknown';
}
/**
* Skip to next preset (manually triggered)
+ * Uses shuffled queue in random mode, sequential in normal mode
*/
nextPreset() {
- this.loadNextPreset();
+ if (!this.visualizer || this.presetKeys.length === 0) return;
+
+ const randomize = visualizerSettings.isButterchurnRandomizeEnabled();
+
+ if (randomize) {
+ this.shuffledIndex = (this.shuffledIndex + 1) % this.shuffledQueue.length;
+ if (this.shuffledIndex === 0) {
+ // Re-shuffle when we've gone through all presets
+ this.generateShuffledQueue();
+ this.shuffledIndex = 0;
+ }
+ this.currentPresetIndex = this.shuffledQueue[this.shuffledIndex];
+ } else {
+ this.currentPresetIndex = (this.currentPresetIndex + 1) % this.presetKeys.length;
+ }
+
+ const presetKey = this.presetKeys[this.currentPresetIndex];
+ const preset = this.presets[presetKey];
+
+ if (preset) {
+ try {
+ this.visualizer.loadPreset(preset, this.blendDuration);
+ } catch (error) {
+ console.warn('[Butterchurn] Failed to load preset:', presetKey, error);
+ if (this.presetKeys.length > 1) {
+ this.nextPreset();
+ }
+ }
+ }
this.lastPresetChange = performance.now();
}
+ /**
+ * Skip to previous preset (manually triggered)
+ * Uses shuffled queue in random mode, sequential in normal mode
+ */
+ prevPreset() {
+ if (!this.visualizer || this.presetKeys.length === 0) return;
+
+ const randomize = visualizerSettings.isButterchurnRandomizeEnabled();
+
+ if (randomize) {
+ this.shuffledIndex = (this.shuffledIndex - 1 + this.shuffledQueue.length) % this.shuffledQueue.length;
+ this.currentPresetIndex = this.shuffledQueue[this.shuffledIndex];
+ } else {
+ this.currentPresetIndex = (this.currentPresetIndex - 1 + this.presetKeys.length) % this.presetKeys.length;
+ }
+
+ const presetKey = this.presetKeys[this.currentPresetIndex];
+ const preset = this.presets[presetKey];
+
+ if (preset) {
+ try {
+ this.visualizer.loadPreset(preset, this.blendDuration);
+ } catch (error) {
+ console.warn('[Butterchurn] Failed to load preset:', presetKey, error);
+ }
+ }
+ this.lastPresetChange = performance.now();
+ }
+
+ /**
+ * Toggle auto-cycle on/off
+ */
+ toggleCycle() {
+ const current = visualizerSettings.isButterchurnCycleEnabled();
+ visualizerSettings.setButterchurnCycleEnabled(!current);
+ return !current;
+ }
+
/**
* Resize handler
*/
diff --git a/styles.css b/styles.css
index 4f2fa45..e0b0796 100644
--- a/styles.css
+++ b/styles.css
@@ -4635,6 +4635,116 @@ input:checked + .slider::before {
border-bottom: none;
}
+.customize-shortcuts-content {
+ padding: 1rem;
+}
+
+.shortcut-hint {
+ color: var(--muted-foreground);
+ font-size: 0.85rem;
+ margin-bottom: 1rem;
+}
+
+.shortcuts-list {
+ /* list scrolls with modal */
+}
+
+.customize-shortcut-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 0;
+ border-bottom: 1px solid var(--border);
+}
+
+.customize-shortcut-item:last-child {
+ border-bottom: none;
+}
+
+.customize-shortcut-item .shortcut-description {
+ flex: 1;
+}
+
+.customize-shortcut-item .shortcut-key {
+ display: flex;
+ gap: 0.25rem;
+ align-items: center;
+}
+
+.customize-shortcut-item kbd {
+ background: var(--secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 0.25rem 0.5rem;
+ font-size: 0.8rem;
+ font-family: inherit;
+ min-width: 28px;
+ text-align: center;
+}
+
+.customize-shortcut-item kbd.recording {
+ background: var(--primary);
+ color: var(--primary-foreground);
+ border-color: var(--primary);
+ animation: pulse 1s infinite;
+}
+
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.7;
+ }
+}
+
+.customize-shortcut-item .shortcut-btn {
+ background: transparent;
+ border: none;
+ color: var(--muted-foreground);
+ cursor: pointer;
+ padding: 0.25rem;
+ margin-left: 0.5rem;
+ opacity: 0;
+ transition: opacity var(--transition);
+}
+
+.customize-shortcut-item:hover .shortcut-btn {
+ opacity: 1;
+}
+
+.customize-shortcut-item .shortcut-btn:hover {
+ color: var(--foreground);
+}
+
+.customize-shortcuts-actions {
+ display: flex;
+ justify-content: space-between;
+ padding: 1rem;
+ border-top: 1px solid var(--border);
+}
+
+.close-customize-shortcuts {
+ background: transparent;
+ border: none;
+ color: var(--muted-foreground);
+ font-size: 1.5rem;
+ cursor: pointer;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius);
+ transition: all var(--transition);
+}
+
+.close-customize-shortcuts:hover {
+ background: var(--secondary);
+ color: var(--foreground);
+}
+
#playlist-detail-description,
#mix-detail-description {
color: var(--foreground);