Files
rubic-cube/src/components/renderers/CubeMoveControls.vue

254 lines
5.9 KiB
Vue

<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import { Locate, LocateFixed } from "lucide-vue-next";
const props = defineProps({
isViewDefault: { type: Boolean, default: true },
});
const emit = defineEmits(["move", "scramble", "solve", "reset-camera"]);
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>
<div>
<div class="controls controls-left">
<div class="controls-row">
<button class="btn-neon move-btn" @click="emit('move', 'U')">U</button>
<button class="btn-neon move-btn" @click="emit('move', 'D')">D</button>
<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>
</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>
</div>
</div>
<div class="controls controls-right">
<div class="controls-row">
<button class="btn-neon move-btn" @click="emit('move', 'R')">R</button>
<button class="btn-neon move-btn" @click="emit('move', 'F')">F</button>
<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>
</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>
</div>
</div>
<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 class="bottom-right-controls">
<button
class="btn-neon move-btn camera-reset-btn"
:class="{ 'is-default': props.isViewDefault }"
:disabled="props.isViewDefault"
@click="emit('reset-camera')"
>
<LocateFixed v-if="props.isViewDefault" :size="18" />
<Locate v-else :size="18" />
</button>
</div>
</div>
</template>
<style scoped>
.controls {
position: absolute;
top: 96px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 50;
}
.controls-left {
left: 24px;
}
.controls-right {
right: 24px;
}
.controls-row {
display: flex;
gap: 8px;
justify-content: center;
}
.move-btn {
min-width: 44px;
height: 36px;
font-size: 0.9rem;
padding: 0 10px;
}
.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);
}
.dropdown-item:focus {
outline: none;
}
.bottom-right-controls {
position: absolute;
bottom: 72px;
right: 24px;
z-index: 50;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-end;
}
.camera-reset-btn {
min-width: 40px;
height: 40px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s, transform 0.2s;
}
.camera-reset-btn.is-default {
opacity: 0.3;
cursor: default;
pointer-events: none;
}
.camera-reset-btn:not(.is-default):hover {
transform: scale(1.1);
}
</style>