mirror of
https://github.com/isledecomp/isle.pizza.git
synced 2026-03-01 14:27: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;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-tab-btn:hover {
|
.config-tab-btn:hover {
|
||||||
|
|||||||
@ -4,6 +4,13 @@
|
|||||||
|
|
||||||
export let gap = 10;
|
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 trackRef;
|
||||||
let canScrollLeft = false;
|
let canScrollLeft = false;
|
||||||
let canScrollRight = false;
|
let canScrollRight = false;
|
||||||
@ -14,6 +21,9 @@
|
|||||||
// Exposed so parent can check if a drag occurred (to prevent click handling)
|
// Exposed so parent can check if a drag occurred (to prevent click handling)
|
||||||
export let hasDragged = false;
|
export let hasDragged = false;
|
||||||
|
|
||||||
|
$: leftDisabled = hasPrev !== undefined ? !hasPrev : !canScrollLeft;
|
||||||
|
$: rightDisabled = hasNext !== undefined ? !hasNext : !canScrollRight;
|
||||||
|
|
||||||
function updateArrows() {
|
function updateArrows() {
|
||||||
if (!trackRef) return;
|
if (!trackRef) return;
|
||||||
const { scrollLeft, scrollWidth, clientWidth } = trackRef;
|
const { scrollLeft, scrollWidth, clientWidth } = trackRef;
|
||||||
@ -21,13 +31,41 @@
|
|||||||
canScrollRight = scrollLeft + clientWidth < scrollWidth - 1;
|
canScrollRight = scrollLeft + clientWidth < scrollWidth - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollLeft() {
|
function handleLeft() {
|
||||||
|
if (onPrev) {
|
||||||
|
onPrev();
|
||||||
|
} else {
|
||||||
trackRef?.scrollBy({ left: -200, behavior: 'smooth' });
|
trackRef?.scrollBy({ left: -200, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function scrollRight() {
|
function handleRight() {
|
||||||
|
if (onNext) {
|
||||||
|
onNext();
|
||||||
|
} else {
|
||||||
trackRef?.scrollBy({ left: 200, behavior: 'smooth' });
|
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) {
|
function handleMouseDown(e) {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
@ -55,24 +93,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(e) {
|
function handleClick(e) {
|
||||||
// Find the direct child element that was clicked
|
|
||||||
const clickedCard = e.target.closest('.carousel-track > *');
|
const clickedCard = e.target.closest('.carousel-track > *');
|
||||||
if (!clickedCard || hasDragged) return;
|
if (!clickedCard || hasDragged) return;
|
||||||
|
scrollChildIntoView(clickedCard);
|
||||||
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' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@ -86,7 +109,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="carousel">
|
<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 -->
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions a11y_no_noninteractive_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="carousel-track"
|
class="carousel-track"
|
||||||
@ -103,7 +126,7 @@
|
|||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<NavButton direction="right" onclick={scrollRight} disabled={!canScrollRight} />
|
<NavButton direction="right" onclick={handleRight} disabled={rightDisabled} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -53,6 +53,46 @@
|
|||||||
|
|
||||||
// Carousel state (bound from Carousel component)
|
// Carousel state (bound from Carousel component)
|
||||||
let carouselHasDragged = false;
|
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 () => {
|
onMount(async () => {
|
||||||
await loadSlots();
|
await loadSlots();
|
||||||
@ -278,7 +318,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="config-main">
|
<div class="config-main">
|
||||||
{#if loading || error || existingSlots.length > 0}
|
{#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}
|
{#if loading}
|
||||||
<span class="save-status-text">Loading save files...</span>
|
<span class="save-status-text">Loading save files...</span>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
@ -326,7 +366,8 @@
|
|||||||
|
|
||||||
{#if currentSlot && currentSlot.exists}
|
{#if currentSlot && currentSlot.exists}
|
||||||
<div class="config-tabs">
|
<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}
|
{#each saveTabs as tab}
|
||||||
<button
|
<button
|
||||||
class="config-tab-btn"
|
class="config-tab-btn"
|
||||||
@ -336,6 +377,7 @@
|
|||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Player Tab -->
|
<!-- Player Tab -->
|
||||||
@ -607,6 +649,42 @@
|
|||||||
image-rendering: pixelated;
|
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) {
|
@media (max-width: 400px) {
|
||||||
.name-slot {
|
.name-slot {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user