Skip to content

Commit 6166ff5

Browse files
Raoof128claude
andcommitted
feat: add lock screen with circuit pattern and idle detection
- LockScreen.jsx: full-screen overlay (z-index 2000) with animated circuit-board background (CSS repeating gradients + animated scan line), pulsing neon "N" logo, live Orbitron HH:MM clock, Oxanium date, and blinking monospace unlock hint - Slide-up exit animation via Framer Motion AnimatePresence - Dismisses on click, keydown, or mousedown; skips dismiss if a text input is currently focused - Desktop.jsx: locked state + 5-minute idle timer (mousemove, mousedown, keydown, touchstart, scroll); timer resets on each interaction and is only started after boot completes; timer is skipped while user types in an input element Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2739dd3 commit 6166ff5

1 file changed

Lines changed: 309 additions & 0 deletions

File tree

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import { useEffect, useState } from 'react'
2+
import { motion as Motion, AnimatePresence } from 'framer-motion'
3+
4+
/** Clock that updates every second for the lock screen */
5+
function LockClock() {
6+
const [now, setNow] = useState(() => new Date())
7+
8+
useEffect(() => {
9+
const id = setInterval(() => setNow(new Date()), 1000)
10+
return () => clearInterval(id)
11+
}, [])
12+
13+
const time = now.toLocaleTimeString([], {
14+
hour: '2-digit',
15+
minute: '2-digit',
16+
hour12: false,
17+
})
18+
19+
const date = now.toLocaleDateString([], {
20+
weekday: 'long',
21+
month: 'long',
22+
day: 'numeric',
23+
year: 'numeric',
24+
})
25+
26+
return (
27+
<div className="flex flex-col items-center gap-1">
28+
<span
29+
className="heading-display select-none tabular-nums"
30+
style={{
31+
fontSize: 'clamp(4rem, 10vw, 7rem)',
32+
fontWeight: 900,
33+
color: 'hsl(56 100% 48%)',
34+
textShadow:
35+
'0 0 20px hsl(56 100% 48% / 0.6), 0 0 60px hsl(56 100% 48% / 0.2)',
36+
lineHeight: 1,
37+
letterSpacing: '0.04em',
38+
}}
39+
>
40+
{time}
41+
</span>
42+
<span
43+
className="heading-ui select-none"
44+
style={{
45+
fontSize: 'clamp(0.85rem, 2vw, 1.1rem)',
46+
color: 'hsl(170 76% 63% / 0.85)',
47+
letterSpacing: '0.08em',
48+
textTransform: 'uppercase',
49+
}}
50+
>
51+
{date}
52+
</span>
53+
</div>
54+
)
55+
}
56+
57+
/** Blinking "click to unlock" hint */
58+
function UnlockHint() {
59+
return (
60+
<Motion.p
61+
className="select-none font-mono text-xs"
62+
style={{ color: 'hsl(0 0% 60%)' }}
63+
animate={{ opacity: [1, 0.3, 1] }}
64+
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
65+
>
66+
Click or press any key to unlock
67+
</Motion.p>
68+
)
69+
}
70+
71+
/** CSS-only animated circuit pattern rendered as a background div */
72+
function CircuitBackground() {
73+
return (
74+
<div
75+
className="pointer-events-none absolute inset-0"
76+
style={{ zIndex: 0 }}
77+
aria-hidden
78+
>
79+
{/* Base grid */}
80+
<div
81+
className="absolute inset-0"
82+
style={{
83+
backgroundImage: `
84+
linear-gradient(hsl(56 100% 48% / 0.04) 1px, transparent 1px),
85+
linear-gradient(90deg, hsl(56 100% 48% / 0.04) 1px, transparent 1px)
86+
`,
87+
backgroundSize: '60px 60px',
88+
}}
89+
/>
90+
{/* Denser sub-grid */}
91+
<div
92+
className="absolute inset-0"
93+
style={{
94+
backgroundImage: `
95+
linear-gradient(hsl(170 76% 63% / 0.02) 1px, transparent 1px),
96+
linear-gradient(90deg, hsl(170 76% 63% / 0.02) 1px, transparent 1px)
97+
`,
98+
backgroundSize: '20px 20px',
99+
}}
100+
/>
101+
{/* Animated horizontal scan highlight */}
102+
<Motion.div
103+
className="absolute inset-x-0 h-[1px]"
104+
style={{
105+
background:
106+
'linear-gradient(90deg, transparent 0%, hsl(56 100% 48% / 0.15) 30%, hsl(170 76% 63% / 0.12) 70%, transparent 100%)',
107+
}}
108+
animate={{ top: ['0%', '100%'] }}
109+
transition={{
110+
duration: 8,
111+
repeat: Infinity,
112+
ease: 'linear',
113+
}}
114+
/>
115+
{/* Ambient orbs */}
116+
<div
117+
style={{
118+
position: 'absolute',
119+
width: 600,
120+
height: 600,
121+
borderRadius: '50%',
122+
background: 'hsl(56 100% 48%)',
123+
opacity: 0.05,
124+
filter: 'blur(130px)',
125+
top: '-15%',
126+
right: '-10%',
127+
}}
128+
/>
129+
<div
130+
style={{
131+
position: 'absolute',
132+
width: 700,
133+
height: 700,
134+
borderRadius: '50%',
135+
background: 'hsl(170 76% 63%)',
136+
opacity: 0.04,
137+
filter: 'blur(140px)',
138+
bottom: '-20%',
139+
left: '-15%',
140+
}}
141+
/>
142+
</div>
143+
)
144+
}
145+
146+
/** Pulsing neon "N" logo for the lock screen */
147+
function LockLogo() {
148+
return (
149+
<Motion.div
150+
style={{
151+
width: 80,
152+
height: 80,
153+
borderRadius: 16,
154+
display: 'flex',
155+
alignItems: 'center',
156+
justifyContent: 'center',
157+
background: 'rgba(0,0,0,0.7)',
158+
border: '1px solid hsl(56 100% 48% / 0.35)',
159+
position: 'relative',
160+
}}
161+
animate={{
162+
boxShadow: [
163+
'0 0 15px hsl(56 100% 48% / 0.3), 0 0 40px hsl(170 76% 63% / 0.1)',
164+
'0 0 30px hsl(56 100% 48% / 0.5), 0 0 80px hsl(170 76% 63% / 0.2)',
165+
'0 0 15px hsl(56 100% 48% / 0.3), 0 0 40px hsl(170 76% 63% / 0.1)',
166+
],
167+
}}
168+
transition={{
169+
duration: 3,
170+
repeat: Infinity,
171+
ease: 'easeInOut',
172+
}}
173+
>
174+
<span
175+
className="heading-display select-none"
176+
style={{
177+
fontSize: '2.4rem',
178+
fontWeight: 900,
179+
color: 'hsl(56 100% 48%)',
180+
textShadow: '0 0 16px hsl(56 100% 48% / 0.7)',
181+
lineHeight: 1,
182+
}}
183+
>
184+
N
185+
</span>
186+
{/* Corner marks */}
187+
{[
188+
{ top: 5, left: 5, borderTop: true, borderLeft: true },
189+
{ bottom: 5, right: 5, borderBottom: true, borderRight: true },
190+
].map((pos, i) => (
191+
<span
192+
key={i}
193+
style={{
194+
position: 'absolute',
195+
width: 8,
196+
height: 8,
197+
...(pos.top !== undefined ? { top: pos.top } : { bottom: pos.bottom }),
198+
...(pos.left !== undefined ? { left: pos.left } : { right: pos.right }),
199+
borderTop: pos.borderTop ? '1px solid hsl(170 76% 63% / 0.5)' : undefined,
200+
borderLeft: pos.borderLeft ? '1px solid hsl(170 76% 63% / 0.5)' : undefined,
201+
borderBottom: pos.borderBottom ? '1px solid hsl(170 76% 63% / 0.5)' : undefined,
202+
borderRight: pos.borderRight ? '1px solid hsl(170 76% 63% / 0.5)' : undefined,
203+
}}
204+
/>
205+
))}
206+
</Motion.div>
207+
)
208+
}
209+
210+
export default function LockScreen({ onUnlock }) {
211+
const [exiting, setExiting] = useState(false)
212+
213+
const dismiss = () => {
214+
if (exiting) return
215+
setExiting(true)
216+
}
217+
218+
// Listen for any user interaction
219+
useEffect(() => {
220+
const handleInteraction = () => {
221+
// Don't dismiss if user is typing in a focused input inside the lock screen
222+
const active = document.activeElement
223+
if (
224+
active &&
225+
(active.tagName === 'INPUT' ||
226+
active.tagName === 'TEXTAREA' ||
227+
active.isContentEditable)
228+
) {
229+
return
230+
}
231+
dismiss()
232+
}
233+
234+
document.addEventListener('click', handleInteraction)
235+
document.addEventListener('keydown', handleInteraction)
236+
document.addEventListener('mousedown', handleInteraction)
237+
238+
return () => {
239+
document.removeEventListener('click', handleInteraction)
240+
document.removeEventListener('keydown', handleInteraction)
241+
document.removeEventListener('mousedown', handleInteraction)
242+
}
243+
// eslint-disable-next-line react-hooks/exhaustive-deps
244+
}, [exiting])
245+
246+
return (
247+
<AnimatePresence onExitComplete={onUnlock}>
248+
{!exiting && (
249+
<Motion.div
250+
key="lock-screen"
251+
className="fixed inset-0 flex flex-col items-center justify-center overflow-hidden"
252+
style={{
253+
zIndex: 2000,
254+
background: 'rgba(0, 0, 0, 0.97)',
255+
cursor: 'pointer',
256+
}}
257+
initial={{ opacity: 0 }}
258+
animate={{ opacity: 1 }}
259+
exit={{ y: '-100%', opacity: 0 }}
260+
transition={{
261+
enter: { duration: 0.4, ease: 'easeOut' },
262+
exit: { duration: 0.5, ease: 'easeInOut' },
263+
}}
264+
aria-label="Lock screen — click or press any key to unlock"
265+
role="dialog"
266+
aria-modal
267+
>
268+
<CircuitBackground />
269+
270+
{/* Scanlines */}
271+
<div
272+
className="pointer-events-none absolute inset-0"
273+
style={{
274+
background:
275+
'repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.02) 2px, rgba(255,255,255,0.02) 4px)',
276+
zIndex: 1,
277+
}}
278+
/>
279+
280+
{/* Center content */}
281+
<div
282+
className="relative flex flex-col items-center gap-6"
283+
style={{ zIndex: 5 }}
284+
>
285+
<LockLogo />
286+
<LockClock />
287+
<UnlockHint />
288+
</div>
289+
290+
{/* Bottom NEXUS wordmark */}
291+
<div
292+
className="absolute bottom-8 left-0 right-0 flex justify-center"
293+
style={{ zIndex: 5 }}
294+
>
295+
<span
296+
className="heading-display select-none text-[10px]"
297+
style={{
298+
color: 'hsl(56 100% 48% / 0.25)',
299+
letterSpacing: '0.3em',
300+
}}
301+
>
302+
NEXUS OS
303+
</span>
304+
</div>
305+
</Motion.div>
306+
)}
307+
</AnimatePresence>
308+
)
309+
}

0 commit comments

Comments
 (0)