@@ -13,6 +13,8 @@ const ry = ref(45)
const rz = ref ( 0 )
const SCALE = 100
const GAP = 0
const MIN _MOVES _COLUMN _GAP = 6
const movesColumnGap = ref ( MIN _MOVES _COLUMN _GAP )
// --- Interaction State ---
const isDragging = ref ( false )
@@ -269,6 +271,8 @@ const movesHistory = ref([])
const movesHistoryEl = ref ( null )
const samplePillEl = ref ( null )
const movesPerRow = ref ( 0 )
const isAddModalOpen = ref ( false )
const addMovesText = ref ( '' )
const displayMoves = computed ( ( ) => {
const list = movesHistory . value . slice ( )
@@ -340,13 +344,29 @@ const recalcMovesLayout = () => {
const pill = samplePillEl . value
if ( ! container || ! pill ) return
const containerWidth = container . clientWidth - 4
const pillWidth = pill . offsetWidth + 8
const containerWidth = container . clientWidth
const pillWidth = pill . offsetWidth
if ( pillWidth <= 0 ) return
const rawCount = Math . floor ( containerWidth / pillWidth )
const count = Math . max ( 1 , rawCount - 1 )
movesPerRow . value = count
const totalWidth = ( cols ) => {
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
while ( cols > 1 && totalWidth ( cols ) > containerWidth ) {
cols -= 1
}
let gap = 0
if ( cols > 1 ) {
gap = ( containerWidth - cols * pillWidth ) / ( cols - 1 )
}
movesPerRow . value = cols
movesColumnGap . value = gap
}
const resetQueue = ( ) => {
@@ -356,6 +376,47 @@ const resetQueue = () => {
nextTick ( recalcMovesLayout )
}
const openAddModal = ( ) => {
addMovesText . value = ''
isAddModalOpen . value = true
}
const closeAddModal = ( ) => {
isAddModalOpen . value = false
}
const handleAddMoves = ( ) => {
const text = addMovesText . value || ''
const tokens = text . split ( /\s+/ ) . filter ( Boolean )
const moves = [ ]
tokens . forEach ( ( token ) => {
const t = token . trim ( )
if ( ! t ) return
const base = t [ 0 ]
if ( ! 'UDLRFB' . includes ( base ) ) return
const rest = t . slice ( 1 )
let key = null
if ( rest === '' ) key = base
else if ( rest === '2' ) key = base + '2'
else if ( rest === "'" || rest === '’ ' ) key = base + '-prime'
if ( key && MOVE _MAP [ key ] ) {
moves . push ( key )
}
} )
moves . forEach ( ( m ) => applyMove ( m ) )
addMovesText . value = ''
isAddModalOpen . value = false
}
const handleKeydown = ( e ) => {
if ( e . key === 'Escape' && isAddModalOpen . value ) {
e . preventDefault ( )
closeAddModal ( )
}
}
const getCubieStyle = ( c ) => {
// Base Position
const x = c . x * ( SCALE + GAP )
@@ -575,6 +636,7 @@ onMounted(() => {
window . addEventListener ( 'mousemove' , onMouseMove )
window . addEventListener ( 'mouseup' , onMouseUp )
window . addEventListener ( 'resize' , recalcMovesLayout )
window . addEventListener ( 'keydown' , handleKeydown )
nextTick ( recalcMovesLayout )
} )
@@ -582,6 +644,7 @@ onUnmounted(() => {
window . removeEventListener ( 'mousemove' , onMouseMove )
window . removeEventListener ( 'mouseup' , onMouseUp )
window . removeEventListener ( 'resize' , recalcMovesLayout )
window . removeEventListener ( 'keydown' , handleKeydown )
} )
watch ( displayMoves , ( ) => {
@@ -657,7 +720,7 @@ watch(displayMoves, () => {
v-for = "(row, rowIndex) in moveRows"
:key = "rowIndex"
class = "moves-row"
: class = "{ 'moves-row-justify': rowIndex < moveRows.length - 1 }"
: style = "{ columnGap: movesColumnGap + 'px' }"
>
< span
v-for = "(m, idx) in row"
@@ -673,9 +736,42 @@ watch(displayMoves, () => {
< / span >
< / div >
< / div >
< div v-if = "displayMoves.length" class= "moves-actions">
< button class = "queue-action" @click ="c opyQueueToClipboard " > copy < / button >
< button class = "queue-action" @click ="resetQueue" > reset < / button >
< div class = "moves-actions" >
< button class = "queue-action" @click ="openAddModal " > add < / button >
< button
v-if = "displayMoves.length"
class = "queue-action"
@click ="copyQueueToClipboard"
>
copy
< / button >
< button
v-if = "displayMoves.length"
class = "queue-action"
@click ="resetQueue"
>
reset
< / button >
< / div >
< / div >
< div
v-if = "isAddModalOpen"
class = "moves-modal-backdrop"
@click.self ="closeAddModal"
>
< div class = "moves-modal" >
< textarea
v-model = "addMovesText"
class = "moves-modal-textarea"
/ >
< div class = "moves-modal-actions" >
< button class = "btn-neon move-btn moves-modal-button" @click ="closeAddModal" >
cancel
< / button >
< button class = "btn-neon move-btn moves-modal-button" @click ="handleAddMoves" >
add moves
< / button >
< / div >
< / div >
< / div >
< / div >
@@ -773,22 +869,18 @@ watch(displayMoves, () => {
. moves - row {
display : flex ;
column - gap : 8 px ;
}
. moves - row - justify {
justify - content : space - between ;
}
. move - pill {
display : flex ;
align - items : center ;
justify - content : center ;
width : 16 px ;
padding : 4 px 8 px ;
border - radius : 999 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.2 ) ;
font - size : 0.8 rem ;
color : # f0f0f0 ;
color : # fff ;
white - space : nowrap ;
}
@@ -828,6 +920,62 @@ watch(displayMoves, () => {
box - shadow : none ;
}
. moves - modal - backdrop {
position : fixed ;
inset : 0 ;
background : rgba ( 0 , 0 , 0 , 0.65 ) ;
display : flex ;
align - items : center ;
justify - content : center ;
z - index : 200 ;
}
. moves - modal {
background : var ( -- panel - bg ) ;
border : 1 px solid var ( -- panel - border ) ;
color : var ( -- text - color ) ;
border - radius : 10 px ;
padding : 24 px ;
min - width : 480 px ;
max - width : 800 px ;
box - shadow : 0 20 px 40 px rgba ( 0 , 0 , 0 , 0.7 ) ;
}
. moves - modal - textarea {
width : 100 % ;
min - height : 220 px ;
background : var ( -- panel - bg ) ;
color : var ( -- text - color ) ;
box - sizing : border - box ;
border - radius : 6 px ;
border : 1 px solid var ( -- panel - border ) ;
padding : 10 px ;
resize : vertical ;
font - family : inherit ;
font - size : 0.85 rem ;
}
. moves - modal - textarea : focus {
outline : none ;
box - shadow : none ;
}
. moves - modal - actions {
margin - top : 20 px ;
display : flex ;
justify - content : flex - end ;
gap : 12 px ;
}
. moves - modal - button {
font - size : 0.85 rem ;
}
. moves - modal - button : focus {
outline : none ;
box - shadow : none ;
}
/* Projection Styles */
. projections {
position : absolute ;