mirror of
https://github.com/isledecomp/isle.pizza.git
synced 2026-03-01 06:17:38 +00:00
Add carousel tabs and selection-based nav to save editor
Wrap save editor tab buttons in a Carousel to prevent overflow on desktop. Carousel nav buttons now cycle through the selected item (save slot or tab) instead of scrolling, with auto-scroll-into-view. On mobile, tabs reflow with flex-wrap as before.
This commit is contained in:
parent
e1b9f2e696
commit
124b7a1370
@ -505,6 +505,8 @@ body {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.config-tab-btn:hover {
|
||||
|
||||
@ -4,6 +4,13 @@
|
||||
|
||||
export let gap = 10;
|
||||
|
||||
// Optional selection-based navigation. When provided, nav buttons
|
||||
// change the selected item instead of scrolling.
|
||||
export let onPrev = null;
|
||||
export let onNext = null;
|
||||
export let hasPrev = undefined;
|
||||
export let hasNext = undefined;
|
||||
|
||||
let trackRef;
|
||||
let canScrollLeft = false;
|
||||
let canScrollRight = false;
|
||||
@ -14,6 +21,9 @@
|
||||
// Exposed so parent can check if a drag occurred (to prevent click handling)
|
||||
export let hasDragged = false;
|
||||
|
||||
$: leftDisabled = hasPrev !== undefined ? !hasPrev : !canScrollLeft;
|
||||
$: rightDisabled = hasNext !== undefined ? !hasNext : !canScrollRight;
|
||||
|
||||
function updateArrows() {
|
||||
if (!trackRef) return;
|
||||
const { scrollLeft, scrollWidth, clientWidth } = trackRef;
|
||||
@ -21,13 +31,41 @@
|
||||
canScrollRight = scrollLeft + clientWidth < scrollWidth - 1;
|
||||
}
|
||||
|
||||
function scrollLeft() {
|
||||
function handleLeft() {
|
||||
if (onPrev) {
|
||||
onPrev();
|
||||
} else {
|
||||
trackRef?.scrollBy({ left: -200, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
function scrollRight() {
|
||||
function handleRight() {
|
||||
if (onNext) {
|
||||
onNext();
|
||||
} else {
|
||||
trackRef?.scrollBy({ left: 200, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
function scrollChildIntoView(child) {
|
||||
const trackRect = trackRef.getBoundingClientRect();
|
||||
const childRect = child.getBoundingClientRect();
|
||||
const isFullyVisible = childRect.left >= trackRect.left && childRect.right <= trackRect.right;
|
||||
|
||||
if (!isFullyVisible) {
|
||||
const scrollTarget = childRect.left < trackRect.left
|
||||
? trackRef.scrollLeft - (trackRect.left - childRect.left)
|
||||
: trackRef.scrollLeft + (childRect.right - trackRect.right);
|
||||
trackRef.scrollTo({ left: scrollTarget, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
/** Scroll the nth child (0-indexed) into view */
|
||||
export function scrollToIndex(index) {
|
||||
if (!trackRef) return;
|
||||
const child = trackRef.children[index];
|
||||
if (child) scrollChildIntoView(child);
|
||||
}
|
||||
|
||||
function handleMouseDown(e) {
|
||||
if (e.button !== 0) return;
|
||||
@ -55,24 +93,9 @@
|
||||
}
|
||||
|
||||
function handleClick(e) {
|
||||
// Find the direct child element that was clicked
|
||||
const clickedCard = e.target.closest('.carousel-track > *');
|
||||
if (!clickedCard || hasDragged) return;
|
||||
|
||||
const trackRect = trackRef.getBoundingClientRect();
|
||||
const cardRect = clickedCard.getBoundingClientRect();
|
||||
|
||||
// Check if card is fully visible
|
||||
const isFullyVisible = cardRect.left >= trackRect.left && cardRect.right <= trackRect.right;
|
||||
|
||||
if (!isFullyVisible) {
|
||||
// Scroll to bring card into view
|
||||
const scrollLeft = cardRect.left < trackRect.left
|
||||
? trackRef.scrollLeft - (trackRect.left - cardRect.left)
|
||||
: trackRef.scrollLeft + (cardRect.right - trackRect.right);
|
||||
|
||||
trackRef.scrollTo({ left: scrollLeft, behavior: 'smooth' });
|
||||
}
|
||||
scrollChildIntoView(clickedCard);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@ -86,7 +109,7 @@
|
||||
</script>
|
||||
|
||||
<div class="carousel">
|
||||
<NavButton direction="left" onclick={scrollLeft} disabled={!canScrollLeft} />
|
||||
<NavButton direction="left" onclick={handleLeft} disabled={leftDisabled} />
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="carousel-track"
|
||||
@ -103,7 +126,7 @@
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<NavButton direction="right" onclick={scrollRight} disabled={!canScrollRight} />
|
||||
<NavButton direction="right" onclick={handleRight} disabled={rightDisabled} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@ -53,6 +53,46 @@
|
||||
|
||||
// Carousel state (bound from Carousel component)
|
||||
let carouselHasDragged = false;
|
||||
let slotCarousel;
|
||||
let tabCarousel;
|
||||
|
||||
// Slot carousel navigation
|
||||
$: selectedSlotIndex = existingSlots.findIndex(s => s.slotNumber === selectedSlot);
|
||||
$: hasSlotPrev = selectedSlotIndex > 0;
|
||||
$: hasSlotNext = selectedSlotIndex >= 0 && selectedSlotIndex < existingSlots.length - 1;
|
||||
|
||||
function selectPrevSlot() {
|
||||
if (!hasSlotPrev) return;
|
||||
const newIdx = selectedSlotIndex - 1;
|
||||
handleSlotSelect(existingSlots[newIdx].slotNumber);
|
||||
slotCarousel?.scrollToIndex(newIdx);
|
||||
}
|
||||
|
||||
function selectNextSlot() {
|
||||
if (!hasSlotNext) return;
|
||||
const newIdx = selectedSlotIndex + 1;
|
||||
handleSlotSelect(existingSlots[newIdx].slotNumber);
|
||||
slotCarousel?.scrollToIndex(newIdx);
|
||||
}
|
||||
|
||||
// Tab carousel navigation
|
||||
$: activeTabIndex = saveTabs.findIndex(t => t.id === activeTab);
|
||||
$: hasTabPrev = activeTabIndex > 0;
|
||||
$: hasTabNext = activeTabIndex < saveTabs.length - 1;
|
||||
|
||||
function selectPrevTab() {
|
||||
if (!hasTabPrev) return;
|
||||
const newIdx = activeTabIndex - 1;
|
||||
switchTab(saveTabs[newIdx]);
|
||||
tabCarousel?.scrollToIndex(newIdx);
|
||||
}
|
||||
|
||||
function selectNextTab() {
|
||||
if (!hasTabNext) return;
|
||||
const newIdx = activeTabIndex + 1;
|
||||
switchTab(saveTabs[newIdx]);
|
||||
tabCarousel?.scrollToIndex(newIdx);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await loadSlots();
|
||||
@ -278,7 +318,7 @@
|
||||
</div>
|
||||
<div class="config-main">
|
||||
{#if loading || error || existingSlots.length > 0}
|
||||
<Carousel bind:hasDragged={carouselHasDragged}>
|
||||
<Carousel bind:this={slotCarousel} bind:hasDragged={carouselHasDragged} onPrev={selectPrevSlot} onNext={selectNextSlot} hasPrev={hasSlotPrev} hasNext={hasSlotNext}>
|
||||
{#if loading}
|
||||
<span class="save-status-text">Loading save files...</span>
|
||||
{:else if error}
|
||||
@ -326,7 +366,8 @@
|
||||
|
||||
{#if currentSlot && currentSlot.exists}
|
||||
<div class="config-tabs">
|
||||
<div class="config-tab-buttons">
|
||||
<div class="config-tab-buttons tab-carousel-wrapper">
|
||||
<Carousel bind:this={tabCarousel} gap={5} onPrev={selectPrevTab} onNext={selectNextTab} hasPrev={hasTabPrev} hasNext={hasTabNext}>
|
||||
{#each saveTabs as tab}
|
||||
<button
|
||||
class="config-tab-btn"
|
||||
@ -336,6 +377,7 @@
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
<!-- Player Tab -->
|
||||
@ -607,6 +649,42 @@
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.tab-carousel-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tab-carousel-wrapper :global(.carousel) {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tab-carousel-wrapper :global(.carousel-track) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tab-carousel-wrapper :global(.carousel-track.dragging) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tab-carousel-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.tab-carousel-wrapper :global(.carousel) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.tab-carousel-wrapper :global(.nav-btn) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-carousel-wrapper :global(.carousel-track) {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.name-slot {
|
||||
width: 32px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user