mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:01:31 +00:00
ENH - restore pre-Konsta app overhaul
This commit is contained in:
+1555
-278
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { kBadge } from 'konsta/vue'
|
||||
import { Minus } from 'lucide-vue-next'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { NON_REMOVABLE_PHONE_APP_IDS } from '@/config/apps'
|
||||
@@ -39,6 +39,7 @@ const darkchat = useDarkChatStore()
|
||||
const router = useRouter()
|
||||
const iconFailed = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const calendarToday = ref(new Date())
|
||||
const dragOffset = ref({ x: 0, y: 0 })
|
||||
const dragStyle = computed(() =>
|
||||
isDragging.value
|
||||
@@ -49,6 +50,7 @@ const dragStyle = computed(() =>
|
||||
)
|
||||
const suppressClick = ref(false)
|
||||
let holdTimer: number | undefined
|
||||
let calendarTimer: number | undefined
|
||||
let pointerStart = { x: 0, y: 0 }
|
||||
const unreadCount = computed(() => {
|
||||
if (props.app.id === 'mail') return mail.counts.unread
|
||||
@@ -60,6 +62,12 @@ const notificationBadgeColors = {
|
||||
bg: 'bg-[#ff3b30]',
|
||||
text: 'text-white',
|
||||
}
|
||||
const calendarWeekday = computed(() =>
|
||||
new Intl.DateTimeFormat(phone.lang, { weekday: 'short' })
|
||||
.format(calendarToday.value)
|
||||
.replace(/\.$/, ''),
|
||||
)
|
||||
const calendarDay = computed(() => calendarToday.value.getDate())
|
||||
|
||||
function launch(event: MouseEvent): void {
|
||||
if (props.editMode || suppressClick.value) {
|
||||
@@ -167,8 +175,16 @@ function removeDragListeners(): void {
|
||||
window.removeEventListener('pointercancel', cancelPointerDrag)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.app.id !== 'calendar') return
|
||||
calendarTimer = window.setInterval(() => {
|
||||
calendarToday.value = new Date()
|
||||
}, 60_000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearHold()
|
||||
if (calendarTimer !== undefined) window.clearInterval(calendarTimer)
|
||||
removeDragListeners()
|
||||
})
|
||||
</script>
|
||||
@@ -200,10 +216,17 @@ onBeforeUnmount(() => {
|
||||
<span class="app-icon-anchor" aria-hidden="true">
|
||||
<span
|
||||
class="app-icon"
|
||||
:class="[app.iconClass, { 'app-icon--image': !iconFailed }]"
|
||||
:class="[
|
||||
app.iconClass,
|
||||
{ 'app-icon--image': !iconFailed && app.id !== 'calendar' },
|
||||
]"
|
||||
>
|
||||
<span v-if="app.id === 'calendar'" class="app-icon-calendar">
|
||||
<strong>{{ calendarWeekday }}</strong>
|
||||
<b>{{ calendarDay }}</b>
|
||||
</span>
|
||||
<img
|
||||
v-if="!iconFailed"
|
||||
v-else-if="!iconFailed"
|
||||
:src="app.iconImage"
|
||||
alt=""
|
||||
draggable="false"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -123,8 +123,12 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="memory-app" :aria-label="phone.t('Apps.memory.name')">
|
||||
<header class="memory-header">
|
||||
<main
|
||||
class="memory-app"
|
||||
:class="{ 'memory-app--playing': game }"
|
||||
:aria-label="phone.t('Apps.memory.name')"
|
||||
>
|
||||
<header v-if="!game" class="memory-header">
|
||||
<div>
|
||||
<span>{{ phone.t('Apps.memory.eyebrow') }}</span>
|
||||
<h1>{{ phone.t('Apps.memory.name') }}</h1>
|
||||
@@ -278,6 +282,10 @@ onBeforeUnmount(() => {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.memory-app--playing {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.memory-header {
|
||||
height: 52px;
|
||||
display: flex;
|
||||
@@ -405,50 +413,67 @@ onBeforeUnmount(() => {
|
||||
.memory-difficulty__best { align-self: start; color: #74698f; font-size: 11.5px; line-height: 1.2; }
|
||||
.memory-difficulties svg { grid-column: 3; grid-row: 1 / 3; color: #7658c7; }
|
||||
|
||||
.memory-game { padding-top: 5px; }
|
||||
|
||||
.memory-stats {
|
||||
height: 50px;
|
||||
display: grid;
|
||||
grid-template-columns: 30px auto auto 1fr;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
.memory-game {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.memory-stats div { display: grid; }
|
||||
.memory-stats span { color: #74698f; font-size: 10.5px; font-weight: 800; text-transform: uppercase; }
|
||||
.memory-stats strong { font-size: 19px; line-height: 1.05; }
|
||||
.memory-stats button { justify-self: end; border: 0; color: #7052bf; background: transparent; font-size: 14px; font-weight: 800; }
|
||||
.memory-stats {
|
||||
position: absolute;
|
||||
z-index: 7;
|
||||
top: 66px;
|
||||
right: 18px;
|
||||
left: 18px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr 1fr auto;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid rgb(105 79 160 / 10%);
|
||||
border-radius: 21px;
|
||||
background: rgb(246 241 255 / 64%);
|
||||
box-shadow: 0 8px 24px rgb(75 52 121 / 13%);
|
||||
backdrop-filter: blur(14px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.memory-stats div { height: 32px; display: grid; grid-template-rows: 10px 20px; align-content: center; justify-items: center; }
|
||||
.memory-stats span { color: #74698f; font-size: 10.5px; font-weight: 800; line-height: 10px; text-transform: uppercase; }
|
||||
.memory-stats strong { display: block; font-size: 19px; line-height: 20px; }
|
||||
.memory-stats button { justify-self: end; border: 0; color: #7052bf; background: transparent; font-size: 13px; font-weight: 800; }
|
||||
.memory-stats .memory-menu-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
justify-self: start;
|
||||
padding: 0;
|
||||
border: 1px solid rgb(105 79 160 / 10%);
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: rgb(255 255 255 / 48%);
|
||||
box-shadow: 0 3px 8px rgb(75 52 121 / 8%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.memory-board {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 9px;
|
||||
border: 1px solid rgb(101 77 153 / 9%);
|
||||
border-radius: 20px;
|
||||
background: rgb(255 255 255 / 31%);
|
||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 50%);
|
||||
align-content: center;
|
||||
padding: 108px 17px 28px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background:
|
||||
radial-gradient(circle at 12% 88%, rgb(135 105 207 / 12%), transparent 31%),
|
||||
radial-gradient(circle at 88% 16%, rgb(255 255 255 / 54%), transparent 28%);
|
||||
perspective: 900px;
|
||||
}
|
||||
|
||||
.memory-board--small { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.memory-board--medium,
|
||||
.memory-board--large { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.memory-board--large { gap: 6px; padding: 7px; }
|
||||
.memory-board--large { gap: 6px; padding-right: 15px; padding-left: 15px; }
|
||||
|
||||
.memory-game-card {
|
||||
aspect-ratio: 0.82;
|
||||
@@ -545,7 +570,7 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border-radius: 20px;
|
||||
border-radius: 0;
|
||||
background: rgb(244 239 255 / 90%);
|
||||
backdrop-filter: blur(6px);
|
||||
text-align: center;
|
||||
|
||||
@@ -198,8 +198,12 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="minesweeper-app" :aria-label="phone.t('Apps.minesweeper.name')">
|
||||
<header class="minesweeper-header">
|
||||
<main
|
||||
class="minesweeper-app"
|
||||
:class="{ 'minesweeper-app--playing': !minesweeper.menuOpen && game }"
|
||||
:aria-label="phone.t('Apps.minesweeper.name')"
|
||||
>
|
||||
<header v-if="minesweeper.menuOpen" class="minesweeper-header">
|
||||
<div>
|
||||
<span>{{ phone.t('Apps.minesweeper.eyebrow') }}</span>
|
||||
<h1>{{ phone.t('Apps.minesweeper.name') }}</h1>
|
||||
@@ -420,6 +424,10 @@ onBeforeUnmount(() => {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.minesweeper-app--playing {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.minesweeper-header {
|
||||
height: 55px;
|
||||
display: flex;
|
||||
@@ -524,22 +532,59 @@ onBeforeUnmount(() => {
|
||||
.minesweeper-difficulties small { align-self: start; color: #658a89; font-size: 8px; }
|
||||
.minesweeper-long-press { margin: 0; color: #628887; font-size: 9px; }
|
||||
|
||||
.minesweeper-game { position: relative; padding-top: 5px; }
|
||||
.minesweeper-toolbar { height: 46px; display: grid; grid-template-columns: 36px 1fr 1fr 36px; align-items: center; gap: 7px; }
|
||||
.minesweeper-toolbar div { display: grid; justify-items: center; line-height: 1.05; }
|
||||
.minesweeper-toolbar span { color: #5a8484; font-size: 8px; font-weight: 800; text-transform: uppercase; }
|
||||
.minesweeper-toolbar strong { font-size: 16px; }
|
||||
.minesweeper-game {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 12% 88%, rgb(26 129 130 / 14%), transparent 32%),
|
||||
radial-gradient(circle at 88% 16%, rgb(238 255 249 / 45%), transparent 29%);
|
||||
}
|
||||
|
||||
.minesweeper-toolbar {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 66px;
|
||||
right: 18px;
|
||||
left: 18px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr 1fr 32px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid rgb(23 83 87 / 10%);
|
||||
border-radius: 21px;
|
||||
background: rgb(226 249 242 / 65%);
|
||||
box-shadow: 0 8px 24px rgb(23 73 75 / 13%);
|
||||
backdrop-filter: blur(14px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.minesweeper-toolbar div { height: 32px; display: grid; grid-template-rows: 10px 20px; align-content: center; justify-items: center; }
|
||||
.minesweeper-toolbar span { color: #5a8484; font-size: 10px; font-weight: 800; line-height: 10px; text-transform: uppercase; }
|
||||
.minesweeper-toolbar strong { display: block; font-size: 18px; line-height: 20px; }
|
||||
|
||||
.minesweeper-toolbar .minesweeper-toolbar__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.minesweeper-board {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 14px;
|
||||
left: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--minesweeper-columns), minmax(0, 1fr));
|
||||
gap: 3px;
|
||||
padding: 7px;
|
||||
border: 1px solid rgb(16 78 82 / 10%);
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
background: #187a7e;
|
||||
box-shadow: inset 0 2px 2px rgb(255 255 255 / 10%), 0 15px 27px rgb(20 79 81 / 18%);
|
||||
transform: translateY(-45%);
|
||||
}
|
||||
|
||||
.minesweeper-cell {
|
||||
@@ -582,8 +627,7 @@ onBeforeUnmount(() => {
|
||||
.minesweeper-game--exploding::after {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
inset: 46px 0 0;
|
||||
border-radius: 18px;
|
||||
inset: 0;
|
||||
background: #ffb341;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
@@ -767,13 +811,13 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
@keyframes minesweeper-board-shake {
|
||||
0%, 100% { transform: translate(0); }
|
||||
12% { transform: translate(-7px, 3px) rotate(-1deg); }
|
||||
24% { transform: translate(6px, -4px) rotate(1deg); }
|
||||
38% { transform: translate(-5px, -2px); }
|
||||
52% { transform: translate(4px, 3px); }
|
||||
68% { transform: translate(-2px, -2px); }
|
||||
82% { transform: translate(2px, 1px); }
|
||||
0%, 100% { transform: translate(0, -45%); }
|
||||
12% { transform: translate(-7px, calc(-45% + 3px)) rotate(-1deg); }
|
||||
24% { transform: translate(6px, calc(-45% - 4px)) rotate(1deg); }
|
||||
38% { transform: translate(-5px, calc(-45% - 2px)); }
|
||||
52% { transform: translate(4px, calc(-45% + 3px)); }
|
||||
68% { transform: translate(-2px, calc(-45% - 2px)); }
|
||||
82% { transform: translate(2px, calc(-45% + 1px)); }
|
||||
}
|
||||
|
||||
@keyframes minesweeper-bomb-drop {
|
||||
@@ -866,7 +910,22 @@ onBeforeUnmount(() => {
|
||||
.minesweeper-secondary { position: relative; z-index: 1; min-width: 155px; min-height: 40px; border-radius: 13px; font-size: 11px; font-weight: 850; pointer-events: auto; }
|
||||
.minesweeper-primary { border: 0; color: #104d51; background: #8ce3d2; }
|
||||
.minesweeper-secondary { border: 1px solid rgb(255 255 255 / 13%); color: #e6faf5; background: rgb(255 255 255 / 7%); }
|
||||
.minesweeper-game__hint { margin: 9px 0 0; color: #668c8c; font-size: 9px; text-align: center; }
|
||||
.minesweeper-game__hint {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
right: 45px;
|
||||
bottom: 27px;
|
||||
left: 45px;
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
border-radius: 999px;
|
||||
color: #668c8c;
|
||||
background: rgb(226 249 242 / 52%);
|
||||
backdrop-filter: blur(10px);
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button:active { transform: scale(0.96); }
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ChevronLeft,
|
||||
ChevronsDown,
|
||||
Pause,
|
||||
Play,
|
||||
RotateCcw,
|
||||
RotateCw,
|
||||
Trophy,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
@@ -201,8 +196,12 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="neon-drop-app" :aria-label="phone.t('Apps.neonDrop.name')">
|
||||
<header class="neon-header">
|
||||
<main
|
||||
class="neon-drop-app"
|
||||
:class="{ 'neon-drop-app--playing': !neon.menuOpen && game }"
|
||||
:aria-label="phone.t('Apps.neonDrop.name')"
|
||||
>
|
||||
<header v-if="neon.menuOpen" class="neon-header">
|
||||
<div>
|
||||
<span>{{ phone.t('Apps.neonDrop.eyebrow') }}</span>
|
||||
<h1>{{ phone.t('Apps.neonDrop.name') }}</h1>
|
||||
@@ -335,51 +334,11 @@ onBeforeUnmount(() => {
|
||||
<span>{{ phone.t('Apps.neonDrop.level') }}</span
|
||||
><strong>{{ game.level }}</strong>
|
||||
<div class="neon-level">
|
||||
<i :style="{ height: `${levelProgress * 100}%` }"></i>
|
||||
<i :style="{ width: `${levelProgress * 100}%` }"></i>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="neon-controls"
|
||||
:aria-label="phone.t('Apps.neonDrop.controls')"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.neonDrop.left')"
|
||||
@click="move(-1)"
|
||||
>
|
||||
<ArrowLeft :size="18" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.neonDrop.rotate')"
|
||||
@click="rotate"
|
||||
>
|
||||
<RotateCw :size="18" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.neonDrop.right')"
|
||||
@click="move(1)"
|
||||
>
|
||||
<ArrowRight :size="18" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.neonDrop.softDrop')"
|
||||
@click="softDrop"
|
||||
>
|
||||
<ArrowDown :size="18" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.neonDrop.hardDrop')"
|
||||
@click="hardDrop"
|
||||
>
|
||||
<ChevronsDown :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="neon-hint">{{ phone.t('Apps.neonDrop.gameHint') }}</p>
|
||||
|
||||
<div v-if="game.status === 'paused'" class="neon-overlay">
|
||||
@@ -421,6 +380,9 @@ onBeforeUnmount(() => {
|
||||
user-select: none;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.neon-drop-app--playing {
|
||||
padding: 0;
|
||||
}
|
||||
.neon-header {
|
||||
height: 54px;
|
||||
display: flex;
|
||||
@@ -613,42 +575,70 @@ onBeforeUnmount(() => {
|
||||
background: #ffffff0a;
|
||||
}
|
||||
.neon-game {
|
||||
position: relative;
|
||||
height: calc(100% - 54px);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.neon-toolbar {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 66px;
|
||||
right: 18px;
|
||||
left: 18px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
grid-template-columns: 35px 1fr 1fr 35px;
|
||||
grid-template-columns: 32px 1fr 1fr 32px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid #7cf6e72b;
|
||||
border-radius: 21px;
|
||||
background: #101a38a8;
|
||||
box-shadow: 0 8px 24px #0008;
|
||||
backdrop-filter: blur(14px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.neon-toolbar span {
|
||||
font-size: 11px;
|
||||
line-height: 10px;
|
||||
}
|
||||
.neon-toolbar div {
|
||||
text-align: center;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
grid-template-rows: 10px 20px;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
.neon-toolbar strong {
|
||||
font-size: 21px;
|
||||
display: block;
|
||||
font-size: 19px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.neon-toolbar button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: none;
|
||||
}
|
||||
.neon-play-area {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.neon-board {
|
||||
width: 218px;
|
||||
height: 414px;
|
||||
position: absolute;
|
||||
top: 156px;
|
||||
right: 0;
|
||||
bottom: 76px;
|
||||
left: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
grid-template-rows: repeat(18, 1fr);
|
||||
gap: 1.5px;
|
||||
padding: 5px;
|
||||
border: 1px solid #65f4e43b;
|
||||
border-radius: 13px;
|
||||
background: #06091af2;
|
||||
box-shadow:
|
||||
inset 0 0 25px #000b,
|
||||
0 12px 26px #0008;
|
||||
grid-template-rows: repeat(17, 1fr);
|
||||
gap: 1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.neon-cell {
|
||||
@@ -700,31 +690,35 @@ onBeforeUnmount(() => {
|
||||
animation: neon-clear 0.24s ease-out;
|
||||
}
|
||||
.neon-side {
|
||||
width: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 110px;
|
||||
right: 14px;
|
||||
left: 14px;
|
||||
height: 40px;
|
||||
display: grid;
|
||||
grid-template-columns: auto 40px auto auto 1fr;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 10px 4px 11px;
|
||||
border: 1px solid #65f4e448;
|
||||
border-radius: 13px;
|
||||
background: linear-gradient(180deg, #1420478f, #090d24a3);
|
||||
box-shadow: inset 0 0 16px #53e8d90a;
|
||||
gap: 8px;
|
||||
padding: 0 6px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.neon-side > strong {
|
||||
font-size: 28px;
|
||||
font-size: 21px;
|
||||
}
|
||||
.neon-preview {
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 2px;
|
||||
padding: 5px;
|
||||
border: 1px solid #ffffff35;
|
||||
border-radius: 10px;
|
||||
background: #ffffff08;
|
||||
gap: 1px;
|
||||
padding: 3px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.neon-preview i[class*='neon-piece--'] {
|
||||
border: 1px solid #ffffff68;
|
||||
@@ -734,63 +728,49 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.neon-level {
|
||||
position: relative;
|
||||
width: 12px;
|
||||
height: 221px;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #ffffff0c;
|
||||
}
|
||||
.neon-level i {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(#73a7ff, #71f5e5);
|
||||
box-shadow: 0 0 9px #69f4e5;
|
||||
transition: height 0.25s;
|
||||
}
|
||||
.neon-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 5px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
.neon-controls button {
|
||||
height: 48px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 1px solid #ffffff16;
|
||||
border-radius: 11px;
|
||||
color: #dffefd;
|
||||
background: #ffffff0b;
|
||||
}
|
||||
.neon-controls button:last-child {
|
||||
color: #071225;
|
||||
background: linear-gradient(135deg, #ffe265, #ff9167);
|
||||
}
|
||||
.neon-controls svg {
|
||||
transform: scale(1.2);
|
||||
transition: width 0.25s;
|
||||
}
|
||||
.neon-hint {
|
||||
margin: 7px 0 0;
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
right: 45px;
|
||||
bottom: 27px;
|
||||
left: 45px;
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
border-radius: 999px;
|
||||
color: #d6deed;
|
||||
font-size: 16px;
|
||||
background: #101a3891;
|
||||
backdrop-filter: blur(10px);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.neon-overlay {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
inset: 42px 0 19px;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 25px;
|
||||
border-radius: 17px;
|
||||
background: #080c21e8;
|
||||
backdrop-filter: blur(6px);
|
||||
text-align: center;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
ChevronLeft,
|
||||
RotateCcw,
|
||||
Volume2,
|
||||
@@ -30,16 +26,6 @@ const canResume = computed(
|
||||
const currentHighest = computed(() =>
|
||||
Math.max(0, ...(numberMerge.game?.tiles.map((tile) => tile.value) ?? [])),
|
||||
)
|
||||
const directionButtons: Array<{
|
||||
direction: NumberMergeDirection
|
||||
icon: typeof ArrowUp
|
||||
}> = [
|
||||
{ direction: 'left', icon: ArrowLeft },
|
||||
{ direction: 'up', icon: ArrowUp },
|
||||
{ direction: 'down', icon: ArrowDown },
|
||||
{ direction: 'right', icon: ArrowRight },
|
||||
]
|
||||
|
||||
function formatScore(value: number): string {
|
||||
return new Intl.NumberFormat('en-US').format(value)
|
||||
}
|
||||
@@ -139,8 +125,12 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="number-merge-app" :aria-label="phone.t('Apps.numberMerge.name')">
|
||||
<header class="number-merge-header">
|
||||
<main
|
||||
class="number-merge-app"
|
||||
:class="{ 'number-merge-app--playing': !numberMerge.menuOpen && game }"
|
||||
:aria-label="phone.t('Apps.numberMerge.name')"
|
||||
>
|
||||
<header v-if="numberMerge.menuOpen" class="number-merge-header">
|
||||
<div>
|
||||
<span>{{ phone.t('Apps.numberMerge.eyebrow') }}</span>
|
||||
<h1>{{ phone.t('Apps.numberMerge.name') }}</h1>
|
||||
@@ -317,17 +307,6 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
</div>
|
||||
|
||||
<p class="number-merge-hint">{{ phone.t('Apps.numberMerge.swipeHint') }}</p>
|
||||
<div class="number-merge-controls" :aria-label="phone.t('Apps.numberMerge.controls')">
|
||||
<button
|
||||
v-for="control in directionButtons"
|
||||
:key="control.direction"
|
||||
type="button"
|
||||
:aria-label="phone.t(`Apps.numberMerge.directions.${control.direction}`)"
|
||||
@click="move(control.direction)"
|
||||
>
|
||||
<component :is="control.icon" :size="19" :stroke-width="2.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="confirmNewGame" class="number-merge-confirm" role="dialog" aria-modal="true">
|
||||
@@ -360,6 +339,10 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.number-merge-app--playing {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.number-merge-header {
|
||||
height: 55px;
|
||||
display: flex;
|
||||
@@ -508,40 +491,73 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
}
|
||||
.number-merge-how-to small { display: block; color: #8b6559; font-size: 11px; line-height: 1.35; }
|
||||
|
||||
.number-merge-game { padding-top: 5px; }
|
||||
.number-merge-game {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 12% 88%, rgb(161 77 47 / 13%), transparent 32%),
|
||||
radial-gradient(circle at 88% 16%, rgb(255 246 219 / 42%), transparent 29%);
|
||||
}
|
||||
|
||||
.number-merge-toolbar {
|
||||
height: 48px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 66px;
|
||||
right: 18px;
|
||||
left: 18px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
grid-template-columns: 36px 1fr 1fr 36px;
|
||||
grid-template-columns: 32px 1fr 1fr 32px;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid rgb(109 66 52 / 10%);
|
||||
border-radius: 21px;
|
||||
background: rgb(255 241 216 / 65%);
|
||||
box-shadow: 0 8px 24px rgb(95 48 31 / 13%);
|
||||
backdrop-filter: blur(14px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.number-merge-toolbar div {
|
||||
min-width: 0;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
grid-template-rows: 10px 20px;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.number-merge-toolbar strong { max-width: 80px; overflow: hidden; font-size: 18px; text-overflow: ellipsis; }
|
||||
.number-merge-toolbar span { line-height: 10px; }
|
||||
.number-merge-toolbar strong { display: block; max-width: 80px; overflow: hidden; font-size: 18px; line-height: 20px; text-overflow: ellipsis; }
|
||||
.number-merge-toolbar__restart { justify-self: end; }
|
||||
|
||||
.number-merge-toolbar .number-merge-toolbar__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.number-merge-board {
|
||||
--board-gap: 7px;
|
||||
--board-padding: 9px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 14px;
|
||||
left: 14px;
|
||||
width: auto;
|
||||
aspect-ratio: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--board-gap);
|
||||
padding: var(--board-padding);
|
||||
border: 1px solid rgb(78 41 32 / 9%);
|
||||
border: 0;
|
||||
border-radius: 21px;
|
||||
background: #80564c;
|
||||
box-shadow: inset 0 2px 2px rgb(255 255 255 / 10%), 0 17px 28px rgb(101 52 38 / 18%);
|
||||
box-shadow: inset 0 2px 2px rgb(255 255 255 / 10%), 0 14px 28px rgb(101 52 38 / 15%);
|
||||
transform: translateY(-47%);
|
||||
}
|
||||
|
||||
.number-merge-cell {
|
||||
@@ -638,25 +654,21 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown))
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.number-merge-hint { margin: 10px 0 7px; color: #976f61; font-size: 12px; text-align: center; }
|
||||
|
||||
.number-merge-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 38px);
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.number-merge-controls button {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 1px solid rgb(97 56 44 / 10%);
|
||||
border-radius: 12px;
|
||||
color: #71483b;
|
||||
background: rgb(255 255 255 / 38%);
|
||||
.number-merge-hint {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
right: 45px;
|
||||
bottom: 27px;
|
||||
left: 45px;
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
border-radius: 999px;
|
||||
color: #8e675b;
|
||||
background: rgb(255 241 216 / 52%);
|
||||
backdrop-filter: blur(10px);
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.number-merge-confirm {
|
||||
|
||||
@@ -106,8 +106,15 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="flappy-app" :class="`flappy-app--${flappy.design}`" :aria-label="phone.t('Apps.skyFlappy.name')">
|
||||
<header class="flappy-header">
|
||||
<main
|
||||
class="flappy-app"
|
||||
:class="[
|
||||
`flappy-app--${flappy.design}`,
|
||||
{ 'flappy-app--playing': !flappy.menuOpen && game },
|
||||
]"
|
||||
:aria-label="phone.t('Apps.skyFlappy.name')"
|
||||
>
|
||||
<header v-if="flappy.menuOpen" class="flappy-header">
|
||||
<div><span>{{ phone.t('Apps.skyFlappy.eyebrow') }}</span><h1>{{ phone.t('Apps.skyFlappy.name') }}</h1></div>
|
||||
<button type="button" :aria-label="phone.t(flappy.soundEnabled ? 'Apps.skyFlappy.mute' : 'Apps.skyFlappy.unmute')" @click="toggleSound">
|
||||
<Volume2 v-if="flappy.soundEnabled" :size="18" /><VolumeX v-else :size="18" />
|
||||
@@ -160,10 +167,11 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.flappy-app { --sky-a:#50d8f2;--sky-b:#765ce8;--tower:#574be8;--tower-light:#9b94ff;--tower-dark:#3429a6;--tower-glow:#79e7ff; position:absolute;inset:0;overflow:hidden;padding:52px 16px 27px;color:#fff;background:linear-gradient(160deg,#19375e,#433b80);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;user-select:none;touch-action:manipulation; }
|
||||
.flappy-app--playing { padding:0; }
|
||||
.flappy-app--neon { --sky-a:#151c58;--sky-b:#a329a2;--tower:#19dfe6;--tower-light:#82ffff;--tower-dark:#087f91;--tower-glow:#26fbff; }.flappy-app--storm { --sky-a:#6f8497;--sky-b:#2b3955;--tower:#df765f;--tower-light:#ffb18e;--tower-dark:#8d3e39;--tower-glow:#ff997d; }
|
||||
.flappy-header{height:55px;display:flex;align-items:center;justify-content:space-between}.flappy-header span{display:block;color:#d9e9fa;font-size:14px;font-weight:850;letter-spacing:1.1px;text-transform:uppercase}.flappy-header h1{margin:1px 0 0;font-size:32px;line-height:1}.flappy-header button,.flappy-toolbar button{width:36px;height:36px;display:grid;place-items:center;padding:0;border:1px solid #ffffff35;border-radius:12px;color:#fff;background:#ffffff18}
|
||||
.flappy-menu{height:calc(100% - 55px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center}.flappy-menu__hero{position:relative;width:220px;height:210px;overflow:hidden;border-radius:28px;background:linear-gradient(var(--sky-a),var(--sky-b));box-shadow:inset 0 0 28px #223c6d35,0 18px 32px #101b3b66}.flappy-menu__hero::before{position:absolute;z-index:0;top:22px;left:25px;width:39px;height:39px;border-radius:50%;background:#ffe8a7cc;box-shadow:0 0 18px #fff1b46b;content:""}.flappy-menu__hero::after{position:absolute;z-index:1;bottom:30px;left:-12px;width:68px;height:18px;border-radius:999px;background:#ffffff3d;box-shadow:27px -9px 0 3px #ffffff36,57px 1px 0 -2px #ffffff2c;content:""}.flappy-menu__tower{position:absolute;z-index:3;right:10px;width:42px;background:linear-gradient(90deg,color-mix(in srgb,var(--tower),white 30%),var(--tower));box-shadow:inset 7px 0 0 #ffffff22,inset -6px 0 9px #0003}.flappy-menu__tower::after{position:absolute;left:-8px;width:58px;height:22px;border:3px solid color-mix(in srgb,var(--tower),black 22%);border-radius:7px;background:linear-gradient(90deg,color-mix(in srgb,var(--tower),white 36%),var(--tower));box-shadow:inset 0 4px 0 #ffffff26;content:""}.flappy-menu__tower--top{top:0;height:61px;border-radius:0 0 5px 5px}.flappy-menu__tower--top::after{bottom:-11px}.flappy-menu__tower--bottom{bottom:0;height:67px;border-radius:5px 5px 0 0}.flappy-menu__tower--bottom::after{top:-11px}.flappy-menu__trail{position:absolute;z-index:2;top:102px;left:11px;width:46px;height:4px;border-radius:99px;background:#edfbffb8;box-shadow:13px 12px 0 -1px #e9faff8c,6px -12px 0 -1px #e9faff73;animation:flappy-trail 1s ease-in-out infinite}.sky-bird{position:absolute;z-index:4;width:66px;height:46px;overflow:visible;filter:drop-shadow(0 4px 5px #071d3e66)}.sky-bird :deep(path){stroke-linecap:round;stroke-linejoin:round}.sky-bird :deep(.sky-flappy-bird__far-wing){fill:#2d7188;stroke:#123f57;stroke-width:2}.sky-bird :deep(.sky-flappy-bird__far-wing path:last-child){fill:none;stroke:#8ec6ce;stroke-width:1.4;opacity:.65}.sky-bird :deep(.sky-flappy-bird__tail){fill:#285e78;stroke:#123f57;stroke-width:2}.sky-bird :deep(.sky-flappy-bird__body){fill:url(#sky-bird-body);stroke:#123f57;stroke-width:2}.sky-bird :deep(.sky-flappy-bird__belly){fill:#b7e5df;opacity:.84}.sky-bird :deep(.sky-flappy-bird__neck){fill:#377f98}.sky-bird :deep(.sky-flappy-bird__wing){transform-box:view-box;transform-origin:52px 38px}.sky-bird :deep(.sky-flappy-bird__wing-shape){fill:url(#sky-bird-wing);stroke:#123f57;stroke-width:2}.sky-bird :deep(.sky-flappy-bird__feather){fill:none;stroke:#d4f0ed;stroke-width:1.5;opacity:.78}.sky-bird :deep(.sky-flappy-bird__face){fill:#d8f0e9}.sky-bird :deep(.sky-flappy-bird__eye-ring){fill:#f5fbf7}.sky-bird :deep(.sky-flappy-bird__eye){fill:#102d3d}.sky-bird :deep(.sky-flappy-bird__beak){fill:#f2a84b;stroke:#6f4727;stroke-width:1.5}.sky-bird--hero{top:72px;left:42px;width:100px;height:69px;animation:flappy-hero 1.1s ease-in-out infinite alternate}.sky-bird--hero :deep(.sky-flappy-bird__wing){animation:flappy-bird-glide 1.1s ease-in-out infinite}.sky-bird--hero :deep(.sky-flappy-bird__far-wing){transform-box:view-box;transform-origin:50px 38px;animation:flappy-bird-far-glide 1.1s ease-in-out infinite}.flappy-menu__copy>span{color:#ffda70;font-size:9px;font-weight:900;letter-spacing:1px;text-transform:uppercase}.flappy-menu__copy h2{margin:3px 0 5px;font-size:21px}.flappy-menu__copy p{max-width:275px;margin:0;color:#c0c8df;font-size:10px;line-height:1.4}.flappy-record{width:100%;display:flex;align-items:center;justify-content:space-between;padding:9px 14px;border:1px solid #ffffff14;border-radius:13px;background:#ffffff0c}.flappy-record span{color:#bdc8df;font-size:9px;font-weight:800;text-transform:uppercase}.flappy-record strong{font-size:19px}.flappy-designs{width:100%;display:grid;grid-template-columns:repeat(3,1fr);gap:6px}.flappy-designs button{display:grid;place-items:center;gap:3px;padding:7px 3px;border:1px solid #ffffff12;border-radius:12px;color:#ccd4e9;background:#ffffff0a;font-size:8px}.flappy-designs button.active{border-color:#ffdd72;color:#fff;background:#ffffff1b}.flappy-designs i{width:26px;height:13px;border-radius:8px}.flappy-designs__dawn{background:linear-gradient(90deg,#58d6ee,#ff9b80)}.flappy-designs__neon{background:linear-gradient(90deg,#24255f,#ef55ca)}.flappy-designs__storm{background:linear-gradient(90deg,#71899b,#253750)}.flappy-primary,.flappy-secondary{width:100%;min-height:43px;display:flex;align-items:center;justify-content:center;gap:7px;border-radius:14px;font-size:11px;font-weight:850}.flappy-primary{border:0;color:#173353;background:linear-gradient(135deg,#ffe16c,#ff9d68)}.flappy-secondary{border:1px solid #ffffff18;color:#fff;background:#ffffff0b}.flappy-menu>p,.flappy-game__hint{margin:0;color:#aeb9d2;font-size:9px}
|
||||
.flappy-game{position:relative;height:calc(100% - 55px)}.flappy-toolbar{height:47px;display:grid;grid-template-columns:36px 1fr 1fr 36px;align-items:center;gap:7px}.flappy-toolbar div{display:grid;justify-items:center}.flappy-toolbar span{color:#bac9df;font-size:8px;font-weight:850;text-transform:uppercase}.flappy-toolbar strong{font-size:16px}.flappy-stage{position:relative;width:100%;height:500px;display:block;overflow:hidden;padding:0;border:1px solid #ffffff1c;border-radius:22px;background:linear-gradient(var(--sky-a),var(--sky-b));box-shadow:inset 0 0 35px #15244c55,0 16px 30px #10193477;touch-action:manipulation}.flappy-clouds{position:absolute;z-index:1;inset:0;overflow:hidden;pointer-events:none}.flappy-clouds i{--cloud-scale:1;--cloud-opacity:.34;--cloud-duration:18s;--cloud-delay:0s;position:absolute;left:100%;width:70px;height:18px;border-radius:999px;background:linear-gradient(180deg,#ffffffd9,#eaf7ff9c);box-shadow:0 8px 16px #24376518;opacity:var(--cloud-opacity);animation:cloud-drift var(--cloud-duration) linear var(--cloud-delay) infinite;will-change:transform}.flappy-clouds i::before{position:absolute;bottom:4px;left:12px;width:29px;height:29px;border-radius:50%;background:#f8fcffe6;box-shadow:22px -8px 0 4px #f7fcff,40px 1px 0 -2px #eef9ff;content:""}.flappy-clouds i::after{position:absolute;right:8px;bottom:-3px;left:8px;height:8px;border-radius:50%;background:#bcdff477;filter:blur(4px);content:""}.flappy-clouds i:nth-child(1){--cloud-scale:.7;--cloud-opacity:.3;--cloud-duration:20s;--cloud-delay:-4s;top:9%}.flappy-clouds i:nth-child(2){--cloud-scale:1.05;--cloud-opacity:.4;--cloud-duration:15s;--cloud-delay:-11s;top:22%}.flappy-clouds i:nth-child(3){--cloud-scale:.52;--cloud-opacity:.25;--cloud-duration:23s;--cloud-delay:-17s;top:38%}.flappy-clouds i:nth-child(4){--cloud-scale:.88;--cloud-opacity:.36;--cloud-duration:17s;--cloud-delay:-7s;top:53%}.flappy-clouds i:nth-child(5){--cloud-scale:1.18;--cloud-opacity:.42;--cloud-duration:14s;--cloud-delay:-2s;top:68%}.flappy-clouds i:nth-child(6){--cloud-scale:.62;--cloud-opacity:.27;--cloud-duration:21s;--cloud-delay:-14s;top:78%}.flappy-clouds i:nth-child(7){--cloud-scale:.96;--cloud-opacity:.34;--cloud-duration:16s;--cloud-delay:-9s;top:86%}.flappy-obstacle{position:absolute;z-index:2;top:0;bottom:0}.flappy-obstacle span{position:absolute;right:0;left:0;background:linear-gradient(90deg,color-mix(in srgb,var(--tower),white 20%),var(--tower));box-shadow:inset -6px 0 8px #0003,0 0 13px #17204e55}.flappy-obstacle span::after{position:absolute;right:-4px;left:-4px;height:14px;border-radius:6px;background:color-mix(in srgb,var(--tower),white 10%);box-shadow:inset 0 3px 0 #ffffff25;content:""}.flappy-obstacle__top{top:0;border-radius:0 0 7px 7px}.flappy-obstacle__top::after{bottom:0}.flappy-obstacle__bottom{bottom:0;border-radius:7px 7px 0 0}.flappy-obstacle__bottom::after{top:0}.sky-bird--player{left:23%;animation:flappy-wing .24s ease-out}.sky-bird--player :deep(.sky-flappy-bird__wing){animation:flappy-bird-flap .24s cubic-bezier(.2,.75,.35,1)}.sky-bird--player :deep(.sky-flappy-bird__far-wing){transform-box:view-box;transform-origin:50px 38px;animation:flappy-bird-far-flap .24s cubic-bezier(.2,.75,.35,1)}.flappy-ready{position:absolute;z-index:6;top:36%;left:50%;padding:9px 15px;border-radius:17px;background:#15284fbb;font-size:11px;transform:translateX(-50%)}.flappy-horizon{position:absolute;z-index:3;right:0;bottom:0;left:0;height:12px;background:#263a62;box-shadow:0 -5px 14px #ffffff26}.flappy-stage--crashed{animation:flappy-crash .55s ease-out}.flappy-game__hint{margin-top:8px;text-align:center}.flappy-overlay{position:absolute;z-index:12;inset:47px 0 25px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;padding:28px;border-radius:22px;background:#101a38dc;backdrop-filter:blur(7px);text-align:center}.flappy-overlay>svg{color:#ffdc71}.flappy-overlay>span{color:#ffad78;font-size:9px;font-weight:900;letter-spacing:1px;text-transform:uppercase}.flappy-overlay h2{margin:0 0 5px;font-size:26px}
|
||||
.flappy-game{position:absolute;inset:0}.flappy-toolbar{position:absolute;z-index:10;top:48px;right:14px;left:14px;height:42px;display:grid;grid-template-columns:36px 1fr 1fr 36px;align-items:center;gap:7px;padding:4px 6px;border:1px solid #ffffff2b;border-radius:22px;background:#263c6da8;box-shadow:0 8px 24px #10193455;backdrop-filter:blur(14px)}.flappy-toolbar div{display:grid;justify-items:center}.flappy-toolbar span{color:#bac9df;font-size:8px;font-weight:850;text-transform:uppercase}.flappy-toolbar strong{font-size:16px}.flappy-toolbar button{width:34px;height:34px;border:0;border-radius:50%;box-shadow:none}.flappy-stage{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:hidden;padding:0;border:0;border-radius:0;background:linear-gradient(var(--sky-a),var(--sky-b));box-shadow:inset 0 0 35px #15244c55;touch-action:manipulation}.flappy-clouds{position:absolute;z-index:1;inset:0;overflow:hidden;pointer-events:none}.flappy-clouds i{--cloud-scale:1;--cloud-opacity:.34;--cloud-duration:18s;--cloud-delay:0s;position:absolute;left:100%;width:70px;height:18px;border-radius:999px;background:linear-gradient(180deg,#ffffffd9,#eaf7ff9c);box-shadow:0 8px 16px #24376518;opacity:var(--cloud-opacity);animation:cloud-drift var(--cloud-duration) linear var(--cloud-delay) infinite;will-change:transform}.flappy-clouds i::before{position:absolute;bottom:4px;left:12px;width:29px;height:29px;border-radius:50%;background:#f8fcffe6;box-shadow:22px -8px 0 4px #f7fcff,40px 1px 0 -2px #eef9ff;content:""}.flappy-clouds i::after{position:absolute;right:8px;bottom:-3px;left:8px;height:8px;border-radius:50%;background:#bcdff477;filter:blur(4px);content:""}.flappy-clouds i:nth-child(1){--cloud-scale:.7;--cloud-opacity:.3;--cloud-duration:20s;--cloud-delay:-4s;top:9%}.flappy-clouds i:nth-child(2){--cloud-scale:1.05;--cloud-opacity:.4;--cloud-duration:15s;--cloud-delay:-11s;top:22%}.flappy-clouds i:nth-child(3){--cloud-scale:.52;--cloud-opacity:.25;--cloud-duration:23s;--cloud-delay:-17s;top:38%}.flappy-clouds i:nth-child(4){--cloud-scale:.88;--cloud-opacity:.36;--cloud-duration:17s;--cloud-delay:-7s;top:53%}.flappy-clouds i:nth-child(5){--cloud-scale:1.18;--cloud-opacity:.42;--cloud-duration:14s;--cloud-delay:-2s;top:68%}.flappy-clouds i:nth-child(6){--cloud-scale:.62;--cloud-opacity:.27;--cloud-duration:21s;--cloud-delay:-14s;top:78%}.flappy-clouds i:nth-child(7){--cloud-scale:.96;--cloud-opacity:.34;--cloud-duration:16s;--cloud-delay:-9s;top:86%}.flappy-obstacle{position:absolute;z-index:2;top:0;bottom:0}.flappy-obstacle span{position:absolute;right:0;left:0;background:linear-gradient(90deg,color-mix(in srgb,var(--tower),white 20%),var(--tower));box-shadow:inset -6px 0 8px #0003,0 0 13px #17204e55}.flappy-obstacle span::after{position:absolute;right:-4px;left:-4px;height:14px;border-radius:6px;background:color-mix(in srgb,var(--tower),white 10%);box-shadow:inset 0 3px 0 #ffffff25;content:""}.flappy-obstacle__top{top:0;border-radius:0 0 7px 7px}.flappy-obstacle__top::after{bottom:0}.flappy-obstacle__bottom{bottom:0;border-radius:7px 7px 0 0}.flappy-obstacle__bottom::after{top:0}.sky-bird--player{left:23%;animation:flappy-wing .24s ease-out}.sky-bird--player :deep(.sky-flappy-bird__wing){animation:flappy-bird-flap .24s cubic-bezier(.2,.75,.35,1)}.sky-bird--player :deep(.sky-flappy-bird__far-wing){transform-box:view-box;transform-origin:50px 38px;animation:flappy-bird-far-flap .24s cubic-bezier(.2,.75,.35,1)}.flappy-ready{position:absolute;z-index:6;top:36%;left:50%;padding:9px 15px;border-radius:17px;background:#15284fbb;font-size:11px;transform:translateX(-50%)}.flappy-horizon{position:absolute;z-index:3;right:0;bottom:0;left:0;height:12px;background:#263a62;box-shadow:0 -5px 14px #ffffff26}.flappy-stage--crashed{animation:flappy-crash .55s ease-out}.flappy-game__hint{position:absolute;z-index:6;right:45px;bottom:27px;left:45px;margin:0;padding:7px 10px;border-radius:999px;background:#263c6d91;backdrop-filter:blur(10px);text-align:center;pointer-events:none}.flappy-overlay{position:absolute;z-index:12;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;padding:28px;background:#101a38dc;backdrop-filter:blur(7px);text-align:center}.flappy-overlay>svg{color:#ffdc71}.flappy-overlay>span{color:#ffad78;font-size:9px;font-weight:900;letter-spacing:1px;text-transform:uppercase}.flappy-overlay h2{margin:0 0 5px;font-size:26px}
|
||||
.flappy-menu__tower{border:2px solid var(--tower-dark);background:linear-gradient(90deg,var(--tower-light),var(--tower),var(--tower-dark));box-shadow:inset 7px 0 0 #ffffff38,inset -6px 0 9px #0004,0 0 18px var(--tower-glow)}
|
||||
.flappy-menu__tower::after{border-color:var(--tower-dark);background:linear-gradient(90deg,var(--tower-light),var(--tower));box-shadow:inset 0 4px 0 #ffffff45,0 0 14px var(--tower-glow)}
|
||||
.flappy-menu__copy>span{color:#ffdf7d;font-size:14px}
|
||||
@@ -178,9 +186,13 @@ onBeforeUnmount(() => {
|
||||
.flappy-primary,.flappy-secondary{min-height:50px;gap:8px;font-size:18px}
|
||||
.flappy-secondary{border-color:#ffffff2c;background:#ffffff12}
|
||||
.flappy-menu>p,.flappy-game__hint{color:#e1e9f6;font-size:16px;font-weight:700;line-height:1.35}
|
||||
.flappy-toolbar span{color:#e0eafa;font-size:12px;letter-spacing:.35px}
|
||||
.flappy-toolbar strong{font-size:21px}
|
||||
.flappy-stage{border:2px solid #ffffff70;box-shadow:inset 0 0 35px #15244c38,0 16px 30px #10193477}
|
||||
.flappy-toolbar span{color:#e0eafa;font-size:11px;line-height:10px;letter-spacing:.35px}
|
||||
.flappy-toolbar strong{display:block;font-size:19px;line-height:20px}
|
||||
.flappy-toolbar{top:66px;right:18px;left:18px;height:42px;grid-template-columns:32px 1fr 1fr 32px;gap:4px;padding:4px;border-radius:21px;box-sizing:border-box}
|
||||
.flappy-toolbar div{height:32px;display:grid;grid-template-rows:10px 20px;align-content:center;justify-items:center}
|
||||
.flappy-toolbar button{width:32px;height:32px}
|
||||
.flappy-clouds{top:110px}
|
||||
.flappy-stage{border:0;box-shadow:inset 0 0 35px #15244c38}
|
||||
.flappy-obstacle span{border:2px solid var(--tower-dark);background:linear-gradient(90deg,var(--tower-light),var(--tower),var(--tower-dark));box-shadow:inset 7px 0 0 #ffffff42,inset -6px 0 8px #0004,0 0 18px var(--tower-glow)}
|
||||
.flappy-obstacle span::after{right:-5px;left:-5px;height:17px;border:2px solid var(--tower-dark);background:linear-gradient(180deg,var(--tower-light),var(--tower));box-shadow:inset 0 4px 0 #ffffff55,0 0 14px var(--tower-glow)}
|
||||
.flappy-ready{padding:11px 18px;border:2px solid #ffffff55;border-radius:19px;background:#15284fe6;font-size:16px;font-weight:850}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
Pause,
|
||||
Play,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, watch } from 'vue'
|
||||
|
||||
import {
|
||||
SNAKE_BOARD_HEIGHT,
|
||||
@@ -23,17 +20,7 @@ import { usePhoneStore } from '@/stores/phone'
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const snake = useSnakeStore()
|
||||
const touchStart = ref<SnakePoint | null>(null)
|
||||
const speedOptions: SnakeSpeed[] = ['relaxed', 'normal', 'fast']
|
||||
const directionButtons: Array<{
|
||||
direction: SnakeDirection
|
||||
icon: typeof ChevronUp
|
||||
}> = [
|
||||
{ direction: 'up', icon: ChevronUp },
|
||||
{ direction: 'left', icon: ChevronLeft },
|
||||
{ direction: 'down', icon: ChevronDown },
|
||||
{ direction: 'right', icon: ChevronRight },
|
||||
]
|
||||
const game = computed(() => snake.game)
|
||||
const boardMotionStyle = computed(() => ({
|
||||
'--snake-motion-duration': `${Math.min(
|
||||
@@ -90,7 +77,9 @@ function stopGameTimer(): void {
|
||||
function syncGameTimer(): void {
|
||||
stopGameTimer()
|
||||
if (snake.game?.status === 'playing') {
|
||||
gameTimer = setInterval(() => snake.tick(), snake.tickMilliseconds)
|
||||
gameTimer = setInterval(() => {
|
||||
snake.tick()
|
||||
}, snake.tickMilliseconds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,16 +88,16 @@ function returnToMenu(): void {
|
||||
snake.showMenu()
|
||||
}
|
||||
|
||||
function startGame(): void {
|
||||
snake.start()
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent): void {
|
||||
const directionByKey: Partial<Record<string, SnakeDirection>> = {
|
||||
ArrowDown: 'down',
|
||||
ArrowLeft: 'left',
|
||||
ArrowRight: 'right',
|
||||
ArrowUp: 'up',
|
||||
a: 'left',
|
||||
d: 'right',
|
||||
s: 'down',
|
||||
w: 'up',
|
||||
}
|
||||
const direction = directionByKey[event.key]
|
||||
|
||||
@@ -125,28 +114,6 @@ function handleKeydown(event: KeyboardEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
function beginSwipe(event: TouchEvent): void {
|
||||
const touch = event.changedTouches[0]
|
||||
touchStart.value = touch ? { x: touch.clientX, y: touch.clientY } : null
|
||||
}
|
||||
|
||||
function endSwipe(event: TouchEvent): void {
|
||||
const start = touchStart.value
|
||||
const touch = event.changedTouches[0]
|
||||
touchStart.value = null
|
||||
if (!start || !touch) return
|
||||
|
||||
const deltaX = touch.clientX - start.x
|
||||
const deltaY = touch.clientY - start.y
|
||||
if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 18) return
|
||||
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
snake.turn(deltaX > 0 ? 'right' : 'left')
|
||||
} else {
|
||||
snake.turn(deltaY > 0 ? 'down' : 'up')
|
||||
}
|
||||
}
|
||||
|
||||
snake.hydrate()
|
||||
watch(
|
||||
() => [snake.game?.status, snake.tickMilliseconds],
|
||||
@@ -162,8 +129,12 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="snake-app" :aria-label="phone.t('Apps.snake.name')">
|
||||
<header class="snake-header">
|
||||
<main
|
||||
class="snake-app"
|
||||
:class="{ 'snake-app--playing': game }"
|
||||
:aria-label="phone.t('Apps.snake.name')"
|
||||
>
|
||||
<header v-if="!game" class="snake-header">
|
||||
<span class="snake-brand">{{ phone.t('Apps.snake.name') }}</span>
|
||||
<div class="snake-score-card">
|
||||
<span>{{ phone.t('Apps.snake.highScore') }}</span>
|
||||
@@ -226,7 +197,7 @@ onBeforeUnmount(() => {
|
||||
{{ phone.t(`Apps.snake.speeds.${speed}`) }}
|
||||
</button>
|
||||
</fieldset>
|
||||
<button type="button" class="snake-primary" @click="snake.start">
|
||||
<button type="button" class="snake-primary" @click="startGame">
|
||||
<Play :size="18" fill="currentColor" />
|
||||
{{ phone.t('Apps.snake.start') }}
|
||||
</button>
|
||||
@@ -243,8 +214,10 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<ChevronLeft :size="18" :stroke-width="2.7" aria-hidden="true" />
|
||||
</button>
|
||||
<span>{{ phone.t('Apps.snake.score') }}</span>
|
||||
<strong>{{ game.score }}</strong>
|
||||
<div>
|
||||
<span>{{ phone.t('Apps.snake.score') }}</span>
|
||||
<strong>{{ game.score }}</strong>
|
||||
</div>
|
||||
<button
|
||||
v-if="game.status !== 'game-over'"
|
||||
type="button"
|
||||
@@ -267,8 +240,6 @@ onBeforeUnmount(() => {
|
||||
class="snake-board"
|
||||
:style="boardMotionStyle"
|
||||
:aria-label="phone.t('Apps.snake.board')"
|
||||
@touchstart.passive="beginSwipe"
|
||||
@touchend.passive="endSwipe"
|
||||
>
|
||||
<span
|
||||
v-for="(_, index) in game.body.slice(1)"
|
||||
@@ -306,7 +277,7 @@ onBeforeUnmount(() => {
|
||||
<template v-else>
|
||||
<span class="snake-overline">{{ phone.t('Apps.snake.score') }} {{ game.score }}</span>
|
||||
<h2>{{ phone.t('Apps.snake.gameOver') }}</h2>
|
||||
<button type="button" class="snake-primary" @click="snake.start">
|
||||
<button type="button" class="snake-primary" @click="startGame">
|
||||
{{ phone.t('Apps.snake.restart') }}
|
||||
</button>
|
||||
<button type="button" class="snake-secondary" @click="returnToMenu">
|
||||
@@ -317,18 +288,6 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<p class="snake-hint">{{ phone.t('Apps.snake.swipeHint') }}</p>
|
||||
<div class="snake-controls" :aria-label="phone.t('Apps.snake.controls')">
|
||||
<button
|
||||
v-for="control in directionButtons"
|
||||
:key="control.direction"
|
||||
type="button"
|
||||
:class="`snake-control--${control.direction}`"
|
||||
:aria-label="phone.t(`Apps.snake.directions.${control.direction}`)"
|
||||
@click="snake.turn(control.direction)"
|
||||
>
|
||||
<component :is="control.icon" :size="24" :stroke-width="2.6" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
@@ -348,6 +307,10 @@ onBeforeUnmount(() => {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.snake-app--playing {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.snake-header,
|
||||
.snake-game__meta {
|
||||
display: flex;
|
||||
@@ -507,53 +470,89 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.snake-game {
|
||||
padding-top: 8px;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.snake-game__meta {
|
||||
height: 43px;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
position: absolute;
|
||||
z-index: 7;
|
||||
top: 66px;
|
||||
right: 18px;
|
||||
left: 18px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr 32px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid rgb(255 255 255 / 9%);
|
||||
border-radius: 21px;
|
||||
background: rgb(8 22 18 / 58%);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
|
||||
backdrop-filter: blur(14px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.snake-game__meta div {
|
||||
height: 32px;
|
||||
display: grid;
|
||||
grid-template-rows: 10px 20px;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.snake-game__meta span {
|
||||
color: #9fb3a9;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.snake-game__meta strong {
|
||||
font-size: 22px;
|
||||
display: block;
|
||||
font-size: 19px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.snake-game__meta button {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid rgb(255 255 255 / 9%);
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: #dff6d9;
|
||||
background: rgb(255 255 255 / 7%);
|
||||
background: rgb(255 255 255 / 8%);
|
||||
}
|
||||
|
||||
.snake-game__meta .snake-game__pause { margin-left: auto; }
|
||||
.snake-game__meta .snake-game__back { flex: 0 0 auto; }
|
||||
.snake-game__meta .snake-game__pause { justify-self: end; }
|
||||
.snake-game__meta .snake-game__back { justify-self: start; }
|
||||
|
||||
.snake-board {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 18;
|
||||
position: absolute;
|
||||
top: 118px;
|
||||
right: 18px;
|
||||
bottom: 62px;
|
||||
left: 18px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(167 231 157 / 16%);
|
||||
border: 1px solid rgb(145 228 117 / 24%);
|
||||
border-radius: 18px;
|
||||
background-color: #162d26;
|
||||
background-image:
|
||||
linear-gradient(rgb(255 255 255 / 2%) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgb(255 255 255 / 2%) 1px, transparent 1px);
|
||||
background-size: calc(100% / 16) calc(100% / 18);
|
||||
box-shadow: inset 0 0 35px rgb(0 0 0 / 20%), 0 17px 35px rgb(0 0 0 / 20%);
|
||||
linear-gradient(rgb(173 233 160 / 4%) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgb(173 233 160 / 4%) 1px, transparent 1px),
|
||||
radial-gradient(circle at 50% 42%, rgb(94 191 91 / 8%), transparent 68%);
|
||||
background-size:
|
||||
calc(100% / 16) calc(100% / 30),
|
||||
calc(100% / 16) calc(100% / 30),
|
||||
100% 100%;
|
||||
box-shadow:
|
||||
inset 0 0 55px rgb(0 0 0 / 24%),
|
||||
inset 0 0 0 1px rgb(213 255 199 / 3%),
|
||||
0 12px 30px rgb(0 0 0 / 20%),
|
||||
0 0 18px rgb(89 196 82 / 7%);
|
||||
}
|
||||
|
||||
.snake-head,
|
||||
@@ -679,36 +678,22 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.snake-hint {
|
||||
margin: 9px 0 6px;
|
||||
color: #71867c;
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
right: 50px;
|
||||
bottom: 27px;
|
||||
left: 50px;
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
border-radius: 999px;
|
||||
color: #9eb2a8;
|
||||
background: rgb(8 22 18 / 48%);
|
||||
backdrop-filter: blur(10px);
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.snake-controls {
|
||||
position: relative;
|
||||
width: 132px;
|
||||
height: 91px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.snake-controls button {
|
||||
position: absolute;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid rgb(255 255 255 / 9%);
|
||||
border-radius: 13px;
|
||||
color: #cae6d1;
|
||||
background: rgb(255 255 255 / 7%);
|
||||
}
|
||||
|
||||
.snake-control--up { left: 45px; top: 0; }
|
||||
.snake-control--left { left: 0; top: 45px; }
|
||||
.snake-control--down { left: 45px; top: 45px; }
|
||||
.snake-control--right { right: 0; top: 45px; }
|
||||
|
||||
button:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import { usePhoneStore } from '@/stores/phone'
|
||||
const phone = usePhoneStore()
|
||||
const tower = useTowerStackStore()
|
||||
const game = computed(() => tower.game)
|
||||
const visibleBlocks = computed(() => game.value?.blocks.slice(-9) ?? [])
|
||||
const visibleBlocks = computed(() => game.value?.blocks.slice(-6) ?? [])
|
||||
const placementEffect = ref<'missed' | 'perfect' | 'placed' | null>(null)
|
||||
const fallingBlock = ref<TowerActiveBlock | null>(null)
|
||||
const fallingStyle = ref<CSSProperties>({})
|
||||
@@ -175,8 +175,12 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="tower-app" :aria-label="phone.t('Apps.towerStack.name')">
|
||||
<header class="tower-header">
|
||||
<main
|
||||
class="tower-app"
|
||||
:class="{ 'tower-app--playing': !tower.menuOpen && game }"
|
||||
:aria-label="phone.t('Apps.towerStack.name')"
|
||||
>
|
||||
<header v-if="tower.menuOpen" class="tower-header">
|
||||
<div>
|
||||
<span>{{ phone.t('Apps.towerStack.eyebrow') }}</span>
|
||||
<h1>{{ phone.t('Apps.towerStack.name') }}</h1>
|
||||
@@ -214,7 +218,13 @@ onBeforeUnmount(() => {
|
||||
<Play :size="17" fill="currentColor" />
|
||||
{{ phone.t('Apps.towerStack.resume') }}
|
||||
</button>
|
||||
<button type="button" class="tower-secondary" @click="startGame">
|
||||
<button
|
||||
type="button"
|
||||
:class="game?.status === 'paused' ? 'tower-secondary' : 'tower-primary'"
|
||||
@click="startGame"
|
||||
>
|
||||
<RotateCcw v-if="game" :size="16" aria-hidden="true" />
|
||||
<Play v-else :size="16" fill="currentColor" aria-hidden="true" />
|
||||
{{ phone.t(game ? 'Apps.towerStack.newGame' : 'Apps.towerStack.start') }}
|
||||
</button>
|
||||
<p class="tower-menu__hint">{{ phone.t('Apps.towerStack.tapHint') }}</p>
|
||||
@@ -305,32 +315,34 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.tower-app { position: absolute; inset: 0; overflow: hidden; padding: 52px 16px 27px; color: #eef5ff; background: radial-gradient(circle at 75% 8%, #7146c866, transparent 35%), linear-gradient(170deg, #161634, #242054 52%, #10132c); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; user-select: none; touch-action: manipulation; }
|
||||
.tower-header { height: 55px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.tower-header span { display: block; color: #c1b8f1; font-size: 14px; font-weight: 850; letter-spacing: 1.1px; text-transform: uppercase; }
|
||||
.tower-header h1 { margin: 1px 0 0; font-size: 32px; line-height: 1; letter-spacing: -0.8px; }
|
||||
.tower-app--playing { padding: 0; }
|
||||
.tower-header { height: 50px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.tower-header span { display: block; color: #c1b8f1; font-size: 10px; font-weight: 850; letter-spacing: 1.1px; text-transform: uppercase; }
|
||||
.tower-header h1 { margin: 1px 0 0; font-size: 27px; line-height: 1; letter-spacing: -0.8px; }
|
||||
.tower-header button, .tower-toolbar button { width: 36px; height: 36px; display: grid; place-items: center; padding: 0; border: 1px solid #ffffff14; border-radius: 12px; color: #f2edff; background: #ffffff0d; }
|
||||
.tower-menu { height: calc(100% - 55px); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; text-align: center; }
|
||||
.tower-menu__preview { position: relative; width: 190px; height: 210px; }
|
||||
.tower-menu__preview i { position: absolute; right: 12px; bottom: calc(var(--preview-index) * 25px); left: 25px; height: 29px; border-radius: 7px; background: hsl(calc(var(--preview-index) * 49deg + 5deg) 82% 62%); box-shadow: inset 0 4px 0 #ffffff35, 0 8px 15px #08091c5c; transform: perspective(200px) rotateX(5deg); }
|
||||
.tower-menu__preview i:nth-child(even) { right: 25px; left: 12px; }
|
||||
.tower-menu__preview span { position: absolute; top: 4px; left: 2px; width: 120px; height: 29px; border-radius: 7px; background: #ff6b68; box-shadow: 0 0 24px #ff6b6877; animation: tower-preview-slide 1.5s ease-in-out infinite alternate; }
|
||||
.tower-menu__copy > span { color: #ffcf59; font-size: 14px; font-weight: 900; letter-spacing: 1px; text-transform: uppercase; }
|
||||
.tower-menu__copy h2 { margin: 4px 0 7px; font-size: 30px; }
|
||||
.tower-menu__copy p { max-width: 295px; margin: 0; color: #e0dcf2; font-size: 18px; font-weight: 550; line-height: 1.4; }
|
||||
.tower-menu { height: calc(100% - 50px); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 9px; text-align: center; }
|
||||
.tower-menu__preview { position: relative; flex: 0 0 124px; width: 158px; height: 124px; }
|
||||
.tower-menu__preview i { position: absolute; right: 12px; bottom: calc((var(--preview-index) - 1) * 15px); left: 23px; height: 21px; border-radius: 6px; background: hsl(calc(var(--preview-index) * 49deg + 5deg) 82% 62%); box-shadow: inset 0 3px 0 #ffffff35, 0 5px 11px #08091c5c; transform: perspective(200px) rotateX(5deg); }
|
||||
.tower-menu__preview i:nth-child(even) { right: 23px; left: 12px; }
|
||||
.tower-menu__preview span { position: absolute; top: 0; left: 4px; width: 100px; height: 21px; border-radius: 6px; background: #ff6b68; box-shadow: 0 0 20px #ff6b6877; animation: tower-preview-slide 1.5s ease-in-out infinite alternate; }
|
||||
.tower-menu__copy > span { color: #ffcf59; font-size: 10px; font-weight: 900; letter-spacing: 1px; text-transform: uppercase; }
|
||||
.tower-menu__copy h2 { max-width: 270px; margin: 3px auto 5px; font-size: 24px; line-height: 1.08; }
|
||||
.tower-menu__copy p { max-width: 285px; margin: 0; color: #d8d3eb; font-size: 12px; font-weight: 550; line-height: 1.35; }
|
||||
.tower-records { width: 100%; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.tower-records div { display: grid; gap: 3px; padding: 12px 9px; border: 1px solid #ffffff1f; border-radius: 13px; background: #ffffff0d; }
|
||||
.tower-records span { color: #d1caea; font-size: 15px; font-weight: 850; text-transform: uppercase; }
|
||||
.tower-records strong { font-size: 26px; }
|
||||
.tower-primary, .tower-secondary { width: 100%; min-height: 50px; display: flex; align-items: center; justify-content: center; gap: 8px; border-radius: 14px; font-size: 18px; font-weight: 850; }
|
||||
.tower-records div { display: grid; gap: 1px; padding: 8px 9px; border: 1px solid #ffffff1f; border-radius: 13px; background: #ffffff0d; backdrop-filter: blur(8px); }
|
||||
.tower-records span { color: #d1caea; font-size: 9px; font-weight: 850; letter-spacing: .35px; text-transform: uppercase; }
|
||||
.tower-records strong { font-size: 20px; }
|
||||
.tower-primary, .tower-secondary { width: 100%; min-height: 42px; display: flex; align-items: center; justify-content: center; gap: 7px; border-radius: 14px; font-size: 13px; font-weight: 850; }
|
||||
.tower-primary { border: 0; color: #1e1839; background: linear-gradient(135deg, #ffca4f, #ff8760); box-shadow: 0 8px 18px #ff895330; }
|
||||
.tower-secondary { border: 1px solid #ffffff16; color: #eeeaff; background: #ffffff0a; }
|
||||
.tower-menu__hint, .tower-game__hint { margin: 0; color: #e0daf0; font-size: 16px; font-weight: 700; line-height: 1.35; }
|
||||
.tower-game { position: relative; height: calc(100% - 55px); }
|
||||
.tower-toolbar { height: 47px; display: grid; grid-template-columns: 36px 1fr 1fr 36px; align-items: center; gap: 7px; }
|
||||
.tower-toolbar div { display: grid; justify-items: center; line-height: 1.05; }
|
||||
.tower-toolbar span { color: #c9c2e9; font-size: 12px; font-weight: 850; letter-spacing: .35px; text-transform: uppercase; }
|
||||
.tower-toolbar strong { font-size: 21px; }
|
||||
.tower-stage { position: relative; width: 100%; height: 500px; display: block; overflow: hidden; padding: 0; border: 1px solid #ffffff35; border-radius: 22px; background: linear-gradient(#1d1b52, #433078 60%, #8e4c78); box-shadow: inset 0 0 35px #08091d80, 0 16px 28px #08091c66; touch-action: manipulation; }
|
||||
.tower-menu__hint, .tower-game__hint { margin: 0; color: #c9c2e0; font-size: 10px; font-weight: 700; line-height: 1.35; }
|
||||
.tower-game { position: absolute; inset: 0; }
|
||||
.tower-toolbar { position: absolute; z-index: 10; top: 66px; right: 18px; left: 18px; height: 42px; display: grid; grid-template-columns: 32px 1fr 1fr 32px; align-items: center; gap: 4px; padding: 4px; border: 1px solid #ffffff1a; border-radius: 21px; background: #292451a8; box-shadow: 0 8px 24px #08091c52; backdrop-filter: blur(14px); box-sizing: border-box; }
|
||||
.tower-toolbar div { height: 32px; display: grid; grid-template-rows: 10px 20px; align-content: center; justify-items: center; }
|
||||
.tower-toolbar span { color: #c9c2e9; font-size: 11px; font-weight: 850; line-height: 10px; letter-spacing: .35px; text-transform: uppercase; }
|
||||
.tower-toolbar strong { display: block; font-size: 19px; line-height: 20px; }
|
||||
.tower-toolbar button { width: 32px; height: 32px; border: 0; border-radius: 50%; box-shadow: none; }
|
||||
.tower-stage { position: absolute; inset: 0; width: 100%; height: 100%; display: block; overflow: hidden; padding: 0; border: 0; border-radius: 0; background: linear-gradient(#1d1b52, #433078 60%, #8e4c78); box-shadow: inset 0 0 35px #08091d80; touch-action: manipulation; }
|
||||
.tower-sky { position: absolute; inset: 0; pointer-events: none; }
|
||||
.tower-sky i { position: absolute; width: 3px; height: 3px; border-radius: 50%; background: #fff; box-shadow: 0 0 7px #c5c2ff; opacity: .65; }
|
||||
.tower-sky i:nth-child(1) { top: 8%; left: 12%; } .tower-sky i:nth-child(2) { top: 17%; left: 72%; } .tower-sky i:nth-child(3) { top: 28%; left: 42%; } .tower-sky i:nth-child(4) { top: 37%; left: 88%; } .tower-sky i:nth-child(5) { top: 46%; left: 18%; } .tower-sky i:nth-child(6) { top: 58%; left: 64%; } .tower-sky i:nth-child(7) { top: 70%; left: 31%; } .tower-sky i:nth-child(8) { top: 11%; left: 91%; } .tower-sky i:nth-child(9) { top: 22%; left: 25%; } .tower-sky i:nth-child(10) { top: 50%; left: 78%; } .tower-sky i:nth-child(11) { top: 64%; left: 8%; } .tower-sky i:nth-child(12) { top: 77%; left: 92%; } .tower-sky i:nth-child(13) { top: 33%; left: 58%; }
|
||||
@@ -343,13 +355,13 @@ onBeforeUnmount(() => {
|
||||
.tower-perfect { position: absolute; z-index: 8; top: 24%; left: 50%; padding: 8px 17px; border-radius: 18px; color: #332149; background: #ffdc65; box-shadow: 0 0 24px #ffcf64aa; font-size: 13px; font-weight: 950; letter-spacing: .8px; transform: translateX(-50%); animation: tower-perfect-pop 650ms ease-out forwards; }
|
||||
.tower-stage--perfect { animation: tower-perfect-glow 500ms ease-out; }
|
||||
.tower-stage--missed { animation: tower-stage-shake 500ms ease-out; }
|
||||
.tower-game__hint { margin-top: 8px; text-align: center; }
|
||||
.tower-overlay { position: absolute; z-index: 15; inset: 47px 0 25px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 9px; padding: 28px; border-radius: 22px; color: #f7f2ff; background: #11122bd9; backdrop-filter: blur(7px); text-align: center; }
|
||||
.tower-game__hint { position: absolute; z-index: 6; right: 45px; bottom: 27px; left: 45px; margin: 0; padding: 7px 10px; border-radius: 999px; color: #e0daf0; background: #29245191; backdrop-filter: blur(10px); text-align: center; pointer-events: none; }
|
||||
.tower-overlay { position: absolute; z-index: 15; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 9px; padding: 28px; color: #f7f2ff; background: #11122bd9; backdrop-filter: blur(7px); text-align: center; }
|
||||
.tower-overlay > svg { color: #ffbd4d; }
|
||||
.tower-overlay > span { color: #ffad78; font-size: 12px; font-weight: 900; letter-spacing: 1.2px; text-transform: uppercase; }
|
||||
.tower-overlay h2 { margin: 0; font-size: 25px; }
|
||||
.tower-overlay p { margin: -3px 0 5px; color: #c7c1e4; font-size: 14px; }
|
||||
@keyframes tower-preview-slide { from { transform: translateX(0) rotate(-2deg); } to { transform: translateX(66px) rotate(2deg); } }
|
||||
@keyframes tower-preview-slide { from { transform: translateX(0) rotate(-2deg); } to { transform: translateX(48px) rotate(2deg); } }
|
||||
@keyframes tower-land { from { filter: brightness(1.8); transform: translateY(-8px) scaleY(.85); } to { filter: brightness(1); transform: translateY(0) scaleY(1); } }
|
||||
@keyframes tower-active-pulse { from { filter: saturate(1.2) brightness(1.08); } to { filter: saturate(1.45) brightness(1.35); } }
|
||||
@keyframes tower-fall { 0% { opacity: 1; transform: translate(0) rotate(0); } 100% { opacity: 0; transform: translate(45px, 390px) rotate(145deg); } }
|
||||
|
||||
@@ -159,7 +159,7 @@ Locales["en"] = {
|
||||
readyBody = "Collect fruit, grow longer, and stay clear of every wall.", readyTitle = "Ready to play?",
|
||||
restart = "Play Again", resume = "Resume game", score = "Score", speed = "Speed",
|
||||
speeds = { fast = "Fast", normal = "Normal", relaxed = "Relaxed" }, start = "Start Game",
|
||||
swipeHint = "Swipe, tap the controls, or use arrow keys / WASD",
|
||||
swipeHint = "Use the arrow keys to steer",
|
||||
},
|
||||
memory = {
|
||||
name = "Memory", backToMenu = "Back to board selection", board = "Memory card board",
|
||||
@@ -185,7 +185,7 @@ Locales["en"] = {
|
||||
keepPlaying = "Keep Playing", mainMenu = "Main Menu",
|
||||
menuBody = "Slide matching numbers together and work your way up to 2048.", menuTitle = "Build the 2048 tile",
|
||||
mute = "Mute game sounds", newGame = "New Game", score = "Score",
|
||||
swipeHint = "Swipe the board, use the buttons, or press arrow keys / WASD",
|
||||
swipeHint = "Swipe the board or use arrow keys / WASD",
|
||||
unmute = "Turn on game sounds",
|
||||
wonBody = "You reached the legendary tile. Continue climbing or begin again.", wonTitle = "You made 2048!",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user