mlv2boii 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +273 -191
- package/package.json +1 -1
package/index.html
CHANGED
|
@@ -7,107 +7,129 @@
|
|
|
7
7
|
<meta name="description" content="mlv2: a small, fast library of games." />
|
|
8
8
|
<meta name="color-scheme" content="dark" />
|
|
9
9
|
<meta name="theme-color" content="#000000" />
|
|
10
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23000'/%3E%3Ccircle cx='16' cy='16' r='
|
|
10
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23000'/%3E%3Ccircle cx='16' cy='16' r='7' fill='%23f01a36'/%3E%3Ccircle cx='14' cy='13' r='2.2' fill='%23fff' opacity='.7'/%3E%3C/svg%3E" />
|
|
11
11
|
|
|
12
|
-
<!-- Fonts: SF Pro on Apple devices (system stack), Inter everywhere else -->
|
|
13
12
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
14
13
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
15
14
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
16
15
|
|
|
17
|
-
<!-- Tailwind CSS v4 (Play CDN, mirrored on jsDelivr) -->
|
|
18
16
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
19
17
|
<style type="text/tailwindcss">
|
|
20
18
|
/*
|
|
21
|
-
Design language
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Type: one family. 17/19/21 for reading, 13 for labels, 44-72 for the title.
|
|
25
|
-
Shape: 10px controls, 12px groups, 28px sheets (concentric: 12 + 16 padding), pills for chips,
|
|
26
|
-
22.37% on game icons (the iOS app icon ratio).
|
|
27
|
-
Motion: 150-300ms ease-out on interaction, one entrance, all of it off under reduced-motion.
|
|
19
|
+
Design language: void black + ambient colour glow, glass on every floating layer,
|
|
20
|
+
glossy "inflated" candy elements (orbs, badges, chips, buttons, hero shapes).
|
|
21
|
+
Brand red-pink is the lead; orange / lime / blue / purple are category colours.
|
|
28
22
|
*/
|
|
29
23
|
@theme {
|
|
30
24
|
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
31
25
|
--font-mono: ui-monospace, "SF Mono", Menlo, monospace;
|
|
32
|
-
|
|
33
|
-
--color-ink-
|
|
34
|
-
--color-ink-
|
|
35
|
-
--color-
|
|
36
|
-
--color-ink-800: #161618;
|
|
37
|
-
--color-ink-700: #232326;
|
|
38
|
-
--color-ink-600: #2c2c30;
|
|
39
|
-
--color-ink-500: #48484c;
|
|
40
|
-
--color-ink-400: #76767b;
|
|
41
|
-
--color-ink-300: #a1a1a6;
|
|
42
|
-
--color-ink-200: #c7c7cc;
|
|
43
|
-
--color-ink-100: #e5e5ea;
|
|
44
|
-
--color-ink-50: #f5f5f7;
|
|
45
|
-
|
|
46
|
-
--color-red-700: #8f0e20;
|
|
47
|
-
--color-red-600: #c11228;
|
|
48
|
-
--color-red-500: #e8142f;
|
|
49
|
-
--color-red-400: #ff4d61;
|
|
50
|
-
--color-red-300: #ff8b96;
|
|
26
|
+
--color-ink-950: #000000; --color-ink-900: #0a0a0b; --color-ink-850: #101012; --color-ink-800: #161618;
|
|
27
|
+
--color-ink-700: #232326; --color-ink-600: #2c2c30; --color-ink-500: #48484c; --color-ink-400: #76767b;
|
|
28
|
+
--color-ink-300: #a1a1a6; --color-ink-200: #c7c7cc; --color-ink-100: #e5e5ea; --color-ink-50: #f5f5f7;
|
|
29
|
+
--color-red-700: #8f0e20; --color-red-600: #c11228; --color-red-500: #f01a36; --color-red-400: #ff4d63; --color-red-300: #ff8b96;
|
|
51
30
|
}
|
|
52
|
-
|
|
53
31
|
@layer components {
|
|
54
|
-
.btn { @apply inline-flex h-11 items-center justify-center rounded-
|
|
55
|
-
.btn
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
.field { @apply h-11 w-full rounded-[10px] bg-white/[0.07] px-3 text-[16px] text-ink-50 ring-1 ring-inset ring-white/10 transition-shadow duration-200 placeholder:text-ink-400 focus:ring-2 focus:ring-red-500 sm:h-9 sm:text-[14px]; }
|
|
59
|
-
.chip { @apply h-10 shrink-0 whitespace-nowrap rounded-full px-4 text-[14px] font-medium transition-colors duration-200 sm:h-9; }
|
|
32
|
+
.btn { @apply inline-flex h-11 items-center justify-center rounded-full px-5 text-[15px] font-semibold transition-[background-color,filter,transform] duration-200 active:scale-[0.97] disabled:opacity-50 sm:h-9 sm:text-[14px]; }
|
|
33
|
+
.icon-btn { @apply grid h-11 w-11 shrink-0 place-items-center rounded-full text-ink-200 transition-colors duration-200 hover:text-white sm:h-9 sm:w-9; }
|
|
34
|
+
.field { @apply h-11 w-full rounded-[12px] bg-white/[0.07] px-3 text-[16px] text-ink-50 ring-1 ring-inset ring-white/10 transition-shadow duration-200 placeholder:text-ink-400 focus:ring-2 focus:ring-red-500 sm:h-9 sm:text-[14px]; }
|
|
35
|
+
.chip { @apply inline-flex h-10 shrink-0 items-center gap-2 whitespace-nowrap rounded-full pl-4 pr-1.5 text-[14px] font-medium transition-colors duration-200 sm:h-9; }
|
|
60
36
|
}
|
|
61
37
|
</style>
|
|
62
38
|
|
|
63
39
|
<style>
|
|
64
|
-
html
|
|
40
|
+
html { background: #000; }
|
|
65
41
|
body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
|
|
66
|
-
::selection { background: rgba(
|
|
42
|
+
::selection { background: rgba(240,26,54,0.4); color: #fff; }
|
|
67
43
|
input { caret-color: var(--color-red-400); }
|
|
68
|
-
|
|
69
|
-
/* keyboard focus: one red ring everywhere */
|
|
70
44
|
:focus-visible { outline: 2px solid var(--color-red-400); outline-offset: 2px; }
|
|
71
45
|
input:focus-visible { outline: none; }
|
|
72
46
|
.tile:focus-visible { outline: none; }
|
|
73
47
|
.tile:focus-visible .tile-icon { outline: 2px solid var(--color-red-400); outline-offset: 3px; }
|
|
74
|
-
|
|
75
|
-
/* safe areas (viewport-fit=cover) */
|
|
76
48
|
.page-x { padding-left: max(1.25rem, env(safe-area-inset-left)); padding-right: max(1.25rem, env(safe-area-inset-right)); }
|
|
77
|
-
|
|
78
|
-
/* game icons: 10% edge so dark art never melts into the canvas */
|
|
79
49
|
.tile-img { outline: 1px solid rgba(255,255,255,0.1); outline-offset: -1px; }
|
|
80
50
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.glass-sheet {
|
|
91
|
-
|
|
92
|
-
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
|
93
|
-
backdrop-filter: blur(40px) saturate(180%);
|
|
94
|
-
}
|
|
95
|
-
.glass::before {
|
|
96
|
-
content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 1px; pointer-events: none;
|
|
51
|
+
/* ---------- ambient colour: gives the glass something to refract ---------- */
|
|
52
|
+
.ambient { position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
|
53
|
+
background:
|
|
54
|
+
radial-gradient(680px 420px at 85% 2%, rgba(240,26,54,0.11), transparent 70%),
|
|
55
|
+
radial-gradient(720px 520px at 8% 108%, rgba(240,26,54,0.06), transparent 70%); }
|
|
56
|
+
|
|
57
|
+
/* ---------- liquid glass ---------- */
|
|
58
|
+
.glass { background: rgba(255,255,255,0.06); -webkit-backdrop-filter: blur(24px) saturate(180%); backdrop-filter: blur(24px) saturate(180%);
|
|
59
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.22), inset 0 -1px 0 rgba(255,255,255,0.04), 0 10px 32px rgba(0,0,0,0.45); }
|
|
60
|
+
.glass-sheet { background: rgba(28,28,31,0.55); -webkit-backdrop-filter: blur(40px) saturate(180%); backdrop-filter: blur(40px) saturate(180%); }
|
|
61
|
+
.glass::before { content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 1px; pointer-events: none;
|
|
97
62
|
background: linear-gradient(135deg, rgba(255,255,255,0.4), rgba(255,255,255,0.05) 40%, rgba(255,255,255,0.05) 60%, rgba(255,255,255,0.22));
|
|
98
|
-
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
99
|
-
-webkit-mask-composite: xor; mask-composite: exclude;
|
|
100
|
-
}
|
|
101
|
-
/* small controls: same edge light, no blur (nothing behind them to blur) */
|
|
63
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; }
|
|
102
64
|
.glass-lite { background: rgba(255,255,255,0.07); box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), inset 0 0 0 1px rgba(255,255,255,0.08); }
|
|
103
65
|
.glass-lite:hover { background: rgba(255,255,255,0.12); }
|
|
104
|
-
|
|
105
|
-
@
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
66
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .glass { background: rgba(22,22,24,0.94); } .glass-sheet { background: rgba(18,18,20,0.97); } }
|
|
67
|
+
@media (prefers-reduced-transparency: reduce) { .glass, .glass-sheet { -webkit-backdrop-filter: none; backdrop-filter: none; background: #161618; } }
|
|
68
|
+
|
|
69
|
+
/* ---------- bubbly / inflated ---------- */
|
|
70
|
+
.orb { width: 14px; height: 14px; border-radius: 999px; flex-shrink: 0;
|
|
71
|
+
background: radial-gradient(circle at 32% 28%, #fff 0 12%, #ff7a8a 28%, #f01a36 62%, #a30c22);
|
|
72
|
+
box-shadow: 0 0 14px rgba(240,26,54,0.8), inset 0 -2px 3px rgba(0,0,0,0.25); }
|
|
73
|
+
.badge { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 14px 0 10px; border-radius: 999px; font-size: 13px; font-weight: 600; color: #fff;
|
|
74
|
+
background: linear-gradient(180deg, #ff6b7d, #f01a36 60%, #c80f27);
|
|
75
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.55), inset 0 -2px 0 rgba(0,0,0,0.16), 0 6px 22px -2px rgba(240,26,54,0.7); }
|
|
76
|
+
.glow-line { display: inline-block; padding: 0 0.04em 0.1em;
|
|
77
|
+
background: linear-gradient(180deg, #ffb8c0 0%, #ff4d63 48%, #f01a36 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
|
78
|
+
.btn-primary { color: #fff; background: linear-gradient(180deg, #ff6b7d, #f01a36 55%, #c80f27);
|
|
79
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), inset 0 -2px 0 rgba(0,0,0,0.18), 0 8px 22px -6px rgba(240,26,54,0.7); }
|
|
80
|
+
.btn-primary:hover { filter: brightness(1.08); }
|
|
81
|
+
.btn-ghost { color: var(--color-ink-50); background: rgba(255,255,255,0.08); box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), inset 0 0 0 1px rgba(255,255,255,0.08); }
|
|
82
|
+
.btn-ghost:hover { background: rgba(255,255,255,0.14); }
|
|
83
|
+
.chip-on { background: linear-gradient(180deg, #ff6b7d, #f01a36 60%, #c80f27);
|
|
84
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.5), inset 0 -2px 0 rgba(0,0,0,0.16), 0 6px 20px -4px rgba(240,26,54,0.65); }
|
|
85
|
+
.chip-count { display: grid; place-items: center; min-width: 26px; height: 26px; padding: 0 8px; border-radius: 999px; font-size: 12px; font-weight: 700; color: #111; background: var(--c);
|
|
86
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.6), 0 0 14px color-mix(in srgb, var(--c) 55%, transparent); }
|
|
87
|
+
.chip-on .chip-count { background: #fff; color: #b50e28; box-shadow: none; }
|
|
88
|
+
|
|
89
|
+
/* game tiles: glass card, glossy icon, glow in the category colour */
|
|
90
|
+
.tile { background: rgba(255,255,255,0.05); border-radius: 28px; padding: 10px 10px 12px;
|
|
91
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.16), inset 0 0 0 1px rgba(255,255,255,0.07);
|
|
92
|
+
transition: background-color .3s ease-out, box-shadow .3s ease-out; }
|
|
93
|
+
.tile:hover { background: color-mix(in srgb, var(--c) 13%, rgba(255,255,255,0.05));
|
|
94
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.28), inset 0 0 0 1px color-mix(in srgb, var(--c) 55%, transparent), 0 16px 40px -10px color-mix(in srgb, var(--c) 60%, transparent); }
|
|
95
|
+
.tile-icon { position: relative; box-shadow: 0 10px 24px -8px color-mix(in srgb, var(--c) 28%, transparent); }
|
|
96
|
+
.tile-icon::after { content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
|
|
97
|
+
background: linear-gradient(160deg, rgba(255,255,255,0.3), rgba(255,255,255,0) 42%);
|
|
98
|
+
box-shadow: inset 0 2px 0 rgba(255,255,255,0.35), inset 0 -8px 14px rgba(0,0,0,0.25); }
|
|
99
|
+
|
|
100
|
+
/* hero shapes */
|
|
101
|
+
.float { display: block; width: 100%; height: auto; aspect-ratio: 1; filter: drop-shadow(0 14px 22px var(--g)); transform: rotate(var(--r, 0deg));
|
|
102
|
+
animation: float 6s ease-in-out infinite; animation-delay: var(--d, 0s); }
|
|
103
|
+
@keyframes float { 0%, 100% { transform: translateY(0) rotate(var(--r, 0deg)); } 50% { transform: translateY(-12px) rotate(calc(var(--r, 0deg) + 4deg)); } }
|
|
104
|
+
|
|
105
|
+
/* ---------- motion ---------- */
|
|
106
|
+
.w { display: inline-block; opacity: 0; }
|
|
107
|
+
.glowwrap { display: inline-block; filter: drop-shadow(0 0 22px rgba(240,26,54,0.55)); }
|
|
108
|
+
.shape { position: absolute; opacity: 0; }
|
|
109
|
+
.par { will-change: transform; }
|
|
110
|
+
.ambient { animation: breathe 10s ease-in-out infinite; }
|
|
111
|
+
@keyframes breathe { 0%, 100% { opacity: 0.75; } 50% { opacity: 1; } }
|
|
112
|
+
.badge { position: relative; overflow: hidden; }
|
|
113
|
+
.badge::after { content: ""; position: absolute; inset: 0; pointer-events: none;
|
|
114
|
+
background: linear-gradient(105deg, transparent 35%, rgba(255,255,255,0.6) 50%, transparent 65%);
|
|
115
|
+
transform: translateX(-120%); animation: sheen 4.5s ease-in-out 1.6s infinite; }
|
|
116
|
+
@keyframes sheen { 0%, 55% { transform: translateX(-120%); } 100% { transform: translateX(120%); } }
|
|
117
|
+
.shimmer { background: linear-gradient(100deg, #161618 30%, #242428 50%, #161618 70%); background-size: 220% 100%; animation: shimmer 1.4s linear infinite; }
|
|
118
|
+
@keyframes shimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } }
|
|
119
|
+
#search-wrap { width: 100%; max-width: 16rem; transition: max-width .4s cubic-bezier(.22,1,.36,1); }
|
|
120
|
+
#search-wrap:focus-within { max-width: 22rem; }
|
|
121
|
+
.chip { transition: translate .2s ease-out, background-color .2s, color .2s; }
|
|
122
|
+
.chip:hover { translate: 0 -2px; }
|
|
123
|
+
.chip:active { scale: .94; }
|
|
124
|
+
.tile { position: relative; }
|
|
125
|
+
.tile::before { content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; opacity: 0; transition: opacity .3s;
|
|
126
|
+
background: radial-gradient(170px circle at var(--mx, 50%) var(--my, 50%), rgba(255,255,255,0.15), transparent 70%); }
|
|
127
|
+
.tile:hover::before { opacity: 1; }
|
|
128
|
+
.tile-icon { transform: perspective(700px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)); transition-timing-function: cubic-bezier(.34,1.56,.64,1); }
|
|
129
|
+
.tile.moving .tile-icon { transition-duration: .12s; transition-timing-function: ease-out; }
|
|
130
|
+
.pulse-orb { animation: pulse-orb 1.2s ease-in-out infinite; }
|
|
131
|
+
@keyframes pulse-orb { 0%, 100% { transform: scale(.8); opacity: .6; } 50% { transform: scale(1.25); opacity: 1; } }
|
|
132
|
+
@media (prefers-reduced-motion: reduce) { .ambient, .badge::after, .shimmer, .pulse-orb { animation: none; } .tile-icon { transform: none; } }
|
|
111
133
|
|
|
112
134
|
.no-scrollbar { scrollbar-width: none; }
|
|
113
135
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
@@ -115,28 +137,35 @@
|
|
|
115
137
|
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
116
138
|
*::-webkit-scrollbar-thumb { background: #2c2c30; border-radius: 999px; }
|
|
117
139
|
|
|
118
|
-
|
|
119
|
-
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
|
140
|
+
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
120
141
|
.rise { animation: rise 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; animation-delay: var(--d, 0s); }
|
|
121
|
-
@media (prefers-reduced-motion: reduce) { .rise { animation: none; } }
|
|
122
|
-
|
|
142
|
+
@media (prefers-reduced-motion: reduce) { .rise, .float { animation: none; } }
|
|
123
143
|
</style>
|
|
124
144
|
</head>
|
|
125
145
|
|
|
126
146
|
<body class="min-h-screen bg-ink-950 font-sans text-ink-50 antialiased">
|
|
127
147
|
|
|
148
|
+
<div class="ambient" aria-hidden="true"></div>
|
|
149
|
+
|
|
150
|
+
<!-- shared gradients for the hero shapes -->
|
|
151
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
|
|
152
|
+
<radialGradient id="gP" cx=".35" cy=".25" r=".9"><stop offset="0" stop-color="#ffc0c7"/><stop offset=".38" stop-color="#ff3347"/><stop offset="1" stop-color="#b30d25"/></radialGradient>
|
|
153
|
+
<radialGradient id="gO" cx=".35" cy=".25" r=".9"><stop offset="0" stop-color="#ffb8c0"/><stop offset=".38" stop-color="#e8142f"/><stop offset="1" stop-color="#7d0a1c"/></radialGradient>
|
|
154
|
+
<radialGradient id="gL" cx=".35" cy=".25" r=".9"><stop offset="0" stop-color="#ffd0d5"/><stop offset=".38" stop-color="#ff5c70"/><stop offset="1" stop-color="#c11228"/></radialGradient>
|
|
155
|
+
<radialGradient id="gB" cx=".35" cy=".25" r=".9"><stop offset="0" stop-color="#ffffff"/><stop offset=".4" stop-color="#e5e5ea"/><stop offset="1" stop-color="#9a9aa2"/></radialGradient>
|
|
156
|
+
<radialGradient id="gV" cx=".35" cy=".25" r=".9"><stop offset="0" stop-color="#85858d"/><stop offset=".4" stop-color="#2c2c30"/><stop offset="1" stop-color="#0a0a0b"/></radialGradient>
|
|
157
|
+
</defs></svg>
|
|
158
|
+
|
|
128
159
|
<!-- ================= HEADER ================= -->
|
|
129
160
|
<header class="pointer-events-none sticky top-0 z-40 pt-[calc(env(safe-area-inset-top)+0.75rem)]">
|
|
130
161
|
<div class="page-x mx-auto max-w-5xl">
|
|
131
162
|
<div class="glass pointer-events-auto relative flex h-14 items-center gap-4 rounded-full pl-5 pr-2">
|
|
132
|
-
<a href="#" class="flex shrink-0 items-center gap-2" aria-label="mlv2 home">
|
|
133
|
-
<span class="
|
|
163
|
+
<a href="#" class="flex shrink-0 items-center gap-2.5" aria-label="mlv2 home">
|
|
164
|
+
<span class="orb"></span>
|
|
134
165
|
<span class="text-[17px] font-semibold tracking-[-0.02em]">mlv2</span>
|
|
135
166
|
</a>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
<span id="count-badge" class="hidden shrink-0 text-[13px] tabular-nums text-ink-400 sm:block"><span id="count-num">0</span> games</span>
|
|
139
|
-
<div class="relative w-full max-w-[16rem]">
|
|
167
|
+
<div class="ml-auto flex min-w-0 flex-1 items-center justify-end">
|
|
168
|
+
<div id="search-wrap" class="relative">
|
|
140
169
|
<svg class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-ink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
|
141
170
|
<input id="search" type="text" placeholder="Search games" aria-label="Search games" autocomplete="off" enterkeyhint="search" class="field rounded-full pl-9" />
|
|
142
171
|
</div>
|
|
@@ -146,41 +175,61 @@
|
|
|
146
175
|
</header>
|
|
147
176
|
|
|
148
177
|
<!-- ================= MAIN ================= -->
|
|
149
|
-
<main class="page-x mx-auto max-w-5xl pb-24">
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<h1 class="
|
|
154
|
-
<p class="rise mt-4 text-[19px] text-ink-300 sm:text-[21px]" style="--d: 0.
|
|
178
|
+
<main class="page-x relative z-10 mx-auto max-w-5xl pb-24">
|
|
179
|
+
|
|
180
|
+
<section class="relative pb-10 pt-12 sm:pb-14 sm:pt-20">
|
|
181
|
+
<div class="rise mb-5"><span id="count-badge" class="badge"><svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l2.4 6.6L21 11l-6.6 2.4L12 20l-2.4-6.6L3 11l6.6-2.4z"/></svg><span><span id="count-num">0</span> games</span></span></div>
|
|
182
|
+
<h1 class="text-balance text-[2.75rem] font-semibold leading-[1.04] tracking-[-0.04em] text-ink-50 sm:text-[4.5rem]"><span class="w">school</span> <span class="w">sucks,</span><br/><span class="glowwrap"><span class="w glow-line">games</span> <span class="w glow-line">r</span> <span class="w glow-line">better</span></span></h1>
|
|
183
|
+
<p class="rise mt-4 text-[19px] text-ink-300 sm:text-[21px]" style="--d: 0.6s">we're so back</p>
|
|
184
|
+
|
|
185
|
+
<!-- floating bubbly shapes (large screens) -->
|
|
186
|
+
<div id="shapes" class="pointer-events-none absolute right-2 top-4 hidden h-[360px] w-[400px] lg:block" aria-hidden="true">
|
|
187
|
+
<div class="shape" style="left:20px;top:120px;width:190px"><div class="par" data-depth="1"><svg viewBox="0 0 120 120" class="float" style="--r:-8deg;--g:rgba(240,26,54,.4);--d:-1s">
|
|
188
|
+
<rect x="8" y="30" width="104" height="62" rx="31" fill="url(#gP)"/>
|
|
189
|
+
<path d="M34 50v24M22 62h24" stroke="#fff" stroke-width="9" stroke-linecap="round" opacity=".92"/>
|
|
190
|
+
<circle cx="82" cy="54" r="6" fill="#fff" opacity=".92"/><circle cx="96" cy="68" r="6" fill="#fff" opacity=".92"/>
|
|
191
|
+
<ellipse cx="44" cy="40" rx="28" ry="7" fill="#fff" opacity=".45"/>
|
|
192
|
+
</svg></div></div>
|
|
193
|
+
<div class="shape" style="left:250px;top:0;width:120px"><div class="par" data-depth="1.6"><svg viewBox="0 0 120 120" class="float" style="--r:10deg;--g:rgba(240,26,54,.35);--d:-3s">
|
|
194
|
+
<polygon points="60,16 71.8,43.8 101.9,46.4 79,66.2 85.9,95.6 60,80 34.1,95.6 41,66.2 18.2,46.4 48.2,43.8" fill="url(#gO)" stroke="url(#gO)" stroke-width="14" stroke-linejoin="round"/>
|
|
195
|
+
<ellipse cx="48" cy="40" rx="14" ry="5" fill="#fff" opacity=".5" transform="rotate(-25 48 40)"/>
|
|
196
|
+
</svg></div></div>
|
|
197
|
+
<div class="shape" style="left:272px;top:170px;width:120px"><div class="par" data-depth="0.7"><svg viewBox="0 0 120 120" class="float" style="--r:-6deg;--g:rgba(255,255,255,.14);--d:-2s">
|
|
198
|
+
<path d="M44 34 L90 60 L44 86 Z" fill="url(#gB)" stroke="url(#gB)" stroke-width="16" stroke-linejoin="round"/>
|
|
199
|
+
<ellipse cx="50" cy="48" rx="8" ry="4" fill="#fff" opacity=".5" transform="rotate(35 50 48)"/>
|
|
200
|
+
</svg></div></div>
|
|
201
|
+
<div class="shape" style="left:60px;top:270px;width:86px"><div class="par" data-depth="1.3"><svg viewBox="0 0 120 120" class="float" style="--r:8deg;--g:rgba(240,26,54,.3);--d:-4s">
|
|
202
|
+
<path d="M60 26v68M26 60h68" fill="none" stroke="url(#gL)" stroke-width="30" stroke-linecap="round"/>
|
|
203
|
+
<ellipse cx="52" cy="30" rx="6" ry="3" fill="#fff" opacity=".55"/>
|
|
204
|
+
</svg></div></div>
|
|
205
|
+
<div class="shape" style="left:150px;top:20px;width:64px"><div class="par" data-depth="2"><svg viewBox="0 0 120 120" class="float" style="--r:0deg;--g:rgba(255,255,255,.12);--d:-5s">
|
|
206
|
+
<circle cx="60" cy="60" r="40" fill="url(#gV)"/><ellipse cx="46" cy="42" rx="16" ry="8" fill="#fff" opacity=".5" transform="rotate(-30 46 42)"/>
|
|
207
|
+
</svg></div></div>
|
|
208
|
+
</div>
|
|
155
209
|
</section>
|
|
156
210
|
|
|
157
|
-
|
|
158
|
-
<div id="category-bar" role="group" aria-label="Categories" class="no-scrollbar -mx-5 mb-8 flex items-center gap-2 overflow-x-auto px-5 sm:mx-0 sm:mb-10 sm:px-0"></div>
|
|
211
|
+
<div id="category-bar" role="group" aria-label="Categories" class="no-scrollbar -mx-5 -mt-2 mb-6 flex items-center gap-2 overflow-x-auto px-5 py-3 sm:mx-0 sm:mb-8 sm:px-0"></div>
|
|
159
212
|
|
|
160
|
-
|
|
161
|
-
<div id="setup-banner" class="mb-8 hidden rounded-xl bg-ink-900 p-4 ring-1 ring-white/10">
|
|
213
|
+
<div id="setup-banner" class="glass-lite mb-8 hidden rounded-2xl p-4">
|
|
162
214
|
<p class="text-[15px] font-semibold">Not connected yet</p>
|
|
163
215
|
<p class="mt-1 text-[14px] leading-relaxed text-ink-300">Open this file and fill in the <code class="font-mono text-ink-50">CONFIG</code> object near the bottom with your Appwrite endpoint, project, database, table and bucket IDs.</p>
|
|
164
216
|
</div>
|
|
165
217
|
|
|
166
|
-
|
|
167
|
-
<div id="skeleton-grid" class="grid grid-cols-3 gap-x-4 gap-y-7 sm:grid-cols-4 sm:gap-x-5 sm:gap-y-8 md:grid-cols-5 lg:grid-cols-6"></div>
|
|
218
|
+
<div id="skeleton-grid" class="grid grid-cols-3 gap-3 sm:grid-cols-4 sm:gap-4 md:grid-cols-5 lg:grid-cols-6"></div>
|
|
168
219
|
|
|
169
|
-
<!-- empty state -->
|
|
170
220
|
<div id="empty-state" class="hidden flex-col items-center justify-center py-24 text-center">
|
|
171
221
|
<p id="empty-title" class="text-[19px] font-semibold">No games yet</p>
|
|
172
222
|
<p id="empty-sub" class="mt-2 max-w-xs text-pretty text-[15px] text-ink-400">Games you add in Appwrite with a valid link will show up here automatically.</p>
|
|
173
223
|
</div>
|
|
174
224
|
|
|
175
|
-
|
|
176
|
-
<div id="grid" class="hidden grid grid-cols-3 gap-x-4 gap-y-7 sm:grid-cols-4 sm:gap-x-5 sm:gap-y-8 md:grid-cols-5 lg:grid-cols-6"></div>
|
|
225
|
+
<div id="grid" class="hidden grid grid-cols-3 gap-3 sm:grid-cols-4 sm:gap-4 md:grid-cols-5 lg:grid-cols-6"></div>
|
|
177
226
|
</main>
|
|
178
227
|
|
|
179
228
|
<!-- ================= FOOTER ================= -->
|
|
180
|
-
<footer class="border-t border-white/[0.08]">
|
|
229
|
+
<footer class="relative z-10 border-t border-white/[0.08]">
|
|
181
230
|
<div class="page-x mx-auto flex max-w-5xl items-center justify-between py-4 text-[13px] text-ink-400">
|
|
182
231
|
<p>mlv2 © <span id="year"></span></p>
|
|
183
|
-
<button id="admin-trigger" class="-
|
|
232
|
+
<button id="admin-trigger" class="glass-lite rounded-full px-4 py-2 transition-colors duration-200 hover:text-ink-50">Admin</button>
|
|
184
233
|
</div>
|
|
185
234
|
</footer>
|
|
186
235
|
|
|
@@ -205,7 +254,8 @@
|
|
|
205
254
|
</div>
|
|
206
255
|
</div>
|
|
207
256
|
<div class="relative flex-1 bg-black">
|
|
208
|
-
<
|
|
257
|
+
<div id="player-loading" class="absolute inset-0 grid place-items-center"><span class="orb pulse-orb" style="width:22px;height:22px"></span></div>
|
|
258
|
+
<iframe id="player-frame" class="relative h-full w-full" allow="autoplay; fullscreen; gamepad; pointer-lock; cross-origin-isolated" allowfullscreen loading="eager"></iframe>
|
|
209
259
|
</div>
|
|
210
260
|
</div>
|
|
211
261
|
</div>
|
|
@@ -217,14 +267,9 @@
|
|
|
217
267
|
<form id="admin-login-form" class="glass glass-sheet relative w-full max-w-[340px] rounded-[28px] p-6 opacity-0" data-fx>
|
|
218
268
|
<p class="text-[19px] font-semibold">Enter PIN</p>
|
|
219
269
|
<p class="mt-1.5 text-pretty text-[14px] text-ink-400">This unlocks the admin screen on this device. Write access is controlled by your Appwrite permissions.</p>
|
|
220
|
-
|
|
221
|
-
<div id="pin-boxes" class="mt-5 grid grid-cols-6 gap-2">
|
|
222
|
-
<!-- 6 single-digit inputs, filled in by JS -->
|
|
223
|
-
</div>
|
|
270
|
+
<div id="pin-boxes" class="mt-5 grid grid-cols-6 gap-2"></div>
|
|
224
271
|
<input id="admin-pin" type="hidden" />
|
|
225
|
-
|
|
226
272
|
<p id="admin-login-error" class="mt-3 hidden text-[14px] text-red-400" role="alert"></p>
|
|
227
|
-
|
|
228
273
|
<button type="submit" id="admin-login-submit" class="btn btn-primary mt-5 w-full">Unlock</button>
|
|
229
274
|
<button type="button" data-admin-login-close class="btn btn-ghost mt-2 w-full">Cancel</button>
|
|
230
275
|
</form>
|
|
@@ -236,7 +281,6 @@
|
|
|
236
281
|
<div class="absolute inset-0 bg-black/50 backdrop-blur-md" data-admin-panel-close></div>
|
|
237
282
|
<div class="absolute inset-0 flex items-start justify-center p-0 sm:items-center sm:p-4">
|
|
238
283
|
<div id="admin-panel-card" class="glass glass-sheet relative flex h-full w-full flex-col opacity-0 sm:h-[85vh] sm:max-w-2xl sm:rounded-[32px]" data-fx>
|
|
239
|
-
|
|
240
284
|
<div class="flex h-14 shrink-0 items-center justify-between border-b border-white/[0.08] px-5 pt-[env(safe-area-inset-top)] box-content">
|
|
241
285
|
<div>
|
|
242
286
|
<p class="text-[17px] font-semibold leading-none">Admin</p>
|
|
@@ -249,41 +293,24 @@
|
|
|
249
293
|
</button>
|
|
250
294
|
</div>
|
|
251
295
|
</div>
|
|
252
|
-
|
|
253
296
|
<div class="flex-1 overflow-y-auto px-5 py-5">
|
|
254
|
-
|
|
255
|
-
<!-- add game form -->
|
|
256
|
-
<form id="add-game-form" class="rounded-xl p-4 ring-1 ring-white/10">
|
|
297
|
+
<form id="add-game-form" class="glass-lite rounded-2xl p-4">
|
|
257
298
|
<p class="text-[15px] font-semibold">Add game</p>
|
|
258
299
|
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
259
|
-
<div>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
</div>
|
|
263
|
-
<div>
|
|
264
|
-
<label for="new-category" class="text-[13px] text-ink-300">Category (optional)</label>
|
|
265
|
-
<input id="new-category" class="field mt-1.5" />
|
|
266
|
-
</div>
|
|
267
|
-
<div class="sm:col-span-2">
|
|
268
|
-
<label for="new-url" class="text-[13px] text-ink-300">Game URL</label>
|
|
269
|
-
<input id="new-url" required type="url" placeholder="https://" class="field mt-1.5" />
|
|
270
|
-
</div>
|
|
300
|
+
<div><label for="new-name" class="text-[13px] text-ink-300">Name</label><input id="new-name" required class="field mt-1.5" /></div>
|
|
301
|
+
<div><label for="new-category" class="text-[13px] text-ink-300">Category (optional)</label><input id="new-category" class="field mt-1.5" /></div>
|
|
302
|
+
<div class="sm:col-span-2"><label for="new-url" class="text-[13px] text-ink-300">Game URL</label><input id="new-url" required type="url" placeholder="https://" class="field mt-1.5" /></div>
|
|
271
303
|
<div>
|
|
272
304
|
<label for="new-icon-file" class="text-[13px] text-ink-300">Icon upload</label>
|
|
273
305
|
<input id="new-icon-file" type="file" accept="image/*" class="mt-2 w-full text-[13px] text-ink-400 file:mr-3 file:rounded-full file:border-0 file:bg-white/10 file:px-3.5 file:py-1.5 file:text-[13px] file:font-medium file:text-ink-50" />
|
|
274
306
|
</div>
|
|
275
|
-
<div>
|
|
276
|
-
<label for="new-icon-url" class="text-[13px] text-ink-300">Or external icon URL</label>
|
|
277
|
-
<input id="new-icon-url" type="url" placeholder="https://" class="field mt-1.5" />
|
|
278
|
-
</div>
|
|
307
|
+
<div><label for="new-icon-url" class="text-[13px] text-ink-300">Or external icon URL</label><input id="new-icon-url" type="url" placeholder="https://" class="field mt-1.5" /></div>
|
|
279
308
|
</div>
|
|
280
309
|
<div class="mt-5 flex items-center gap-3">
|
|
281
310
|
<button type="submit" id="add-game-submit" class="btn btn-primary">Add game</button>
|
|
282
311
|
<span id="add-game-status" class="text-[13px] text-ink-400"></span>
|
|
283
312
|
</div>
|
|
284
313
|
</form>
|
|
285
|
-
|
|
286
|
-
<!-- existing games -->
|
|
287
314
|
<p class="mb-3 mt-8 text-[15px] font-semibold">Games</p>
|
|
288
315
|
<div id="admin-list" class="space-y-2"></div>
|
|
289
316
|
</div>
|
|
@@ -291,7 +318,6 @@
|
|
|
291
318
|
</div>
|
|
292
319
|
</div>
|
|
293
320
|
|
|
294
|
-
<!-- ================= TOASTS ================= -->
|
|
295
321
|
<div id="toast-root" class="fixed bottom-4 right-4 z-[60] flex flex-col items-end gap-2 pb-[env(safe-area-inset-bottom)]"></div>
|
|
296
322
|
|
|
297
323
|
<script src="https://cdn.jsdelivr.net/npm/appwrite@26.2.0"></script>
|
|
@@ -302,7 +328,7 @@ const CONFIG = {
|
|
|
302
328
|
endpoint: "https://nyc.cloud.appwrite.io/v1",
|
|
303
329
|
projectId: "6a9b8a1b001b9c61537f",
|
|
304
330
|
databaseId: "6a9b8b23003de0ec5cf7",
|
|
305
|
-
gamesTableId: "6a9b8bb3000013a3fd80",
|
|
331
|
+
gamesTableId: "6a9b8bb3000013a3fd80",
|
|
306
332
|
iconsBucketId: "6a9b8b36002565ac5987",
|
|
307
333
|
};
|
|
308
334
|
|
|
@@ -316,9 +342,7 @@ const tablesDB = new TablesDB(client);
|
|
|
316
342
|
const storage = new Storage(client);
|
|
317
343
|
|
|
318
344
|
const isConfigured = () =>
|
|
319
|
-
!CONFIG.endpoint.includes("YOUR_") &&
|
|
320
|
-
!CONFIG.projectId.includes("YOUR_") &&
|
|
321
|
-
!CONFIG.databaseId.includes("YOUR_");
|
|
345
|
+
!CONFIG.endpoint.includes("YOUR_") && !CONFIG.projectId.includes("YOUR_") && !CONFIG.databaseId.includes("YOUR_");
|
|
322
346
|
|
|
323
347
|
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
324
348
|
function fx(targets, keyframes, options = {}) {
|
|
@@ -326,10 +350,20 @@ function fx(targets, keyframes, options = {}) {
|
|
|
326
350
|
return animate(targets, keyframes, options);
|
|
327
351
|
}
|
|
328
352
|
|
|
353
|
+
function pop(targets, { y = 12, scale = 0.94, delay = 0, stiff = 320, damp = 24 } = {}) {
|
|
354
|
+
if (reduceMotion) return animate(targets, { opacity: 1 }, { duration: 0 });
|
|
355
|
+
animate(targets, { opacity: [0, 1] }, { duration: 0.35, delay, ease: "easeOut" });
|
|
356
|
+
return animate(targets, { y: [y, 0], scale: [scale, 1] }, { type: "spring", stiffness: stiff, damping: damp, delay });
|
|
357
|
+
}
|
|
358
|
+
|
|
329
359
|
/* ------------------------------- state ------------------------------- */
|
|
330
360
|
let allGames = [];
|
|
331
361
|
let activeCategory = "All";
|
|
332
362
|
let searchTerm = "";
|
|
363
|
+
const BRAND = "#f01a36";
|
|
364
|
+
const PALETTE = ["#f01a36", "#ff6b7d", "#ff98a5"];
|
|
365
|
+
let catColors = {};
|
|
366
|
+
let barShown = false;
|
|
333
367
|
|
|
334
368
|
const el = (id) => document.getElementById(id);
|
|
335
369
|
document.getElementById("year").textContent = new Date().getFullYear();
|
|
@@ -348,7 +382,7 @@ function monogramSrc(name) {
|
|
|
348
382
|
const initials = (name || "?").trim().slice(0, 2).toUpperCase();
|
|
349
383
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
350
384
|
<rect width="100" height="100" fill="#161618"/>
|
|
351
|
-
<text x="50" y="58" font-family="-apple-system, Inter, sans-serif" font-size="34" font-weight="600" fill="#
|
|
385
|
+
<text x="50" y="58" font-family="-apple-system, Inter, sans-serif" font-size="34" font-weight="600" fill="#f01a36" text-anchor="middle">${initials}</text>
|
|
352
386
|
</svg>`;
|
|
353
387
|
return "data:image/svg+xml;utf8," + encodeURIComponent(svg);
|
|
354
388
|
}
|
|
@@ -364,7 +398,7 @@ function toast(message, tone = "default") {
|
|
|
364
398
|
node.className = `glass glass-sheet relative rounded-full ${toneClasses} px-4 py-2.5 text-[14px] opacity-0`;
|
|
365
399
|
node.textContent = message;
|
|
366
400
|
root.appendChild(node);
|
|
367
|
-
|
|
401
|
+
pop(node, { y: 16, scale: 0.9, stiff: 420, damp: 24 });
|
|
368
402
|
setTimeout(() => {
|
|
369
403
|
fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2, ease: "easeIn" }).finished.then(() => node.remove());
|
|
370
404
|
}, 2800);
|
|
@@ -373,7 +407,7 @@ function toast(message, tone = "default") {
|
|
|
373
407
|
/* -------------------------- render: skeleton -------------------------- */
|
|
374
408
|
function renderSkeleton() {
|
|
375
409
|
el("skeleton-grid").innerHTML = Array.from({ length: 12 })
|
|
376
|
-
.map(() => `<div class="
|
|
410
|
+
.map(() => `<div class="glass-lite rounded-[28px] p-2.5"><div class="shimmer aspect-square rounded-[22.37%]"></div><div class="shimmer mx-auto mt-3 h-3 w-2/3 rounded-full"></div></div>`)
|
|
377
411
|
.join("");
|
|
378
412
|
}
|
|
379
413
|
renderSkeleton();
|
|
@@ -387,24 +421,34 @@ function currentFiltered() {
|
|
|
387
421
|
});
|
|
388
422
|
}
|
|
389
423
|
|
|
390
|
-
function renderCategoryBar() {
|
|
391
|
-
const
|
|
424
|
+
function renderCategoryBar(mode) {
|
|
425
|
+
const names = [...new Set(allGames.map((g) => g.category || "Uncategorized"))];
|
|
426
|
+
catColors = {};
|
|
427
|
+
names.forEach((n, i) => (catColors[n] = PALETTE[i % PALETTE.length]));
|
|
428
|
+
const countOf = (n) => allGames.filter((g) => (g.category || "Uncategorized") === n).length;
|
|
392
429
|
const bar = el("category-bar");
|
|
393
|
-
bar.innerHTML =
|
|
430
|
+
bar.innerHTML = ["All", ...names]
|
|
394
431
|
.map((c) => {
|
|
395
432
|
const active = c === activeCategory;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
433
|
+
const color = c === "All" ? BRAND : catColors[c];
|
|
434
|
+
const n = c === "All" ? allGames.length : countOf(c);
|
|
435
|
+
return `<button data-cat="${escapeHtml(c)}" aria-pressed="${active}" style="--c:${color}" class="chip ${mode === "intro" ? "opacity-0 " : ""}${
|
|
436
|
+
active ? "chip-on text-white" : "glass-lite text-ink-200 hover:text-ink-50"
|
|
437
|
+
}">${escapeHtml(c)}<span class="chip-count">${n}</span></button>`;
|
|
399
438
|
})
|
|
400
439
|
.join("");
|
|
401
440
|
bar.querySelectorAll("[data-cat]").forEach((btn) =>
|
|
402
441
|
btn.addEventListener("click", () => {
|
|
403
442
|
activeCategory = btn.dataset.cat;
|
|
404
|
-
renderCategoryBar();
|
|
443
|
+
renderCategoryBar("click");
|
|
405
444
|
renderGrid();
|
|
406
445
|
})
|
|
407
446
|
);
|
|
447
|
+
if (mode === "intro") pop("#category-bar .chip", { y: 10, scale: 0.9, delay: stagger(0.05, { startDelay: 0.3 }), stiff: 380, damp: 22 });
|
|
448
|
+
if (mode === "click" && !reduceMotion) {
|
|
449
|
+
const on = bar.querySelector('[aria-pressed="true"]');
|
|
450
|
+
if (on) animate(on, { scale: [0.88, 1] }, { type: "spring", stiffness: 520, damping: 16 });
|
|
451
|
+
}
|
|
408
452
|
}
|
|
409
453
|
|
|
410
454
|
let gridShown = false;
|
|
@@ -435,11 +479,12 @@ function renderGrid() {
|
|
|
435
479
|
grid.innerHTML = games
|
|
436
480
|
.map((g) => {
|
|
437
481
|
const src = iconSrcFor(g) || monogramSrc(g.name);
|
|
438
|
-
|
|
439
|
-
|
|
482
|
+
const color = catColors[g.category || "Uncategorized"] || BRAND;
|
|
483
|
+
return `<button data-fx data-id="${g.$id}" style="--c:${color}" class="tile group block w-full text-left opacity-0">
|
|
484
|
+
<span class="tile-icon block aspect-square overflow-hidden rounded-[22.37%] bg-ink-800 transition-transform duration-300 ease-out group-hover:scale-[1.05] group-active:scale-[0.97]">
|
|
440
485
|
<img src="${src}" alt="" loading="lazy" class="tile-img h-full w-full object-cover" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
|
|
441
486
|
</span>
|
|
442
|
-
<span class="mt-
|
|
487
|
+
<span class="mt-3 block truncate text-center text-[13px] font-medium leading-tight text-ink-50">${escapeHtml(g.name)}</span>
|
|
443
488
|
${g.category ? `<span class="mt-0.5 block truncate text-center text-[12px] text-ink-400">${escapeHtml(g.category)}</span>` : ""}
|
|
444
489
|
</button>`;
|
|
445
490
|
})
|
|
@@ -449,14 +494,11 @@ function renderGrid() {
|
|
|
449
494
|
card.addEventListener("click", () => openPlayer(games.find((g) => g.$id === card.dataset.id)))
|
|
450
495
|
);
|
|
451
496
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
{ duration: gridShown ? 0.18 : 0.5, delay: gridShown ? 0 : stagger(0.03), ease: "easeOut" });
|
|
497
|
+
if (!gridShown) pop("#grid [data-fx]", { y: 22, scale: 0.88, delay: stagger(0.04, { startDelay: 0.2 }), stiff: 260, damp: 20 });
|
|
498
|
+
else pop("#grid [data-fx]", { y: 8, scale: 0.94, delay: stagger(0.015), stiff: 420, damp: 28 });
|
|
455
499
|
gridShown = true;
|
|
456
500
|
}
|
|
457
501
|
|
|
458
|
-
el("count-badge") && (el("count-num").textContent = "0");
|
|
459
|
-
|
|
460
502
|
/* ------------------------------ fetching ------------------------------ */
|
|
461
503
|
async function loadGames() {
|
|
462
504
|
if (!isConfigured()) {
|
|
@@ -475,8 +517,11 @@ async function loadGames() {
|
|
|
475
517
|
queries: [Query.limit(200), Query.orderAsc("order")],
|
|
476
518
|
});
|
|
477
519
|
allGames = res.rows.filter((g) => g.url && g.enabled !== false);
|
|
478
|
-
el("count-num").textContent
|
|
479
|
-
|
|
520
|
+
const from = Number(el("count-num").textContent) || 0;
|
|
521
|
+
if (reduceMotion || from === allGames.length) el("count-num").textContent = allGames.length;
|
|
522
|
+
else animate(from, allGames.length, { duration: 1, delay: 0.3, ease: "easeOut", onUpdate: (v) => (el("count-num").textContent = Math.round(v)) });
|
|
523
|
+
renderCategoryBar(barShown ? undefined : "intro");
|
|
524
|
+
barShown = true;
|
|
480
525
|
renderGrid();
|
|
481
526
|
} catch (err) {
|
|
482
527
|
console.error("Failed to load games:", err);
|
|
@@ -508,10 +553,10 @@ function openPlayer(game) {
|
|
|
508
553
|
el("player-icon").src = iconSrcFor(game) || monogramSrc(game.name);
|
|
509
554
|
el("player-newtab").href = game.url;
|
|
510
555
|
el("player-frame").src = game.url;
|
|
511
|
-
|
|
512
|
-
modal.classList.remove("hidden");
|
|
556
|
+
el("player-loading").style.display = "grid";
|
|
557
|
+
el("player-modal").classList.remove("hidden");
|
|
513
558
|
document.documentElement.style.overflow = "hidden";
|
|
514
|
-
|
|
559
|
+
pop("#player-panel", { y: 20, scale: 0.94, stiff: 260, damp: 26 });
|
|
515
560
|
}
|
|
516
561
|
function closePlayer() {
|
|
517
562
|
fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18, ease: "easeIn" }).finished.then(() => {
|
|
@@ -533,7 +578,7 @@ document.addEventListener("keydown", (e) => {
|
|
|
533
578
|
/* -------------------------------- admin -------------------------------- */
|
|
534
579
|
function openAdminLogin() {
|
|
535
580
|
el("admin-login-modal").classList.remove("hidden");
|
|
536
|
-
|
|
581
|
+
pop("#admin-login-form", { y: 16, scale: 0.92, stiff: 380, damp: 24 });
|
|
537
582
|
resetPinBoxes();
|
|
538
583
|
pinInputs[0]?.focus();
|
|
539
584
|
}
|
|
@@ -543,41 +588,25 @@ function closeAdminLogin() {
|
|
|
543
588
|
}
|
|
544
589
|
function openAdminPanel() {
|
|
545
590
|
el("admin-panel").classList.remove("hidden");
|
|
546
|
-
|
|
591
|
+
pop("#admin-panel-card", { y: 24, scale: 0.97, stiff: 300, damp: 28 });
|
|
547
592
|
renderAdminList();
|
|
548
593
|
}
|
|
549
|
-
function closeAdminPanel() {
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
function isUnlocked() {
|
|
553
|
-
return sessionStorage.getItem(ADMIN_SESSION_KEY) === "1";
|
|
554
|
-
}
|
|
594
|
+
function closeAdminPanel() { el("admin-panel").classList.add("hidden"); }
|
|
595
|
+
function isUnlocked() { return sessionStorage.getItem(ADMIN_SESSION_KEY) === "1"; }
|
|
555
596
|
|
|
556
|
-
el("admin-trigger").addEventListener("click", () => {
|
|
557
|
-
if (isUnlocked()) openAdminPanel();
|
|
558
|
-
else openAdminLogin();
|
|
559
|
-
});
|
|
597
|
+
el("admin-trigger").addEventListener("click", () => { if (isUnlocked()) openAdminPanel(); else openAdminLogin(); });
|
|
560
598
|
document.querySelector("[data-admin-login-close]").addEventListener("click", closeAdminLogin);
|
|
561
599
|
document.querySelector("[data-admin-panel-close]").addEventListener("click", closeAdminPanel);
|
|
562
600
|
|
|
563
|
-
/* ------- 6-digit PIN pad: build boxes, wire auto-advance + paste ------- */
|
|
564
601
|
const pinBoxesEl = el("pin-boxes");
|
|
565
602
|
pinBoxesEl.innerHTML = Array.from({ length: 6 })
|
|
566
|
-
.map(
|
|
567
|
-
|
|
568
|
-
`<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1" aria-label="Digit ${i + 1}"
|
|
569
|
-
class="h-12 w-full rounded-[10px] bg-white/[0.07] text-center text-[20px] tabular-nums outline-none ring-1 ring-inset ring-white/10 transition-shadow focus:ring-2 focus:ring-red-500" />`
|
|
570
|
-
)
|
|
603
|
+
.map((_, i) => `<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1" aria-label="Digit ${i + 1}"
|
|
604
|
+
class="h-12 w-full rounded-[12px] bg-white/[0.07] text-center text-[20px] tabular-nums outline-none ring-1 ring-inset ring-white/10 transition-shadow focus:ring-2 focus:ring-red-500" />`)
|
|
571
605
|
.join("");
|
|
572
606
|
const pinInputs = Array.from(pinBoxesEl.querySelectorAll("[data-pin-idx]"));
|
|
573
607
|
|
|
574
|
-
function resetPinBoxes() {
|
|
575
|
-
|
|
576
|
-
el("admin-pin").value = "";
|
|
577
|
-
}
|
|
578
|
-
function syncPinValue() {
|
|
579
|
-
el("admin-pin").value = pinInputs.map((i) => i.value).join("");
|
|
580
|
-
}
|
|
608
|
+
function resetPinBoxes() { pinInputs.forEach((i) => (i.value = "")); el("admin-pin").value = ""; }
|
|
609
|
+
function syncPinValue() { el("admin-pin").value = pinInputs.map((i) => i.value).join(""); }
|
|
581
610
|
pinInputs.forEach((input, idx) => {
|
|
582
611
|
input.addEventListener("input", () => {
|
|
583
612
|
input.value = input.value.replace(/[^0-9]/g, "").slice(0, 1);
|
|
@@ -607,6 +636,7 @@ el("admin-login-form").addEventListener("submit", (e) => {
|
|
|
607
636
|
openAdminPanel();
|
|
608
637
|
} else {
|
|
609
638
|
errBox.textContent = "Wrong PIN.";
|
|
639
|
+
if (!reduceMotion) animate("#pin-boxes", { x: [0, -10, 10, -8, 8, -4, 0] }, { duration: 0.45 });
|
|
610
640
|
errBox.classList.remove("hidden");
|
|
611
641
|
resetPinBoxes();
|
|
612
642
|
pinInputs[0]?.focus();
|
|
@@ -635,7 +665,7 @@ async function renderAdminList() {
|
|
|
635
665
|
list.innerHTML = res.rows
|
|
636
666
|
.map(
|
|
637
667
|
(g) => `
|
|
638
|
-
<div class="flex items-center gap-3 rounded-
|
|
668
|
+
<div class="glass-lite flex items-center gap-3 rounded-2xl p-3" data-row="${g.$id}">
|
|
639
669
|
<img src="${iconSrcFor(g) || monogramSrc(g.name)}" alt="" class="tile-img h-10 w-10 shrink-0 rounded-[22.37%] object-cover" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
|
|
640
670
|
<div class="min-w-0 flex-1">
|
|
641
671
|
<input data-field="name" data-id="${g.$id}" value="${escapeHtml(g.name)}" aria-label="Name" class="w-full border-b border-transparent bg-transparent text-[15px] font-medium outline-none focus:border-red-500" />
|
|
@@ -651,6 +681,7 @@ async function renderAdminList() {
|
|
|
651
681
|
</div>`
|
|
652
682
|
)
|
|
653
683
|
.join("");
|
|
684
|
+
if (!reduceMotion) animate("#admin-list > div", { opacity: [0, 1], y: [8, 0] }, { duration: 0.3, delay: stagger(0.03), ease: "easeOut" });
|
|
654
685
|
|
|
655
686
|
list.querySelectorAll("input[data-field]").forEach((input) => {
|
|
656
687
|
const commit = async () => {
|
|
@@ -709,15 +740,7 @@ el("add-game-form").addEventListener("submit", async (e) => {
|
|
|
709
740
|
databaseId: CONFIG.databaseId,
|
|
710
741
|
tableId: CONFIG.gamesTableId,
|
|
711
742
|
rowId: ID.unique(),
|
|
712
|
-
data: {
|
|
713
|
-
name,
|
|
714
|
-
url,
|
|
715
|
-
category,
|
|
716
|
-
iconFileId,
|
|
717
|
-
iconUrl: iconFileId ? "" : iconUrlField,
|
|
718
|
-
order: allGames.length,
|
|
719
|
-
enabled: true,
|
|
720
|
-
},
|
|
743
|
+
data: { name, url, category, iconFileId, iconUrl: iconFileId ? "" : iconUrlField, order: allGames.length, enabled: true },
|
|
721
744
|
});
|
|
722
745
|
|
|
723
746
|
statusEl.textContent = "Added.";
|
|
@@ -733,6 +756,65 @@ el("add-game-form").addEventListener("submit", async (e) => {
|
|
|
733
756
|
setTimeout(() => (statusEl.textContent = ""), 2500);
|
|
734
757
|
}
|
|
735
758
|
});
|
|
759
|
+
|
|
760
|
+
/* ---------------------------- tile hover FX ---------------------------- */
|
|
761
|
+
const gridEl = el("grid");
|
|
762
|
+
gridEl.addEventListener("pointermove", (e) => {
|
|
763
|
+
if (e.pointerType !== "mouse" || reduceMotion) return;
|
|
764
|
+
const t = e.target.closest(".tile");
|
|
765
|
+
if (!t) return;
|
|
766
|
+
const r = t.getBoundingClientRect();
|
|
767
|
+
const x = (e.clientX - r.left) / r.width, y = (e.clientY - r.top) / r.height;
|
|
768
|
+
t.classList.add("moving");
|
|
769
|
+
t.style.setProperty("--mx", x * 100 + "%");
|
|
770
|
+
t.style.setProperty("--my", y * 100 + "%");
|
|
771
|
+
const icon = t.firstElementChild;
|
|
772
|
+
icon.style.setProperty("--ry", ((x - 0.5) * 16).toFixed(2) + "deg");
|
|
773
|
+
icon.style.setProperty("--rx", ((0.5 - y) * 16).toFixed(2) + "deg");
|
|
774
|
+
});
|
|
775
|
+
gridEl.addEventListener("pointerout", (e) => {
|
|
776
|
+
const t = e.target.closest(".tile");
|
|
777
|
+
if (!t || t.contains(e.relatedTarget)) return;
|
|
778
|
+
t.classList.remove("moving");
|
|
779
|
+
t.firstElementChild.style.removeProperty("--rx");
|
|
780
|
+
t.firstElementChild.style.removeProperty("--ry");
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
el("player-frame").addEventListener("load", () => {
|
|
784
|
+
if (el("player-frame").getAttribute("src")) el("player-loading").style.display = "none";
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
/* ------------------------------ hero motion ------------------------------ */
|
|
788
|
+
(function hero() {
|
|
789
|
+
const show = () => document.querySelectorAll(".w, .shape").forEach((n) => (n.style.opacity = 1));
|
|
790
|
+
if (reduceMotion) return show();
|
|
791
|
+
animate(".w", { opacity: [0, 1], y: [32, 0], filter: ["blur(14px)", "blur(0px)"] },
|
|
792
|
+
{ duration: 0.85, delay: stagger(0.08, { startDelay: 0.1 }), ease: [0.22, 1, 0.36, 1] })
|
|
793
|
+
.finished.then(() => document.querySelectorAll(".w").forEach((w) => (w.style.filter = "none")));
|
|
794
|
+
animate(".shape", { opacity: [0, 1] }, { duration: 0.4, delay: stagger(0.1, { startDelay: 0.4 }) });
|
|
795
|
+
animate(".shape", { scale: [0.2, 1], rotate: [-40, 0] }, { type: "spring", stiffness: 170, damping: 13, delay: stagger(0.1, { startDelay: 0.4 }) });
|
|
796
|
+
|
|
797
|
+
// gentle cursor + scroll parallax on the shapes
|
|
798
|
+
const pars = [...document.querySelectorAll(".par")];
|
|
799
|
+
const wrap = el("shapes");
|
|
800
|
+
let tx = 0, ty = 0, cx = 0, cy = 0;
|
|
801
|
+
addEventListener("pointermove", (e) => { tx = e.clientX / innerWidth - 0.5; ty = e.clientY / innerHeight - 0.5; }, { passive: true });
|
|
802
|
+
(function loop() {
|
|
803
|
+
cx += (tx - cx) * 0.07;
|
|
804
|
+
cy += (ty - cy) * 0.07;
|
|
805
|
+
if (wrap.offsetParent) {
|
|
806
|
+
pars.forEach((p) => {
|
|
807
|
+
const d = Number(p.dataset.depth);
|
|
808
|
+
p.style.transform = `translate3d(${(-cx * 36 * d).toFixed(2)}px, ${(-cy * 36 * d - scrollY * 0.12 * d).toFixed(2)}px, 0)`;
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
requestAnimationFrame(loop);
|
|
812
|
+
})();
|
|
813
|
+
})();
|
|
814
|
+
</script>
|
|
815
|
+
<script>
|
|
816
|
+
// safety net: never leave the title or shapes invisible if the module script is blocked
|
|
817
|
+
setTimeout(function () { document.querySelectorAll(".w, .shape").forEach(function (n) { if (getComputedStyle(n).opacity < 1) n.style.opacity = 1; }); }, 3500);
|
|
736
818
|
</script>
|
|
737
819
|
</body>
|
|
738
820
|
</html>
|