feat: reposition solver controls to a dropdown
Moved the Kociemba/Beginner solve options into a sleek dropdown menu positioned above the Scramble button on the left side of the screen. This ensures the solver controls no longer obstruct the programmatic move queue at the bottom.
This commit is contained in:
@@ -1,5 +1,31 @@
|
||||
<script setup>
|
||||
const emit = defineEmits(['move', 'scramble'])
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
const emit = defineEmits(["move", "scramble", "solve"]);
|
||||
|
||||
const showSolveDropdown = ref(false);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
showSolveDropdown.value = !showSolveDropdown.value;
|
||||
};
|
||||
|
||||
const triggerSolve = (method) => {
|
||||
showSolveDropdown.value = false;
|
||||
emit("solve", method);
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
const closeDropdown = (e) => {
|
||||
if (!e.target.closest(".solve-dropdown-wrapper")) {
|
||||
showSolveDropdown.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", closeDropdown);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", closeDropdown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -11,14 +37,26 @@ const emit = defineEmits(['move', 'scramble'])
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'L')">L</button>
|
||||
</div>
|
||||
<div class="controls-row">
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'U-prime')">U'</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'D-prime')">D'</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'L-prime')">L'</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'U-prime')">
|
||||
U'
|
||||
</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'D-prime')">
|
||||
D'
|
||||
</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'L-prime')">
|
||||
L'
|
||||
</button>
|
||||
</div>
|
||||
<div class="controls-row">
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'U2')">U2</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'D2')">D2</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'L2')">L2</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'U2')">
|
||||
U2
|
||||
</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'D2')">
|
||||
D2
|
||||
</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'L2')">
|
||||
L2
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,20 +67,48 @@ const emit = defineEmits(['move', 'scramble'])
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'B')">B</button>
|
||||
</div>
|
||||
<div class="controls-row">
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'R-prime')">R'</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'F-prime')">F'</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'B-prime')">B'</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'R-prime')">
|
||||
R'
|
||||
</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'F-prime')">
|
||||
F'
|
||||
</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'B-prime')">
|
||||
B'
|
||||
</button>
|
||||
</div>
|
||||
<div class="controls-row">
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'R2')">R2</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'F2')">F2</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'B2')">B2</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'R2')">
|
||||
R2
|
||||
</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'F2')">
|
||||
F2
|
||||
</button>
|
||||
<button class="btn-neon move-btn" @click="emit('move', 'B2')">
|
||||
B2
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-neon move-btn scramble-btn" @click="emit('scramble')">
|
||||
Scramble
|
||||
</button>
|
||||
<div class="bottom-left-controls">
|
||||
<div class="solve-dropdown-wrapper">
|
||||
<button class="btn-neon move-btn solve-btn" @click="toggleDropdown">
|
||||
Solve ▾
|
||||
</button>
|
||||
<div v-if="showSolveDropdown" class="solve-dropdown-menu">
|
||||
<button class="dropdown-item" @click="triggerSolve('kociemba')">
|
||||
Kociemba (Optimal)
|
||||
</button>
|
||||
<button class="dropdown-item" @click="triggerSolve('beginner')">
|
||||
Beginner (Human)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-neon move-btn scramble-btn" @click="emit('scramble')">
|
||||
Scramble
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -77,11 +143,58 @@ const emit = defineEmits(['move', 'scramble'])
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.scramble-btn {
|
||||
.bottom-left-controls {
|
||||
position: absolute;
|
||||
bottom: 72px;
|
||||
left: 24px;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.solve-dropdown-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.solve-dropdown-menu {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
margin-bottom: 8px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 180px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,96 +1,99 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
moves: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['reset', 'copy', 'add-moves', 'open-add-modal'])
|
||||
const emit = defineEmits(["reset", "copy", "add-moves", "open-add-modal"]);
|
||||
|
||||
const MIN_MOVES_COLUMN_GAP = 6
|
||||
const MIN_MOVES_COLUMN_GAP = 6;
|
||||
|
||||
const movesHistoryEl = ref(null)
|
||||
const samplePillEl = ref(null)
|
||||
const movesPerRow = ref(0)
|
||||
const movesColumnGap = ref(MIN_MOVES_COLUMN_GAP)
|
||||
const movesHistoryEl = ref(null);
|
||||
const samplePillEl = ref(null);
|
||||
const movesPerRow = ref(0);
|
||||
const movesColumnGap = ref(MIN_MOVES_COLUMN_GAP);
|
||||
|
||||
const displayMoves = computed(() => props.moves || [])
|
||||
const displayMoves = computed(() => props.moves || []);
|
||||
|
||||
const moveRows = computed(() => {
|
||||
const perRow = movesPerRow.value || displayMoves.value.length || 1
|
||||
const rows = []
|
||||
const all = displayMoves.value
|
||||
const perRow = movesPerRow.value || displayMoves.value.length || 1;
|
||||
const rows = [];
|
||||
const all = displayMoves.value;
|
||||
for (let i = 0; i < all.length; i += perRow) {
|
||||
rows.push(all.slice(i, i + perRow))
|
||||
rows.push(all.slice(i, i + perRow));
|
||||
}
|
||||
return rows
|
||||
})
|
||||
return rows;
|
||||
});
|
||||
|
||||
const hasMoves = computed(() => displayMoves.value.length > 0)
|
||||
const hasMoves = computed(() => displayMoves.value.length > 0);
|
||||
|
||||
const copyQueueToClipboard = () => {
|
||||
emit('copy')
|
||||
}
|
||||
emit("copy");
|
||||
};
|
||||
|
||||
const resetQueue = () => {
|
||||
emit('reset')
|
||||
}
|
||||
emit("reset");
|
||||
};
|
||||
|
||||
const setSamplePill = (el) => {
|
||||
if (el && !samplePillEl.value) {
|
||||
samplePillEl.value = el
|
||||
samplePillEl.value = el;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const recalcMovesLayout = () => {
|
||||
const container = movesHistoryEl.value
|
||||
const pill = samplePillEl.value
|
||||
if (!container || !pill) return
|
||||
const container = movesHistoryEl.value;
|
||||
const pill = samplePillEl.value;
|
||||
if (!container || !pill) return;
|
||||
|
||||
const containerWidth = container.clientWidth
|
||||
const pillWidth = pill.offsetWidth
|
||||
if (pillWidth <= 0) return
|
||||
const containerWidth = container.clientWidth;
|
||||
const pillWidth = pill.offsetWidth;
|
||||
if (pillWidth <= 0) return;
|
||||
|
||||
const totalWidth = (cols) => {
|
||||
if (cols <= 0) return 0
|
||||
if (cols === 1) return pillWidth
|
||||
return cols * pillWidth + (cols - 1) * MIN_MOVES_COLUMN_GAP
|
||||
}
|
||||
if (cols <= 0) return 0;
|
||||
if (cols === 1) return pillWidth;
|
||||
return cols * pillWidth + (cols - 1) * MIN_MOVES_COLUMN_GAP;
|
||||
};
|
||||
|
||||
let cols = Math.floor((containerWidth + MIN_MOVES_COLUMN_GAP) / (pillWidth + MIN_MOVES_COLUMN_GAP))
|
||||
if (cols < 1) cols = 1
|
||||
let cols = Math.floor(
|
||||
(containerWidth + MIN_MOVES_COLUMN_GAP) /
|
||||
(pillWidth + MIN_MOVES_COLUMN_GAP),
|
||||
);
|
||||
if (cols < 1) cols = 1;
|
||||
while (cols > 1 && totalWidth(cols) > containerWidth) {
|
||||
cols -= 1
|
||||
cols -= 1;
|
||||
}
|
||||
|
||||
let gap = 0
|
||||
let gap = 0;
|
||||
if (cols > 1) {
|
||||
gap = (containerWidth - cols * pillWidth) / (cols - 1)
|
||||
gap = (containerWidth - cols * pillWidth) / (cols - 1);
|
||||
}
|
||||
|
||||
movesPerRow.value = cols
|
||||
movesColumnGap.value = gap
|
||||
}
|
||||
movesPerRow.value = cols;
|
||||
movesColumnGap.value = gap;
|
||||
};
|
||||
|
||||
const openAddModal = () => {
|
||||
emit('open-add-modal')
|
||||
}
|
||||
emit("open-add-modal");
|
||||
};
|
||||
|
||||
watch(displayMoves, () => {
|
||||
nextTick(recalcMovesLayout)
|
||||
})
|
||||
nextTick(recalcMovesLayout);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', recalcMovesLayout)
|
||||
nextTick(recalcMovesLayout)
|
||||
})
|
||||
window.addEventListener("resize", recalcMovesLayout);
|
||||
nextTick(recalcMovesLayout);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', recalcMovesLayout)
|
||||
})
|
||||
window.removeEventListener("resize", recalcMovesLayout);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -108,7 +111,7 @@ onUnmounted(() => {
|
||||
class="move-pill"
|
||||
:class="{
|
||||
'move-pill-active': m.status === 'in_progress',
|
||||
'move-pill-pending': m.status === 'pending'
|
||||
'move-pill-pending': m.status === 'pending',
|
||||
}"
|
||||
:ref="rowIndex === 0 && idx === 0 ? setSamplePill : null"
|
||||
>
|
||||
@@ -135,7 +138,6 @@ onUnmounted(() => {
|
||||
reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -218,5 +220,4 @@ onUnmounted(() => {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user