@@ -1,18 +1,20 @@
< script setup >
import { ref , computed , onMounted , onUnmounted } from 'vue'
import { ref , computed , onMounted , onUnmounted , watch , nextTick } from 'vue'
import { useCube } from '../../composables/useCube'
import { useSettings } from '../../composables/useSettings'
import Line3D from '../common/Line3D.vue '
import { LAYER _ANIMATION _DURATION } from '../../config/animationSettings '
const { cubies , initCube , rotateLayer , turn , FACES } = useCube ( )
const { showProjections , isCubeTranslucent } = useSettings ( )
const { isCubeTranslucent } = useSettings ( )
// --- Visual State ---
const rx = ref ( - 25 ) // Initial View Rotation X
const ry = ref ( 45 ) // Initial View Rotation Y
const rx = ref ( - 25 )
const ry = ref ( 45 )
const rz = ref ( 0 )
const SCALE = 100 // Size of one cubie in px
const GAP = 0 // Gap between cubies
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 )
@@ -29,6 +31,8 @@ const selectedFace = ref(null) // 'front', 'up', etc.
const activeLayer = ref ( null ) // { axis, index, tangent, direction }
const currentLayerRotation = ref ( 0 ) // Visual rotation in degrees
const isAnimating = ref ( false )
const pendingLogicalUpdate = ref ( false )
const currentMoveId = ref ( null )
// --- Constants & Helpers ---
@@ -74,113 +78,20 @@ const project = (v) => {
const radY = ry . value * Math . PI / 180
const radZ = rz . value * Math . PI / 180
// 1. Rotate Z
let x1 = v . x * Math . cos ( radZ ) - v . y * Math . sin ( radZ )
let y1 = v . x * Math . sin ( radZ ) + v . y * Math . cos ( radZ )
let z1 = v . z
// 2. Rotate Y
let x2 = x1 * Math . cos ( radY ) + z1 * Math . sin ( radY )
let y2 = y1
let z2 = - x1 * Math . sin ( radY ) + z1 * Math . cos ( radY )
// 3. Rotate X
let x3 = x2
let y3 = y2 * Math . cos ( radX ) - z2 * Math . sin ( radX )
// let z3 = ... (depth not needed for projection vector direction)
return { x : x3 , y : y3 }
}
const projectedCubies = computed ( ( ) => {
// Filter cubies for each face based on logical coordinates
// x: -1 (Left), 1 (Right)
// y: -1 (Down/Bottom), 1 (Up/Top)
// z: -1 (Back), 1 (Front)
const left = cubies . value . filter ( c => Math . round ( c . x ) === - 1 )
const back = cubies . value . filter ( c => Math . round ( c . z ) === - 1 )
const down = cubies . value . filter ( c => Math . round ( c . y ) === - 1 )
return { left , back , down }
} )
const projectionTransforms = {
left : { tx : - 350 , ty : 0 , tz : 0 , ry : - 90 } ,
back : { tx : 0 , ty : - 200 , tz : - 350 , ry : 0 } ,
down : { tx : 0 , ty : 350 , tz : 0 , rx : 90 }
}
const projectionLines = computed ( ( ) => {
const lines = [ ]
// Helper to transform point
const transformPoint = ( p , transform ) => {
let x = p . x , y = p . y , z = p . z
// Rotate
if ( transform . ry ) {
const rad = transform . ry * Math . PI / 180
const x0 = x , z0 = z
x = x0 * Math . cos ( rad ) + z0 * Math . sin ( rad )
z = - x0 * Math . sin ( rad ) + z0 * Math . cos ( rad )
}
if ( transform . rx ) {
const rad = transform . rx * Math . PI / 180
const y0 = y , z0 = z
y = y0 * Math . cos ( rad ) - z0 * Math . sin ( rad )
z = y0 * Math . sin ( rad ) + z0 * Math . cos ( rad )
}
// Translate
x += transform . tx || 0
y += transform . ty || 0
z += transform . tz || 0
return { x , y , z }
}
const S = 150 // Half size
// 1. Left Projection
{
const t = projectionTransforms . left
// Start: Left Face corners (x = -S)
const start = [
{ x : - S , y : - S , z : - S } , { x : - S , y : - S , z : S } ,
{ x : - S , y : S , z : - S } , { x : - S , y : S , z : S }
]
const end = start . map ( p => transformPoint ( p , t ) )
start . forEach ( ( p , i ) => lines . push ( { start : p , end : end [ i ] } ) )
}
// 2. Back Projection
{
const t = projectionTransforms . back
// Start: Back Face corners (z = -S)
const start = [
{ x : - S , y : - S , z : - S } , { x : S , y : - S , z : - S } ,
{ x : - S , y : S , z : - S } , { x : S , y : S , z : - S }
]
const end = start . map ( p => transformPoint ( p , t ) )
start . forEach ( ( p , i ) => lines . push ( { start : p , end : end [ i ] } ) )
}
// 3. Down Projection (Down Face)
{
const t = projectionTransforms . down
// Start: Down Face corners (y = -S)
const start = [
{ x : - S , y : - S , z : - S } , { x : S , y : - S , z : - S } ,
{ x : - S , y : - S , z : S } , { x : S , y : - S , z : S }
]
const end = start . map ( p => transformPoint ( p , t ) )
start . forEach ( ( p , i ) => lines . push ( { start : p , end : end [ i ] } ) )
}
return lines
} )
// --- Interaction Logic ---
const onMouseDown = ( e ) => {
@@ -322,10 +233,9 @@ const snapRotation = () => {
const target = Math . round ( currentLayerRotation . value / 90 ) * 90
const steps = Math . round ( currentLayerRotation . value / 90 )
// Animation loop
const start = currentLayerRotation . value
const startTime = performance . now ( )
const duration = 200
const duration = LAYER _ANIMATION _DURATION
const animate = ( time ) => {
const p = Math . min ( ( time - startTime ) / duration , 1 )
@@ -344,24 +254,167 @@ const snapRotation = () => {
requestAnimationFrame ( animate )
}
const finishMove = ( steps ) => {
const finishMove = ( steps , directionOverride = null ) => {
if ( steps !== 0 && activeLayer . value ) {
const { axis , index } = activeLayer . value
// Logic Call
const count = Math . abs ( steps )
const direction = steps > 0 ? 1 : - 1
const direction = directionOverride !== null ? directionOverride : ( steps > 0 ? 1 : - 1 )
pendingLogicalUpdate . value = true
for ( let i = 0 ; i < count ; i ++ ) {
rotateLayer ( axis , index , direction )
}
}
}
// Reset
activeLayer . value = null
currentLayerRotation . value = 0
isAnimating . value = false
selectedCubie . value = null
selectedFace . value = null
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 ( )
moveQueue . forEach ( ( q , idx ) => {
const stepsMod = ( ( q . steps % 4 ) + 4 ) % 4
if ( stepsMod === 0 ) return
let modifier = ''
if ( stepsMod === 1 ) modifier = "'"
else if ( stepsMod === 2 ) modifier = '2'
else if ( stepsMod === 3 ) modifier = ''
const baseLabel = q . displayBase || q . base
const label = baseLabel + ( modifier === "'" ? "'" : modifier === '2' ? '2' : '' )
list . push ( {
id : ` q- ${ idx } ` ,
label ,
status : 'pending'
} )
} )
return list
} )
const moveRows = computed ( ( ) => {
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 ) )
}
return rows
} )
const copyQueueToClipboard = async ( ) => {
if ( ! displayMoves . value . length ) return
const text = displayMoves . value . map ( m => m . label ) . join ( ' ' )
try {
if ( navigator && navigator . clipboard && navigator . clipboard . writeText ) {
await navigator . clipboard . writeText ( text )
} else {
const textarea = document . createElement ( 'textarea' )
textarea . value = text
textarea . style . position = 'fixed'
textarea . style . opacity = '0'
document . body . appendChild ( textarea )
textarea . focus ( )
textarea . select ( )
try {
document . execCommand ( 'copy' )
} finally {
document . body . removeChild ( textarea )
}
}
} catch ( e ) {
}
}
const setSamplePill = ( el ) => {
if ( el && ! samplePillEl . value ) {
samplePillEl . value = el
}
}
const recalcMovesLayout = ( ) => {
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 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 = ( ) => {
moveQueue . length = 0
movesHistory . value = [ ]
currentMoveId . value = null
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 ) => {
@@ -405,57 +458,197 @@ const getCubieStyle = (c) => {
return { transform }
}
const getProjectionStyle = ( c , face ) => {
let col = 0
let row = 0
const getProjectionStyle = ( ) => ( { } )
if ( fac e === FACES . LEFT ) {
col = 1 - c . z
row = 1 - c . y
} else if ( face === FACES . BACK ) {
col = 1 - c . x
row = 1 - c . y
} else if ( face === FACES . DOWN ) {
col = c . x + 1
row = 1 - c . z
const moveQueu e = [ ]
const dequeueMove = ( ) => {
while ( moveQueue . length ) {
const next = moveQueue . shift ( )
const stepsMod = ( ( next . steps % 4 ) + 4 ) % 4
if ( stepsMod === 0 ) continue
let modifier = ''
if ( stepsMod === 1 ) modifier = "'" // +90 (logical +1)
else if ( stepsMod === 2 ) modifier = '2' // 180 (logical -2)
else if ( stepsMod === 3 ) modifier = '' // -90 (logical -1)
return { base : next . base , modifier , displayBase : next . displayBase }
}
return null
}
const x = ( col - 1 ) * SCALE
const y = ( row - 1 ) * SCALE
const processNextMove = ( ) => {
if ( isAnimating . value || activeLayer . value ) return
const next = dequeueMove ( )
if ( ! next ) return
return { transform : ` translate3d( ${ x } px, ${ y } px, 0px) ` }
const baseLabel = next . displayBase || next . base
const label = baseLabel + ( next . modifier === "'" ? "'" : next . modifier === '2' ? '2' : '' )
const id = movesHistory . value . length
movesHistory . value . push ( { id , label , status : 'in_progress' } )
currentMoveId . value = id
animateProgrammaticMove ( next . base , next . modifier )
}
const animateProgrammaticMove = ( base , modifier ) => {
if ( isAnimating . value || activeLayer . value ) return
// Map base move to axis/index (same warstwa jak przy dragowaniu)
let axis = 'y'
let index = 1
if ( base === 'U' ) {
axis = 'y' ; index = 1
} else if ( base === 'D' ) {
axis = 'y' ; index = - 1
} else if ( base === 'L' ) {
axis = 'x' ; index = - 1
} else if ( base === 'R' ) {
axis = 'x' ; index = 1
} else if ( base === 'F' ) {
axis = 'z' ; index = 1
} else if ( base === 'B' ) {
axis = 'z' ; index = - 1
}
// Kierunek zgodny z RubiksJSModel.rotateLayer:
// dir === 1 -> ruch z apostrofem, dir === -1 -> ruch podstawowy (bez apostrofu)
const count = modifier === '2' ? 2 : 1
const direction = modifier === "'" ? 1 : - 1
activeLayer . value = {
axis ,
index ,
tangent : { x : 1 , y : 0 }
}
currentLayerRotation . value = 0
isAnimating . value = true
const logicalSteps = direction * count
let visualSteps = logicalSteps
if ( axis === 'z' ) visualSteps = - visualSteps
if ( base === 'U' || base === 'D' ) visualSteps = - visualSteps
const target = visualSteps * 90
const start = 0
const startTime = performance . now ( )
const duration = LAYER _ANIMATION _DURATION * count
const animate = ( time ) => {
const p = Math . min ( ( time - startTime ) / duration , 1 )
const ease = 1 - Math . pow ( 1 - p , 3 )
currentLayerRotation . value = start + ( target - start ) * ease
if ( p < 1 ) {
requestAnimationFrame ( animate )
} else {
pendingLogicalUpdate . value = true
for ( let i = 0 ; i < count ; i += 1 ) {
rotateLayer ( axis , index , direction )
}
}
}
requestAnimationFrame ( animate )
}
const MOVE _MAP = {
'U' : { base : 'U' , modifier : '' } ,
'U-prime' : { base : 'U' , modifier : "'" } ,
'U2' : { base : 'U' , modifier : '2' } ,
'D' : { base : 'D' , modifier : "'" } ,
'D-prime' : { base : 'D' , modifier : '' } ,
'D2' : { base : 'D' , modifier : '2' } ,
'L' : { base : 'B' , modifier : "'" } ,
'L-prime' : { base : 'B' , modifier : '' } ,
'L2' : { base : 'B' , modifier : '2' } ,
'R' : { base : 'F' , modifier : '' } ,
'R-prime' : { base : 'F' , modifier : "'" } ,
'R2' : { base : 'F' , modifier : '2' } ,
'F' : { base : 'L' , modifier : "'" } ,
'F-prime' : { base : 'L' , modifier : '' } ,
'F2' : { base : 'L' , modifier : '2' } ,
'B' : { base : 'R' , modifier : '' } ,
'B-prime' : { base : 'R' , modifier : "'" } ,
'B2' : { base : 'R' , modifier : '2' }
}
const applyMove = ( move ) => {
let base = move
let isPrime = false
let turns = 1
const mapping = MOVE _MAP [ move ]
if ( ! mapping ) return
if ( move . endsWith ( '2' ) ) {
turns = 2
base = move [ 0 ]
} else if ( move . endsWith ( '-prime' ) ) {
isPrime = true
b ase = move [ 0 ]
let delta = 0
if ( mapping . modifier === "'" ) delta = 1 // logical +1
else if ( mapping . modifier === '' ) delta = - 1 // logical -1
else if ( mapping . modifier === '2' ) delta = - 2 // logical -2
const displayB ase = move [ 0 ]
const last = moveQueue [ moveQueue . length - 1 ]
if ( last && last . base === mapping . base && last . displayBase === displayBase ) {
last . steps += delta
} else {
moveQueue . push ( { base : mapping . base , displayBase , steps : delta } )
}
const notation = isPrime ? ` ${ base } ' ` : base
processNextMove ( )
}
for ( let i = 0 ; i < turns ; i += 1 ) {
turn ( notation )
const allMoves = Object . keys ( MOVE _MAP )
const scramble = ( ) => {
for ( let i = 0 ; i < 30 ; i += 1 ) {
const move = allMoves [ Math . floor ( Math . random ( ) * allMoves . length ) ]
applyMove ( move )
}
}
watch ( cubies , ( ) => {
if ( ! pendingLogicalUpdate . value ) return
pendingLogicalUpdate . value = false
if ( currentMoveId . value !== null ) {
const idx = movesHistory . value . findIndex ( m => m . id === currentMoveId . value )
if ( idx !== - 1 ) {
movesHistory . value [ idx ] = {
... movesHistory . value [ idx ] ,
status : 'done'
}
}
currentMoveId . value = null
}
activeLayer . value = null
currentLayerRotation . value = 0
isAnimating . value = false
selectedCubie . value = null
selectedFace . value = null
processNextMove ( )
} )
onMounted ( ( ) => {
initCube ( )
window . addEventListener ( 'mousemove' , onMouseMove )
window . addEventListener ( 'mouseup' , onMouseUp )
window . addEventListener ( 'resize' , recalcMovesLayout )
window . addEventListener ( 'keydown' , handleKeydown )
nextTick ( recalcMovesLayout )
} )
onUnmounted ( ( ) => {
window . removeEventListener ( 'mousemove' , onMouseMove )
window . removeEventListener ( 'mouseup' , onMouseUp )
// Clean up any potential animation frames? rafId is local to snapRotation, but harmless.
window . removeEventListener ( 'resize' , recalcMovesLayout )
window . removeEventListener ( 'keydown' , handleKeydown )
} )
watch ( displayMoves , ( ) => {
nextTick ( recalcMovesLayout )
} )
< / script >
@@ -479,59 +672,106 @@ onUnmounted(() => {
< / div >
< / div >
<!-- Projections of Hidden Faces -- >
< div v-if = "showProjections" class="projections" >
< ! - - Guide Lines - - >
< Line3D v-for = "(line, i) in projectionLines" :key="'line-'+i"
:start = "line.start" :end = "line.end"
:color = "'var(--text-strong)'"
:thickness = "1" / >
<!-- Left Face Projection -- >
< div class = "projection-group left-projection" >
< div v-for = "c in projectedCubies.left" :key="c.id"
class = "cubie-placeholder"
: style = "getProjectionStyle(c, FACES.LEFT)" >
< div class = "sticker" :class = "c.faces.left" > < / div >
< / div >
< / div >
<!-- Back Face Projection -- >
< div class = "projection-group back-projection" >
< div v-for = "c in projectedCubies.back" :key="c.id"
class = "cubie-placeholder"
: style = "getProjectionStyle(c, FACES.BACK)" >
< div class = "sticker" :class = "c.faces.back" > < / div >
< / div >
< / div >
<!-- Down Face Projection ( Exploded View ) -- >
< div class = "projection-group down-projection" >
< div v-for = "c in projectedCubies.down" :key="c.id"
class = "cubie-placeholder"
: style = "getProjectionStyle(c, FACES.DOWN)" >
< div class = "sticker" :class = "c.faces.down" > < / div >
< / div >
< / div >
< / div >
< / div >
< div class = "controls" >
< div class = "controls controls-left" >
< div class = "controls-row" >
< button class = "btn-neon move-btn" @click ="applyMove('U')" > U < / button >
< button class = "btn-neon move-btn" @click ="applyMove('U-prime ')" > U ' < / button >
< button class = "btn-neon move-btn" @click ="applyMove('U2 ')" > U2 < / button >
< button class = "btn-neon move-btn" @click ="applyMove('D ')" > D < / button >
< button class = "btn-neon move-btn" @click ="applyMove('L ')" > L < / button >
< / div >
< div class = "controls-row" >
< button class = "btn-neon move-btn" @click ="applyMove('L ')" > L < / button >
< button class = "btn-neon move-btn" @click ="applyMove('U-prime ')" > U ' < / button >
< button class = "btn-neon move-btn" @click ="applyMove('D-prime')" > D ' < / button >
< button class = "btn-neon move-btn" @click ="applyMove('L-prime')" > L ' < / button >
< / div >
< div class = "controls-row" >
< button class = "btn-neon move-btn" @click ="applyMove('U2')" > U2 < / button >
< button class = "btn-neon move-btn" @click ="applyMove('D2')" > D2 < / button >
< button class = "btn-neon move-btn" @click ="applyMove('L2')" > L2 < / button >
< / div >
< / div >
< div class = "controls controls-right" >
< div class = "controls-row" >
< button class = "btn-neon move-btn" @click ="applyMove('R')" > R < / button >
< button class = "btn-neon move-btn" @click ="applyMove('F')" > F < / button >
< button class = "btn-neon move-btn" @click ="applyMove('B')" > B < / button >
< / div >
< div class = "controls-row" >
< button class = "btn-neon move-btn" @click ="applyMove('R-prime')" > R ' < / button >
< button class = "btn-neon move-btn" @click ="applyMove('F-prime')" > F ' < / button >
< button class = "btn-neon move-btn" @click ="applyMove('B-prime')" > B ' < / button >
< / div >
< div class = "controls-row" >
< button class = "btn-neon move-btn" @click ="applyMove('R2')" > R2 < / button >
< button class = "btn-neon move-btn" @click ="applyMove('F2')" > F2 < / button >
< button class = "btn-neon move-btn" @click ="applyMove('B2')" > B2 < / button >
< / div >
< / div >
< button class = "btn-neon move-btn scramble-btn" @click ="scramble" >
Scramble
< / button >
< div class = "moves-history" >
< div class = "moves-inner" ref = "movesHistoryEl" >
< div
v-for = "(row, rowIndex) in moveRows"
:key = "rowIndex"
class = "moves-row"
: style = "{ columnGap: movesColumnGap + 'px' }"
>
< span
v-for = "(m, idx) in row"
:key = "m.id"
class = "move-pill"
: class = "{
'move-pill-active': m.status === 'in_progress',
'move-pill-pending': m.status === 'pending'
}"
: ref = "rowIndex === 0 && idx === 0 ? setSamplePill : null"
>
{ { m . label } }
< / span >
< / div >
< / div >
< 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 >
@@ -573,13 +813,20 @@ onUnmounted(() => {
. controls {
position : absolute ;
top : 96 px ;
right : 24 px ;
display : flex ;
flex - direction : column ;
gap : 8 px ;
z - index : 50 ;
}
. controls - left {
left : 24 px ;
}
. controls - right {
right : 24 px ;
}
. controls - row {
display : flex ;
gap : 8 px ;
@@ -593,6 +840,142 @@ onUnmounted(() => {
padding : 0 10 px ;
}
. scramble - btn {
position : absolute ;
bottom : 72 px ;
left : 24 px ;
z - index : 50 ;
}
. moves - history {
position : absolute ;
bottom : 72 px ;
left : 50 % ;
transform : translateX ( - 50 % ) ;
width : 100 % ;
max - width : calc ( 100 vw - 360 px ) ;
overflow - x : hidden ;
padding : 12 px 12 px 26 px 12 px ;
background : rgba ( 0 , 0 , 0 , 0.4 ) ;
border - radius : 8 px ;
backdrop - filter : blur ( 8 px ) ;
}
. moves - inner {
display : flex ;
flex - direction : column ;
gap : 6 px ;
}
. moves - row {
display : flex ;
}
. 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 : # fff ;
white - space : nowrap ;
}
. move - pill - active {
background : # ffd500 ;
color : # 000 ;
border - color : # ffd500 ;
}
. move - pill - pending {
opacity : 0.4 ;
}
. moves - actions {
position : absolute ;
right : 6 px ;
bottom : 6 px ;
display : flex ;
gap : 0 px ;
}
. queue - action {
border : none ;
background : transparent ;
padding : 6 px 6 px ;
color : # fff ;
font - size : 0.8 rem ;
cursor : pointer ;
}
. moves - history : : after {
content : none ;
}
. queue - action : focus {
outline : none ;
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 ;