Make size changes a slider

This commit is contained in:
Justin Deal 2025-05-03 19:05:21 -07:00
parent 750fe5c629
commit f51fa741cc
4 changed files with 329 additions and 37 deletions

View File

@ -12,12 +12,36 @@
// Register the styleControls component // Register the styleControls component
Alpine.data('styleControls', () => ({ Alpine.data('styleControls', () => ({
iconSize: 'medium', iconSize: 'medium',
iconSizeValue: 2, // Slider value: 1=small, 2=medium, 3=large
viewMode: 'grid', viewMode: 'grid',
displayMode: 'both', displayMode: 'both',
init() { init() {
// Load preferences from localStorage // Load preferences from localStorage
this.loadPreferences(); this.loadPreferences();
// Set initial iconSizeValue based on iconSize
this.updateSliderFromIconSize();
// Listen for events from searchServices component
window.addEventListener('styleControls:updateIconSize', (e) => {
if (e.detail && e.detail.size) {
this.iconSize = e.detail.size;
this.updateSliderFromIconSize();
}
});
window.addEventListener('styleControls:updateViewMode', (e) => {
if (e.detail && e.detail.mode) {
this.viewMode = e.detail.mode;
}
});
window.addEventListener('styleControls:updateDisplayMode', (e) => {
if (e.detail && e.detail.mode) {
this.displayMode = e.detail.mode;
}
});
}, },
loadPreferences() { loadPreferences() {
@ -26,7 +50,7 @@
// Load icon size // Load icon size
const savedIconSize = localStorage.getItem('services-icon-size'); const savedIconSize = localStorage.getItem('services-icon-size');
if (savedIconSize) { if (savedIconSize) {
this.setIconSize(savedIconSize); this.setIconSize(savedIconSize, false); // Don't update slider yet
} }
// Load view mode // Load view mode
@ -40,6 +64,9 @@
if (savedDisplayMode) { if (savedDisplayMode) {
this.displayMode = savedDisplayMode; this.displayMode = savedDisplayMode;
} }
// Update slider value based on loaded icon size
this.updateSliderFromIconSize();
} catch (e) { } catch (e) {
console.error('Error loading preferences:', e); console.error('Error loading preferences:', e);
} }
@ -58,19 +85,158 @@
} }
}, },
setIconSize(size) { // Convert between iconSize string and iconSizeValue number
updateSliderFromIconSize() {
if (this.iconSize === 'small') {
this.iconSizeValue = 1;
} else if (this.iconSize === 'medium') {
this.iconSizeValue = 2;
} else if (this.iconSize === 'large') {
this.iconSizeValue = 3;
}
},
// Update iconSize based on slider value
updateIconSizeFromSlider() {
console.log('Slider value changed:', this.iconSizeValue);
const value = parseInt(this.iconSizeValue);
let size;
if (value === 1) {
size = 'small';
} else if (value === 2) {
size = 'medium';
} else if (value === 3) {
size = 'large';
} else {
size = 'medium'; // Default fallback
}
console.log('Setting icon size to:', size);
this.setIconSize(size);
// Apply the size directly to app-list if it exists
this.applyIconSizeDirectly(size);
},
// Apply icon size directly to elements
applyIconSizeDirectly(size) {
// Try to find app-list element
const appList = document.getElementById('app-list');
if (appList) {
console.log('Found app-list, applying size:', size);
// Remove existing size classes
appList.classList.remove('icon-size-small', 'icon-size-medium', 'icon-size-large');
// Add the new size class
appList.classList.add(`icon-size-${size}`);
} else {
console.log('app-list element not found');
// Try to apply to all service cards directly
const cards = document.querySelectorAll('.app-card, .service-card');
if (cards.length > 0) {
console.log('Found', cards.length, 'cards, applying size:', size);
cards.forEach(card => {
// Remove existing size classes
card.classList.remove('icon-size-small', 'icon-size-medium', 'icon-size-large');
// Add the new size class
card.classList.add(`icon-size-${size}`);
});
} else {
console.log('No service cards found');
}
}
// Dispatch event to notify searchServices component
window.dispatchEvent(new CustomEvent('searchServices:setIconSize', {
detail: { size }
}));
},
setIconSize(size, updateSlider = true) {
console.log('setIconSize called with:', size);
this.iconSize = size; this.iconSize = size;
// Update slider value if requested
if (updateSlider) {
this.updateSliderFromIconSize();
}
this.savePreferences(); this.savePreferences();
// Apply the size directly
this.applyIconSizeDirectly(size);
}, },
toggleViewMode() { toggleViewMode() {
this.viewMode = this.viewMode === 'grid' ? 'list' : 'grid'; this.viewMode = this.viewMode === 'grid' ? 'list' : 'grid';
this.savePreferences(); this.savePreferences();
// Apply view mode directly
this.applyViewModeDirectly(this.viewMode);
// Dispatch event to notify searchServices component
window.dispatchEvent(new CustomEvent('searchServices:setViewMode', {
detail: { mode: this.viewMode }
}));
},
applyViewModeDirectly(mode) {
console.log('Applying view mode directly:', mode);
const appList = document.getElementById('app-list');
if (appList) {
// Remove existing view mode classes
appList.classList.remove('view-mode-grid', 'view-mode-list');
// Add the new view mode class
appList.classList.add(`view-mode-${mode}`);
// Update all category sections
document.querySelectorAll('.category-section').forEach(section => {
const gridContainer = section.querySelector('.grid');
if (gridContainer) {
// Update grid classes based on view mode
if (mode === 'grid') {
gridContainer.classList.remove('grid-cols-1');
gridContainer.classList.add('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
} else {
gridContainer.classList.remove('grid-cols-2', 'sm:grid-cols-3', 'lg:grid-cols-4');
gridContainer.classList.add('grid-cols-1');
}
}
});
}
}, },
setDisplayMode(mode) { setDisplayMode(mode) {
this.displayMode = mode; this.displayMode = mode;
this.savePreferences(); this.savePreferences();
// Apply display mode directly
this.applyDisplayModeDirectly(mode);
// Dispatch event to notify searchServices component
window.dispatchEvent(new CustomEvent('searchServices:setDisplayMode', {
detail: { mode }
}));
},
applyDisplayModeDirectly(mode) {
console.log('Applying display mode directly:', mode);
const appList = document.getElementById('app-list');
if (appList) {
// Remove existing display mode classes
appList.classList.remove('display-both', 'display-image-only', 'display-name-only');
// Add the new display mode class
if (mode === 'image') {
appList.classList.add('display-image-only');
} else if (mode === 'name') {
appList.classList.add('display-name-only');
} else {
appList.classList.add('display-both');
}
}
} }
})); }));
@ -106,12 +272,57 @@
// Listen for window resize events to optimize layout // Listen for window resize events to optimize layout
this.setupResizeListener(); this.setupResizeListener();
// Listen for events from styleControls component
this.setupStyleControlsListeners();
// Set loading to false after initialization // Set loading to false after initialization
setTimeout(() => { setTimeout(() => {
this.loading = false; this.loading = false;
}, 300); }, 300);
}, },
// Setup listeners for styleControls events
setupStyleControlsListeners() {
// Listen for icon size changes
window.addEventListener('searchServices:setIconSize', (e) => {
if (e.detail && e.detail.size) {
console.log('searchServices received icon size event:', e.detail.size);
this.setIconSize(e.detail.size);
// Notify styleControls component of the change
window.dispatchEvent(new CustomEvent('styleControls:updateIconSize', {
detail: { size: e.detail.size }
}));
}
});
// Listen for view mode changes
window.addEventListener('searchServices:setViewMode', (e) => {
if (e.detail && e.detail.mode) {
console.log('searchServices received view mode event:', e.detail.mode);
this.setViewMode(e.detail.mode);
// Notify styleControls component of the change
window.dispatchEvent(new CustomEvent('styleControls:updateViewMode', {
detail: { mode: e.detail.mode }
}));
}
});
// Listen for display mode changes
window.addEventListener('searchServices:setDisplayMode', (e) => {
if (e.detail && e.detail.mode) {
console.log('searchServices received display mode event:', e.detail.mode);
this.setDisplayMode(e.detail.mode);
// Notify styleControls component of the change
window.dispatchEvent(new CustomEvent('styleControls:updateDisplayMode', {
detail: { mode: e.detail.mode }
}));
}
});
},
setupWatchers() { setupWatchers() {
this.$watch('searchQuery', (query) => { this.$watch('searchQuery', (query) => {
// Debounce search for better performance // Debounce search for better performance

View File

@ -16,40 +16,18 @@ const {
{showSizeSelector && ( {showSizeSelector && (
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm zag-text-muted hidden sm:inline">Size:</span> <span class="text-sm zag-text-muted hidden sm:inline">Size:</span>
<div class="size-selector flex items-center gap-1 border-2 border-solid zag-border-b rounded-lg p-1 zag-bg zag-transition"> <div class="size-selector flex flex-col items-center border-2 border-solid zag-border-b rounded-lg p-2 zag-bg zag-transition">
<button <input
@click="setIconSize('small')" type="range"
:class="iconSize === 'small' ? 'active-size' : 'inactive-size'" min="1"
class="p-1.5 transition-all rounded-md focus:outline-none focus:ring-2 focus:ring-current" max="3"
aria-label="Small icons" step="1"
title="Small icons (Alt+1)" x-model="iconSizeValue"
> @input="updateIconSizeFromSlider()"
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> class="size-slider w-full"
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> aria-label="Icon size slider"
</svg> title="Adjust icon size (Alt+1: Small, Alt+2: Medium, Alt+3: Large)"
</button> />
<button
@click="setIconSize('medium')"
:class="iconSize === 'medium' ? 'active-size' : 'inactive-size'"
class="p-1.5 transition-all rounded-md focus:outline-none focus:ring-2 focus:ring-current"
aria-label="Medium icons"
title="Medium icons (Alt+2)"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
</svg>
</button>
<button
@click="setIconSize('large')"
:class="iconSize === 'large' ? 'active-size' : 'inactive-size'"
class="p-1.5 transition-all rounded-md focus:outline-none focus:ring-2 focus:ring-current"
aria-label="Large icons"
title="Large icons (Alt+3)"
>
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
</svg>
</button>
</div> </div>
</div> </div>
)} )}
@ -163,4 +141,85 @@ const {
color: var(--color-zag-light); color: var(--color-zag-light);
} }
} }
/* Slider styles */
.size-slider {
-webkit-appearance: none;
appearance: none;
height: 6px;
border-radius: 3px;
background: var(--color-zag-light-muted);
outline: none;
cursor: pointer;
transition: all 0.2s;
:where(.dark, .dark *) & {
background: var(--color-zag-dark-muted);
}
}
/* Slider thumb */
.size-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--color-zag-dark);
cursor: pointer;
border: 2px solid var(--color-zag-light);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
transition: all 0.2s;
:where(.dark, .dark *) & {
background: var(--color-zag-light);
border: 2px solid var(--color-zag-dark);
}
}
.size-slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--color-zag-dark);
cursor: pointer;
border: 2px solid var(--color-zag-light);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
transition: all 0.2s;
:where(.dark, .dark *) & {
background: var(--color-zag-light);
border: 2px solid var(--color-zag-dark);
}
}
/* Hover state */
.size-slider:hover::-webkit-slider-thumb {
transform: scale(1.1);
}
.size-slider:hover::-moz-range-thumb {
transform: scale(1.1);
}
/* Focus state */
.size-slider:focus {
outline: none;
}
.size-slider:focus::-webkit-slider-thumb {
box-shadow: 0 0 0 3px var(--color-zag-light), 0 0 0 5px var(--color-zag-dark-muted);
:where(.dark, .dark *) & {
box-shadow: 0 0 0 3px var(--color-zag-dark), 0 0 0 5px var(--color-zag-light-muted);
}
}
.size-slider:focus::-moz-range-thumb {
box-shadow: 0 0 0 3px var(--color-zag-light), 0 0 0 5px var(--color-zag-dark-muted);
:where(.dark, .dark *) & {
box-shadow: 0 0 0 3px var(--color-zag-dark), 0 0 0 5px var(--color-zag-light-muted);
}
}
</style> </style>

View File

@ -146,7 +146,7 @@ const webpageData = {
<!-- Search and controls container --> <!-- Search and controls container -->
<div class="mb-4 pt-0"> <div class="mb-4 pt-0">
<!-- Style controls in a centered row above search --> <!-- Style controls in a centered row above search -->
<div class="w-full flex justify-center mb-4"> <div class="w-full flex justify-center mb-4" x-data="styleControls">
<StyleControls /> <StyleControls />
</div> </div>

View File

@ -217,6 +217,28 @@
} }
@layer components { @layer components {
/* Icon size classes for service cards */
.icon-size-small .app-card .app-icon,
.icon-size-small .service-card .service-icon {
width: 32px;
height: 32px;
transition: width 0.3s ease, height 0.3s ease;
}
.icon-size-medium .app-card .app-icon,
.icon-size-medium .service-card .service-icon {
width: 48px;
height: 48px;
transition: width 0.3s ease, height 0.3s ease;
}
.icon-size-large .app-card .app-icon,
.icon-size-large .service-card .service-icon {
width: 64px;
height: 64px;
transition: width 0.3s ease, height 0.3s ease;
}
/* Interactive element base transitions */ /* Interactive element base transitions */
.zag-interactive { .zag-interactive {
position: relative; position: relative;