Spaces:
Running
Running
added dart pad
Browse files- app/components/DartPadEmbed.tsx +170 -0
- app/components/Desktop.tsx +57 -47
- app/components/FlutterRunner.tsx +79 -124
- app/components/MatrixRain.tsx +0 -237
- app/components/SystemPowerOverlay.tsx +126 -0
- app/components/TopBar.tsx +19 -6
app/components/DartPadEmbed.tsx
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import React, { useEffect, useRef } from 'react'
|
| 4 |
+
|
| 5 |
+
interface DartPadEmbedProps {
|
| 6 |
+
code: string
|
| 7 |
+
theme?: 'light' | 'dark'
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function DartPadEmbed({ code, theme = 'light' }: DartPadEmbedProps) {
|
| 11 |
+
const iframeRef = useRef<HTMLIFrameElement>(null)
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
// Load DartPad embed script
|
| 15 |
+
const script = document.createElement('script')
|
| 16 |
+
script.src = 'https://dartpad.dev/inject_embed.dart.js'
|
| 17 |
+
script.defer = true
|
| 18 |
+
document.body.appendChild(script)
|
| 19 |
+
|
| 20 |
+
return () => {
|
| 21 |
+
document.body.removeChild(script)
|
| 22 |
+
}
|
| 23 |
+
}, [])
|
| 24 |
+
|
| 25 |
+
// We can't easily inject code into the iframe via URL params for the standard embed-flutter.html
|
| 26 |
+
// The standard way is to use a gist ID or just let the user type.
|
| 27 |
+
// However, the user's example implies passing code directly.
|
| 28 |
+
// The inject_embed.dart.js script looks for HTML elements with specific classes, but here we are using an iframe directly.
|
| 29 |
+
// Actually, looking at the user's example:
|
| 30 |
+
// src={`https://dartpad.dev/embed-flutter.html?theme=${theme}`}
|
| 31 |
+
// And they pass `code` prop but don't seem to use it in the src in their example?
|
| 32 |
+
// Wait, the user's example has:
|
| 33 |
+
// <DartPadEmbed code={flutterCode} theme="dark" />
|
| 34 |
+
// But the component implementation they provided:
|
| 35 |
+
// export default function DartPadEmbed({ code, theme = 'light' }) { ... }
|
| 36 |
+
// It DOES NOT use `code` in the return!
|
| 37 |
+
// It just renders the iframe.
|
| 38 |
+
// This means the user might expect me to figure out how to inject the code, OR the user's example is incomplete/simplified.
|
| 39 |
+
|
| 40 |
+
// If I look at DartPad embedding docs, usually you use a gist ID.
|
| 41 |
+
// However, there is a way to use `inject_embed.dart.js` on a `div` or `pre` tag containing the code.
|
| 42 |
+
// The user's example uses an iframe AND the script. This is contradictory.
|
| 43 |
+
// If you use the script, you don't create the iframe yourself; the script replaces a div with the iframe.
|
| 44 |
+
// If you use the iframe directly, you usually pass a gist ID in the URL.
|
| 45 |
+
|
| 46 |
+
// Let's look closely at the user's request:
|
| 47 |
+
// "remove zapp and use dartembed api here is a code example"
|
| 48 |
+
// The example creates a script tag for `inject_embed.dart.js` BUT renders an iframe manually.
|
| 49 |
+
// This suggests the user might be confused or I am missing something about how that script works with existing iframes.
|
| 50 |
+
// BUT, if I use the script, I should probably render a div with the code and let the script do its work.
|
| 51 |
+
|
| 52 |
+
// Let's try to implement it using the "inject" method which is what the script is for.
|
| 53 |
+
// The script looks for elements with `data-dartpad-embed`.
|
| 54 |
+
|
| 55 |
+
// HOWEVER, the user explicitly provided this return:
|
| 56 |
+
// return (
|
| 57 |
+
// <iframe
|
| 58 |
+
// ref={iframeRef}
|
| 59 |
+
// src={`https://dartpad.dev/embed-flutter.html?theme=${theme}`}
|
| 60 |
+
// style={{ width: '100%', height: '500px', border: 'none' }}
|
| 61 |
+
// />
|
| 62 |
+
// );
|
| 63 |
+
|
| 64 |
+
// If I just do that, the code won't be there.
|
| 65 |
+
// Maybe I should try to postMessage the code to the iframe?
|
| 66 |
+
// Or maybe the user just wants the environment and will paste the code?
|
| 67 |
+
// "The user wants to replace "zapp" with "dartembed" (DartPad) for running Flutter code."
|
| 68 |
+
// If I can't inject the code, it's not very useful as a "Runner".
|
| 69 |
+
|
| 70 |
+
// Let's check if there is a URL parameter for code.
|
| 71 |
+
// Research suggests `?code=` might work for some versions, or `?id=` for gists.
|
| 72 |
+
// There is also a protobuf encoding for code in URL but that's complex.
|
| 73 |
+
|
| 74 |
+
// Alternative: The `inject_embed.dart.js` is designed to take a DOM element and turn it into an embed.
|
| 75 |
+
// If the user provided code has `useEffect` loading that script, maybe they intended to use the injection method but wrote the iframe part wrong?
|
| 76 |
+
// OR, maybe they want me to use the iframe and somehow get the code in.
|
| 77 |
+
|
| 78 |
+
// Let's try to implement the "Injection" method which is the standard way to use that script.
|
| 79 |
+
// It takes a div with code and replaces it.
|
| 80 |
+
|
| 81 |
+
// Wait, if I use the injection method, I don't need the iframe in my JSX.
|
| 82 |
+
// I'll render a div with the code.
|
| 83 |
+
|
| 84 |
+
// Let's try to stick to the user's example as much as possible but make it work.
|
| 85 |
+
// If I strictly follow their example, I render an iframe that ignores the code.
|
| 86 |
+
// That seems wrong.
|
| 87 |
+
|
| 88 |
+
// Let's try to use the `inject_embed.dart.js` properly.
|
| 89 |
+
// It replaces elements with `class="dartpad-embed-flutter"`.
|
| 90 |
+
|
| 91 |
+
// But wait, the user's example:
|
| 92 |
+
// src={`https://dartpad.dev/embed-flutter.html?theme=${theme}`}
|
| 93 |
+
// This is a direct embed.
|
| 94 |
+
|
| 95 |
+
// Let's assume the user wants me to use the iframe approach but maybe missed how to pass code.
|
| 96 |
+
// Actually, looking at `https://github.com/dart-lang/dart-pad/wiki/Embedding-Guide`,
|
| 97 |
+
// "You can also embed DartPad by using an iframe directly."
|
| 98 |
+
// "To initialize the editor with specific code, you can use a GitHub Gist."
|
| 99 |
+
// It doesn't seem to support direct code injection via URL for arbitrary large code blocks easily without encoding.
|
| 100 |
+
|
| 101 |
+
// HOWEVER, the `inject_embed.dart.js` method allows putting code in the body of the element.
|
| 102 |
+
// So I will use that method instead of the iframe, as it allows passing `code` prop easily.
|
| 103 |
+
|
| 104 |
+
// Refined plan for DartPadEmbed.tsx:
|
| 105 |
+
// Render a div with the correct class and the code inside.
|
| 106 |
+
// The script will find it and replace it with the iframe.
|
| 107 |
+
|
| 108 |
+
// But the user specifically gave me an example with an iframe ref.
|
| 109 |
+
// Maybe I should try to support what they gave.
|
| 110 |
+
// "remove zapp and use dartembed api here is a code example // components/DartPadEmbed.jsx"
|
| 111 |
+
|
| 112 |
+
// Let's look at the user's code again.
|
| 113 |
+
// It imports `useEffect`, `useRef`.
|
| 114 |
+
// It loads the script.
|
| 115 |
+
// It returns an iframe.
|
| 116 |
+
// This is indeed a bit weird.
|
| 117 |
+
|
| 118 |
+
// Let's try to implement a hybrid that works.
|
| 119 |
+
// If I use the `inject_embed.dart.js`, I should render:
|
| 120 |
+
// <div className="dartpad-embed-flutter">{code}</div>
|
| 121 |
+
|
| 122 |
+
// If I do that, I don't need the iframe in JSX.
|
| 123 |
+
// I will assume the user's INTENT is to have the code run, and their example was just a pointer to the library/script.
|
| 124 |
+
// I will implement it using the injection method because it's the only way to pass code without a Gist.
|
| 125 |
+
|
| 126 |
+
// Wait, there is a limitation with the injection method in React:
|
| 127 |
+
// React might get confused if the DOM is modified by an external script.
|
| 128 |
+
// But since we are replacing a node, maybe it's fine if we don't update it often.
|
| 129 |
+
// Or we can use a ref to a container and append the element manually.
|
| 130 |
+
|
| 131 |
+
return (
|
| 132 |
+
<div className="w-full h-full">
|
| 133 |
+
<div
|
| 134 |
+
className="dartpad-embed-flutter"
|
| 135 |
+
data-theme={theme}
|
| 136 |
+
data-run="true"
|
| 137 |
+
style={{ width: '100%', height: '500px' }}
|
| 138 |
+
>
|
| 139 |
+
{code}
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
)
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Wait, if I change the code, will it update?
|
| 146 |
+
// The script runs once. If `code` prop changes, React re-renders the div text.
|
| 147 |
+
// But if the script has already replaced the div with an iframe, React might fail to update or the update won't be reflected in DartPad.
|
| 148 |
+
// This is tricky.
|
| 149 |
+
// The `FlutterRunner` has a "Run" button (or "Preview").
|
| 150 |
+
// Maybe we only mount `DartPadEmbed` when we want to run?
|
| 151 |
+
// In `FlutterRunner`, `mode === 'preview'` renders the preview.
|
| 152 |
+
// If we switch to edit and back, it remounts.
|
| 153 |
+
// So passing the code on mount is sufficient.
|
| 154 |
+
|
| 155 |
+
// Let's try to implement `DartPadEmbed` using the injection method as it seems most robust for "passing code".
|
| 156 |
+
// I'll use the user's provided script URL.
|
| 157 |
+
|
| 158 |
+
// Re-reading the user's code:
|
| 159 |
+
// They return an iframe.
|
| 160 |
+
// Maybe they want me to use the iframe and the script is just... there?
|
| 161 |
+
// No, that makes no sense.
|
| 162 |
+
// I'll stick to the injection method which uses the script to create the embed.
|
| 163 |
+
// I will ignore the iframe part of their example in favor of making it actually work with the provided code.
|
| 164 |
+
|
| 165 |
+
// Actually, looking at the `inject_embed.dart.js` source or docs:
|
| 166 |
+
// It looks for `{{ site.dartpad_embed_class }}` which defaults to `dartpad-embed-flutter` (or similar).
|
| 167 |
+
// I'll use `dartpad-embed-flutter`.
|
| 168 |
+
|
| 169 |
+
// Let's write `DartPadEmbed.tsx`.
|
| 170 |
+
|
app/components/Desktop.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { TopBar } from './TopBar'
|
|
| 6 |
import { FileManager } from './FileManager'
|
| 7 |
import { Calendar } from './Calendar'
|
| 8 |
import { DraggableDesktopIcon } from './DraggableDesktopIcon'
|
| 9 |
-
|
| 10 |
import { HelpModal } from './HelpModal'
|
| 11 |
import { DesktopContextMenu } from './DesktopContextMenu'
|
| 12 |
import { BackgroundSelector } from './BackgroundSelector'
|
|
@@ -20,6 +20,7 @@ import { AboutModal } from './AboutModal'
|
|
| 20 |
import { SessionManagerWindow } from './SessionManagerWindow'
|
| 21 |
import { FlutterRunner } from './FlutterRunner'
|
| 22 |
import { motion, AnimatePresence } from 'framer-motion'
|
|
|
|
| 23 |
|
| 24 |
export function Desktop() {
|
| 25 |
const [fileManagerOpen, setFileManagerOpen] = useState(true)
|
|
@@ -35,7 +36,7 @@ export function Desktop() {
|
|
| 35 |
const [sessionKey, setSessionKey] = useState<string>('')
|
| 36 |
const [sessionInitialized, setSessionInitialized] = useState(false)
|
| 37 |
const [currentPath, setCurrentPath] = useState('')
|
| 38 |
-
|
| 39 |
const [helpModalOpen, setHelpModalOpen] = useState(false)
|
| 40 |
const [contextMenuOpen, setContextMenuOpen] = useState(false)
|
| 41 |
const [contextMenuPos, setContextMenuPos] = useState({ x: 0, y: 0 })
|
|
@@ -45,6 +46,7 @@ export function Desktop() {
|
|
| 45 |
const [sessionManagerOpen, setSessionManagerOpen] = useState(false)
|
| 46 |
const [flutterRunnerOpen, setFlutterRunnerOpen] = useState(false)
|
| 47 |
const [activeFlutterApp, setActiveFlutterApp] = useState<any>(null)
|
|
|
|
| 48 |
|
| 49 |
const openFileManager = (path: string) => {
|
| 50 |
setCurrentPath(path)
|
|
@@ -153,13 +155,7 @@ export function Desktop() {
|
|
| 153 |
}
|
| 154 |
}
|
| 155 |
|
| 156 |
-
const triggerMatrix = () => {
|
| 157 |
-
setMatrixActive(true)
|
| 158 |
-
}
|
| 159 |
|
| 160 |
-
const handleMatrixComplete = () => {
|
| 161 |
-
setMatrixActive(false)
|
| 162 |
-
}
|
| 163 |
|
| 164 |
const openHelpModal = () => {
|
| 165 |
setHelpModalOpen(true)
|
|
@@ -195,37 +191,42 @@ export function Desktop() {
|
|
| 195 |
}
|
| 196 |
}
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
// Initialize session automatically on mount
|
| 199 |
useEffect(() => {
|
| 200 |
const initializeSession = async () => {
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
}
|
| 225 |
-
} catch (error) {
|
| 226 |
-
console.error('Failed to validate session, creating new one:', error)
|
| 227 |
}
|
|
|
|
|
|
|
| 228 |
}
|
|
|
|
| 229 |
|
| 230 |
// Create new session (either no saved session or validation failed)
|
| 231 |
try {
|
|
@@ -242,16 +243,16 @@ export function Desktop() {
|
|
| 242 |
|
| 243 |
const data = await response.json()
|
| 244 |
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
|
| 250 |
-
|
| 251 |
-
|
| 252 |
|
| 253 |
-
|
| 254 |
-
|
| 255 |
} catch (error) {
|
| 256 |
console.error('Failed to create session:', error)
|
| 257 |
}
|
|
@@ -326,7 +327,12 @@ export function Desktop() {
|
|
| 326 |
</svg>
|
| 327 |
</div>
|
| 328 |
|
| 329 |
-
<TopBar
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
<Dock onOpenFileManager={openFileManager} onOpenCalendar={openCalendar} onOpenClock={openClock} onOpenBrowser={openBrowser} onOpenGeminiChat={openGeminiChat} />
|
| 332 |
|
|
@@ -550,10 +556,7 @@ export function Desktop() {
|
|
| 550 |
onAction={handleContextMenuAction}
|
| 551 |
/>
|
| 552 |
|
| 553 |
-
|
| 554 |
-
<div onClick={handleMatrixComplete} style={{ cursor: matrixActive ? 'pointer' : 'default' }}>
|
| 555 |
-
<MatrixRain isActive={matrixActive} onComplete={handleMatrixComplete} />
|
| 556 |
-
</div>
|
| 557 |
|
| 558 |
{/* Help Modal */}
|
| 559 |
<HelpModal isOpen={helpModalOpen} onClose={closeHelpModal} />
|
|
@@ -574,6 +577,13 @@ export function Desktop() {
|
|
| 574 |
currentBackground={currentBackground}
|
| 575 |
/>
|
| 576 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
{/* About Modal */}
|
| 578 |
<AboutModal
|
| 579 |
isOpen={aboutModalOpen}
|
|
|
|
| 6 |
import { FileManager } from './FileManager'
|
| 7 |
import { Calendar } from './Calendar'
|
| 8 |
import { DraggableDesktopIcon } from './DraggableDesktopIcon'
|
| 9 |
+
|
| 10 |
import { HelpModal } from './HelpModal'
|
| 11 |
import { DesktopContextMenu } from './DesktopContextMenu'
|
| 12 |
import { BackgroundSelector } from './BackgroundSelector'
|
|
|
|
| 20 |
import { SessionManagerWindow } from './SessionManagerWindow'
|
| 21 |
import { FlutterRunner } from './FlutterRunner'
|
| 22 |
import { motion, AnimatePresence } from 'framer-motion'
|
| 23 |
+
import { SystemPowerOverlay } from './SystemPowerOverlay'
|
| 24 |
|
| 25 |
export function Desktop() {
|
| 26 |
const [fileManagerOpen, setFileManagerOpen] = useState(true)
|
|
|
|
| 36 |
const [sessionKey, setSessionKey] = useState<string>('')
|
| 37 |
const [sessionInitialized, setSessionInitialized] = useState(false)
|
| 38 |
const [currentPath, setCurrentPath] = useState('')
|
| 39 |
+
|
| 40 |
const [helpModalOpen, setHelpModalOpen] = useState(false)
|
| 41 |
const [contextMenuOpen, setContextMenuOpen] = useState(false)
|
| 42 |
const [contextMenuPos, setContextMenuPos] = useState({ x: 0, y: 0 })
|
|
|
|
| 46 |
const [sessionManagerOpen, setSessionManagerOpen] = useState(false)
|
| 47 |
const [flutterRunnerOpen, setFlutterRunnerOpen] = useState(false)
|
| 48 |
const [activeFlutterApp, setActiveFlutterApp] = useState<any>(null)
|
| 49 |
+
const [powerState, setPowerState] = useState<'active' | 'sleep' | 'restart' | 'shutdown'>('active')
|
| 50 |
|
| 51 |
const openFileManager = (path: string) => {
|
| 52 |
setCurrentPath(path)
|
|
|
|
| 155 |
}
|
| 156 |
}
|
| 157 |
|
|
|
|
|
|
|
|
|
|
| 158 |
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
const openHelpModal = () => {
|
| 161 |
setHelpModalOpen(true)
|
|
|
|
| 191 |
}
|
| 192 |
}
|
| 193 |
|
| 194 |
+
const handleSleep = () => setPowerState('sleep')
|
| 195 |
+
const handleRestart = () => setPowerState('restart')
|
| 196 |
+
const handleShutdown = () => setPowerState('shutdown')
|
| 197 |
+
const handleWake = () => setPowerState('active')
|
| 198 |
+
|
| 199 |
// Initialize session automatically on mount
|
| 200 |
useEffect(() => {
|
| 201 |
const initializeSession = async () => {
|
| 202 |
+
// Check if session already exists in localStorage
|
| 203 |
+
const savedSessionId = localStorage.getItem('reubenOS_sessionId')
|
| 204 |
+
|
| 205 |
+
if (savedSessionId) {
|
| 206 |
+
// Validate the saved session first using session ID
|
| 207 |
+
console.log('🔍 Validating existing session:', savedSessionId)
|
| 208 |
+
try {
|
| 209 |
+
const validateResponse = await fetch('/api/sessions/verify', {
|
| 210 |
+
method: 'POST',
|
| 211 |
+
headers: { 'Content-Type': 'application/json' },
|
| 212 |
+
body: JSON.stringify({ sessionId: savedSessionId })
|
| 213 |
+
})
|
| 214 |
+
const validateData = await validateResponse.json()
|
| 215 |
+
|
| 216 |
+
if (validateData.success && validateData.valid) {
|
| 217 |
+
// Session is still valid - use it
|
| 218 |
+
setUserSession(savedSessionId)
|
| 219 |
+
setSessionKey(savedSessionId) // Use session ID as session key
|
| 220 |
+
setSessionInitialized(true)
|
| 221 |
+
console.log('✅ Existing session is valid:', savedSessionId)
|
| 222 |
+
return
|
| 223 |
+
} else {
|
| 224 |
+
console.log('⚠️ Existing session is invalid, creating new one...')
|
|
|
|
|
|
|
|
|
|
| 225 |
}
|
| 226 |
+
} catch (error) {
|
| 227 |
+
console.error('Failed to validate session, creating new one:', error)
|
| 228 |
}
|
| 229 |
+
}
|
| 230 |
|
| 231 |
// Create new session (either no saved session or validation failed)
|
| 232 |
try {
|
|
|
|
| 243 |
|
| 244 |
const data = await response.json()
|
| 245 |
|
| 246 |
+
if (data.success) {
|
| 247 |
+
setUserSession(data.session.id)
|
| 248 |
+
setSessionKey(data.session.id) // Use session ID as session key
|
| 249 |
+
setSessionInitialized(true)
|
| 250 |
|
| 251 |
+
// Save to localStorage (only need session ID now)
|
| 252 |
+
localStorage.setItem('reubenOS_sessionId', data.session.id)
|
| 253 |
|
| 254 |
+
console.log('✅ Created fresh session:', data.session.id)
|
| 255 |
+
}
|
| 256 |
} catch (error) {
|
| 257 |
console.error('Failed to create session:', error)
|
| 258 |
}
|
|
|
|
| 327 |
</svg>
|
| 328 |
</div>
|
| 329 |
|
| 330 |
+
<TopBar
|
| 331 |
+
onAboutClick={() => setAboutModalOpen(true)}
|
| 332 |
+
onSleep={handleSleep}
|
| 333 |
+
onRestart={handleRestart}
|
| 334 |
+
onShutdown={handleShutdown}
|
| 335 |
+
/>
|
| 336 |
|
| 337 |
<Dock onOpenFileManager={openFileManager} onOpenCalendar={openCalendar} onOpenClock={openClock} onOpenBrowser={openBrowser} onOpenGeminiChat={openGeminiChat} />
|
| 338 |
|
|
|
|
| 556 |
onAction={handleContextMenuAction}
|
| 557 |
/>
|
| 558 |
|
| 559 |
+
|
|
|
|
|
|
|
|
|
|
| 560 |
|
| 561 |
{/* Help Modal */}
|
| 562 |
<HelpModal isOpen={helpModalOpen} onClose={closeHelpModal} />
|
|
|
|
| 577 |
currentBackground={currentBackground}
|
| 578 |
/>
|
| 579 |
|
| 580 |
+
{/* System Power Overlay */}
|
| 581 |
+
<SystemPowerOverlay
|
| 582 |
+
state={powerState}
|
| 583 |
+
onWake={handleWake}
|
| 584 |
+
onRestartComplete={handleWake}
|
| 585 |
+
/>
|
| 586 |
+
|
| 587 |
{/* About Modal */}
|
| 588 |
<AboutModal
|
| 589 |
isOpen={aboutModalOpen}
|
app/components/FlutterRunner.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import React, { useState, useRef, useEffect } from 'react'
|
|
| 4 |
import Window from './Window'
|
| 5 |
import { Copy, CaretRight, CaretLeft, Code as CodeIcon, Play, FloppyDisk, PencilSimple } from '@phosphor-icons/react'
|
| 6 |
import Editor from '@monaco-editor/react'
|
|
|
|
| 7 |
|
| 8 |
interface FlutterRunnerProps {
|
| 9 |
file: {
|
|
@@ -24,7 +25,6 @@ export function FlutterRunner({ file, onClose }: FlutterRunnerProps) {
|
|
| 24 |
const [pubspec, setPubspec] = useState(file.pubspecYaml || '')
|
| 25 |
const [saving, setSaving] = useState(false)
|
| 26 |
const [saved, setSaved] = useState(false)
|
| 27 |
-
const iframeRef = useRef<HTMLIFrameElement>(null)
|
| 28 |
|
| 29 |
// Detect mobile and auto-close sidebar on mobile landscape
|
| 30 |
React.useEffect(() => {
|
|
@@ -89,14 +89,6 @@ export function FlutterRunner({ file, onClose }: FlutterRunnerProps) {
|
|
| 89 |
}
|
| 90 |
}
|
| 91 |
|
| 92 |
-
const handleRunInZapp = () => {
|
| 93 |
-
// Copy code and open Zapp in a new window
|
| 94 |
-
navigator.clipboard.writeText(code)
|
| 95 |
-
window.open('https://zapp.run', '_blank')
|
| 96 |
-
setCopySuccess(true)
|
| 97 |
-
setTimeout(() => setCopySuccess(false), 2000)
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
return (
|
| 101 |
<Window
|
| 102 |
id="flutter-runner"
|
|
@@ -143,43 +135,17 @@ export function FlutterRunner({ file, onClose }: FlutterRunnerProps) {
|
|
| 143 |
{saving ? 'Saving...' : saved ? 'Saved!' : 'Save'}
|
| 144 |
</button>
|
| 145 |
)}
|
| 146 |
-
<button
|
| 147 |
-
onClick={handleRunInZapp}
|
| 148 |
-
className="flex items-center gap-2 px-3 py-1.5 bg-purple-500 hover:bg-purple-600 text-white rounded-lg transition-colors text-sm font-medium"
|
| 149 |
-
>
|
| 150 |
-
<Play size={16} weight="bold" />
|
| 151 |
-
Run in Zapp
|
| 152 |
-
</button>
|
| 153 |
</div>
|
| 154 |
</div>
|
| 155 |
|
| 156 |
{/* Main Content Area */}
|
| 157 |
<div className="flex flex-1 overflow-hidden relative">
|
| 158 |
-
{/* Left Panel - Code Editor or
|
| 159 |
-
<div className="flex-1 relative">
|
| 160 |
{mode === 'preview' ? (
|
| 161 |
-
|
| 162 |
-
<
|
| 163 |
-
|
| 164 |
-
src="https://zapp.run"
|
| 165 |
-
className="w-full h-full border-0"
|
| 166 |
-
allow="accelerometer; camera; encrypted-media; geolocation; gyroscope; microphone; midi; clipboard-read; clipboard-write"
|
| 167 |
-
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-downloads"
|
| 168 |
-
title="Zapp Flutter IDE"
|
| 169 |
-
/>
|
| 170 |
-
|
| 171 |
-
{/* Instruction overlay */}
|
| 172 |
-
{!(isMobile && sidebarOpen) && (
|
| 173 |
-
<div className="absolute top-4 left-4 right-4 bg-blue-500/90 backdrop-blur-sm text-white px-4 py-3 rounded-lg shadow-lg border border-blue-400/50">
|
| 174 |
-
<div className="flex items-center gap-2">
|
| 175 |
-
<CodeIcon size={20} weight="bold" />
|
| 176 |
-
<p className="text-sm font-medium">
|
| 177 |
-
Click "Run in Zapp" to copy code and open Zapp in a new tab!
|
| 178 |
-
</p>
|
| 179 |
-
</div>
|
| 180 |
-
</div>
|
| 181 |
-
)}
|
| 182 |
-
</>
|
| 183 |
) : (
|
| 184 |
<div className="w-full h-full bg-gray-900">
|
| 185 |
<Editor
|
|
@@ -199,107 +165,96 @@ export function FlutterRunner({ file, onClose }: FlutterRunnerProps) {
|
|
| 199 |
/>
|
| 200 |
</div>
|
| 201 |
)}
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
{/* Collapsible Sidebar */}
|
| 205 |
-
{sidebarOpen && (
|
| 206 |
-
<div className={`${isMobile ? 'w-full absolute right-0 top-0 bottom-0 z-10' : 'w-[350px]'} border-l border-gray-200 bg-gradient-to-b from-gray-50 to-white flex flex-col shadow-lg`}>
|
| 207 |
-
{/* Sidebar Header */}
|
| 208 |
-
<div className="h-12 bg-gradient-to-r from-cyan-500 to-blue-500 flex items-center justify-between px-4 text-white">
|
| 209 |
-
<div className="flex items-center gap-2">
|
| 210 |
-
<CodeIcon size={20} weight="bold" />
|
| 211 |
-
<span className="font-semibold text-sm">Source Code</span>
|
| 212 |
-
</div>
|
| 213 |
-
<button
|
| 214 |
-
onClick={() => setSidebarOpen(false)}
|
| 215 |
-
className="hover:bg-white/20 p-1 rounded transition-colors"
|
| 216 |
-
title="Close sidebar"
|
| 217 |
-
>
|
| 218 |
-
<CaretRight size={20} weight="bold" />
|
| 219 |
-
</button>
|
| 220 |
-
</div>
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
<
|
| 229 |
-
|
| 230 |
-
className="flex items-center gap-1 px-3 py-1.5 bg-cyan-500 hover:bg-cyan-600 text-white text-xs rounded-md transition-colors shadow-sm"
|
| 231 |
-
>
|
| 232 |
-
<Copy size={14} weight="bold" />
|
| 233 |
-
{copySuccess ? 'Copied!' : 'Copy'}
|
| 234 |
-
</button>
|
| 235 |
-
</div>
|
| 236 |
-
<div className="bg-gray-900 rounded-lg p-3 max-h-64 overflow-y-auto">
|
| 237 |
-
<pre className="text-xs text-green-400 font-mono whitespace-pre-wrap break-words">
|
| 238 |
-
{code}
|
| 239 |
-
</pre>
|
| 240 |
</div>
|
| 241 |
-
<
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
</div>
|
| 245 |
|
| 246 |
-
{/*
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
<h3 className="text-sm font-semibold text-gray-700">Dependencies</h3>
|
| 250 |
-
<div className="bg-blue-50 rounded-lg p-3 border border-blue-100">
|
| 251 |
-
<ul className="space-y-1">
|
| 252 |
-
{file.dependencies.map((dep, index) => (
|
| 253 |
-
<li key={index} className="text-xs font-mono text-blue-900">
|
| 254 |
-
• {dep}
|
| 255 |
-
</li>
|
| 256 |
-
))}
|
| 257 |
-
</ul>
|
| 258 |
-
</div>
|
| 259 |
-
<p className="text-xs text-gray-600 italic">
|
| 260 |
-
💡 Add these to <code className="bg-gray-100 px-1 py-0.5 rounded">pubspec.yaml</code> in Zapp
|
| 261 |
-
</p>
|
| 262 |
-
</div>
|
| 263 |
-
)}
|
| 264 |
-
|
| 265 |
-
{/* Pubspec.yaml Section */}
|
| 266 |
-
{pubspec && (
|
| 267 |
<div className="space-y-2">
|
| 268 |
<div className="flex items-center justify-between">
|
| 269 |
-
<h3 className="text-sm font-semibold text-gray-700">
|
| 270 |
<button
|
| 271 |
-
onClick={
|
| 272 |
-
className="flex items-center gap-1 px-3 py-1.5 bg-
|
| 273 |
>
|
| 274 |
<Copy size={14} weight="bold" />
|
| 275 |
-
Copy
|
| 276 |
</button>
|
| 277 |
</div>
|
| 278 |
-
<div className="bg-gray-900 rounded-lg p-3 max-h-
|
| 279 |
-
<pre className="text-xs text-
|
| 280 |
-
{
|
| 281 |
</pre>
|
| 282 |
</div>
|
| 283 |
-
<p className="text-xs text-gray-600 italic">
|
| 284 |
-
💡 Replace the entire <code className="bg-gray-100 px-1 py-0.5 rounded">pubspec.yaml</code> file
|
| 285 |
-
</p>
|
| 286 |
</div>
|
| 287 |
-
)}
|
| 288 |
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
</div>
|
| 300 |
</div>
|
| 301 |
-
|
| 302 |
-
)}
|
| 303 |
|
| 304 |
{/* Sidebar Toggle Button (when closed) */}
|
| 305 |
{!sidebarOpen && (
|
|
|
|
| 4 |
import Window from './Window'
|
| 5 |
import { Copy, CaretRight, CaretLeft, Code as CodeIcon, Play, FloppyDisk, PencilSimple } from '@phosphor-icons/react'
|
| 6 |
import Editor from '@monaco-editor/react'
|
| 7 |
+
import { DartPadEmbed } from './DartPadEmbed'
|
| 8 |
|
| 9 |
interface FlutterRunnerProps {
|
| 10 |
file: {
|
|
|
|
| 25 |
const [pubspec, setPubspec] = useState(file.pubspecYaml || '')
|
| 26 |
const [saving, setSaving] = useState(false)
|
| 27 |
const [saved, setSaved] = useState(false)
|
|
|
|
| 28 |
|
| 29 |
// Detect mobile and auto-close sidebar on mobile landscape
|
| 30 |
React.useEffect(() => {
|
|
|
|
| 89 |
}
|
| 90 |
}
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
return (
|
| 93 |
<Window
|
| 94 |
id="flutter-runner"
|
|
|
|
| 135 |
{saving ? 'Saving...' : saved ? 'Saved!' : 'Save'}
|
| 136 |
</button>
|
| 137 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
</div>
|
| 139 |
</div>
|
| 140 |
|
| 141 |
{/* Main Content Area */}
|
| 142 |
<div className="flex flex-1 overflow-hidden relative">
|
| 143 |
+
{/* Left Panel - Code Editor or DartPad Preview */}
|
| 144 |
+
<div className="flex-1 relative bg-gray-900">
|
| 145 |
{mode === 'preview' ? (
|
| 146 |
+
<div className="w-full h-full overflow-hidden">
|
| 147 |
+
<DartPadEmbed code={code} theme="dark" />
|
| 148 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
) : (
|
| 150 |
<div className="w-full h-full bg-gray-900">
|
| 151 |
<Editor
|
|
|
|
| 165 |
/>
|
| 166 |
</div>
|
| 167 |
)}
|
| 168 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
+
{/* Collapsible Sidebar */}
|
| 171 |
+
{sidebarOpen && (
|
| 172 |
+
<div className={`${isMobile ? 'w-full absolute right-0 top-0 bottom-0 z-10' : 'w-[350px]'} border-l border-gray-200 bg-gradient-to-b from-gray-50 to-white flex flex-col shadow-lg`}>
|
| 173 |
+
{/* Sidebar Header */}
|
| 174 |
+
<div className="h-12 bg-gradient-to-r from-cyan-500 to-blue-500 flex items-center justify-between px-4 text-white">
|
| 175 |
+
<div className="flex items-center gap-2">
|
| 176 |
+
<CodeIcon size={20} weight="bold" />
|
| 177 |
+
<span className="font-semibold text-sm">Source Code</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
</div>
|
| 179 |
+
<button
|
| 180 |
+
onClick={() => setSidebarOpen(false)}
|
| 181 |
+
className="hover:bg-white/20 p-1 rounded transition-colors"
|
| 182 |
+
title="Close sidebar"
|
| 183 |
+
>
|
| 184 |
+
<CaretRight size={20} weight="bold" />
|
| 185 |
+
</button>
|
| 186 |
</div>
|
| 187 |
|
| 188 |
+
{/* Sidebar Content */}
|
| 189 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
| 190 |
+
{/* Dart Code Section */}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
<div className="space-y-2">
|
| 192 |
<div className="flex items-center justify-between">
|
| 193 |
+
<h3 className="text-sm font-semibold text-gray-700">Main Dart Code</h3>
|
| 194 |
<button
|
| 195 |
+
onClick={handleCopyCode}
|
| 196 |
+
className="flex items-center gap-1 px-3 py-1.5 bg-cyan-500 hover:bg-cyan-600 text-white text-xs rounded-md transition-colors shadow-sm"
|
| 197 |
>
|
| 198 |
<Copy size={14} weight="bold" />
|
| 199 |
+
{copySuccess ? 'Copied!' : 'Copy'}
|
| 200 |
</button>
|
| 201 |
</div>
|
| 202 |
+
<div className="bg-gray-900 rounded-lg p-3 max-h-64 overflow-y-auto">
|
| 203 |
+
<pre className="text-xs text-green-400 font-mono whitespace-pre-wrap break-words">
|
| 204 |
+
{code}
|
| 205 |
</pre>
|
| 206 |
</div>
|
|
|
|
|
|
|
|
|
|
| 207 |
</div>
|
|
|
|
| 208 |
|
| 209 |
+
{/* Dependencies Section */}
|
| 210 |
+
{file.dependencies && file.dependencies.length > 0 && (
|
| 211 |
+
<div className="space-y-2">
|
| 212 |
+
<h3 className="text-sm font-semibold text-gray-700">Dependencies</h3>
|
| 213 |
+
<div className="bg-blue-50 rounded-lg p-3 border border-blue-100">
|
| 214 |
+
<ul className="space-y-1">
|
| 215 |
+
{file.dependencies.map((dep, index) => (
|
| 216 |
+
<li key={index} className="text-xs font-mono text-blue-900">
|
| 217 |
+
• {dep}
|
| 218 |
+
</li>
|
| 219 |
+
))}
|
| 220 |
+
</ul>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
)}
|
| 224 |
+
|
| 225 |
+
{/* Pubspec.yaml Section */}
|
| 226 |
+
{pubspec && (
|
| 227 |
+
<div className="space-y-2">
|
| 228 |
+
<div className="flex items-center justify-between">
|
| 229 |
+
<h3 className="text-sm font-semibold text-gray-700">pubspec.yaml</h3>
|
| 230 |
+
<button
|
| 231 |
+
onClick={handleCopyPubspec}
|
| 232 |
+
className="flex items-center gap-1 px-3 py-1.5 bg-purple-500 hover:bg-purple-600 text-white text-xs rounded-md transition-colors shadow-sm"
|
| 233 |
+
>
|
| 234 |
+
<Copy size={14} weight="bold" />
|
| 235 |
+
Copy
|
| 236 |
+
</button>
|
| 237 |
+
</div>
|
| 238 |
+
<div className="bg-gray-900 rounded-lg p-3 max-h-48 overflow-y-auto">
|
| 239 |
+
<pre className="text-xs text-yellow-400 font-mono whitespace-pre-wrap break-words">
|
| 240 |
+
{pubspec}
|
| 241 |
+
</pre>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
)}
|
| 245 |
+
|
| 246 |
+
{/* Instructions */}
|
| 247 |
+
<div className="bg-gradient-to-br from-green-50 to-emerald-50 rounded-lg p-4 border border-green-200">
|
| 248 |
+
<h3 className="text-sm font-semibold text-green-800 mb-2">Quick Start</h3>
|
| 249 |
+
<ol className="text-xs text-green-900 space-y-1.5 list-decimal list-inside">
|
| 250 |
+
<li>Click <strong>"Edit Code"</strong> to modify the code</li>
|
| 251 |
+
<li>Click <strong>"Save"</strong> to save your changes</li>
|
| 252 |
+
<li>Click <strong>"Preview"</strong> to run in DartPad</li>
|
| 253 |
+
</ol>
|
| 254 |
+
</div>
|
| 255 |
</div>
|
| 256 |
</div>
|
| 257 |
+
)}
|
|
|
|
| 258 |
|
| 259 |
{/* Sidebar Toggle Button (when closed) */}
|
| 260 |
{!sidebarOpen && (
|
app/components/MatrixRain.tsx
DELETED
|
@@ -1,237 +0,0 @@
|
|
| 1 |
-
'use client'
|
| 2 |
-
|
| 3 |
-
import React, { useEffect, useRef, useState } from 'react'
|
| 4 |
-
import { motion, AnimatePresence } from 'framer-motion'
|
| 5 |
-
|
| 6 |
-
interface MatrixRainProps {
|
| 7 |
-
isActive: boolean
|
| 8 |
-
onComplete?: () => void
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
export function MatrixRain({ isActive, onComplete }: MatrixRainProps) {
|
| 12 |
-
const canvasRef = useRef<HTMLCanvasElement>(null)
|
| 13 |
-
const [showText, setShowText] = useState(false)
|
| 14 |
-
const requestRef = useRef<number>(0)
|
| 15 |
-
|
| 16 |
-
useEffect(() => {
|
| 17 |
-
if (!isActive) return
|
| 18 |
-
|
| 19 |
-
const canvas = canvasRef.current
|
| 20 |
-
if (!canvas) return
|
| 21 |
-
|
| 22 |
-
const ctx = canvas.getContext('2d')
|
| 23 |
-
if (!ctx) return
|
| 24 |
-
|
| 25 |
-
// Set canvas size
|
| 26 |
-
const resizeCanvas = () => {
|
| 27 |
-
canvas.width = window.innerWidth
|
| 28 |
-
canvas.height = window.innerHeight
|
| 29 |
-
}
|
| 30 |
-
resizeCanvas()
|
| 31 |
-
window.addEventListener('resize', resizeCanvas)
|
| 32 |
-
|
| 33 |
-
// Configuration
|
| 34 |
-
const fontSize = 16
|
| 35 |
-
const columns = Math.ceil(canvas.width / fontSize)
|
| 36 |
-
|
| 37 |
-
// State for each column
|
| 38 |
-
// y: current vertical position
|
| 39 |
-
// speed: speed of the drop
|
| 40 |
-
// chars: array of characters in this column (for potential future complex effects, currently just generating on fly)
|
| 41 |
-
// lastDraw: timestamp of last update to control speed
|
| 42 |
-
const drops: { y: number; speed: number; lastDraw: number }[] = []
|
| 43 |
-
|
| 44 |
-
for (let i = 0; i < columns; i++) {
|
| 45 |
-
drops[i] = {
|
| 46 |
-
y: Math.random() * -1000, // Start above screen randomly
|
| 47 |
-
speed: Math.random() * 0.5 + 0.5, // Random speed between 0.5 and 1.0
|
| 48 |
-
lastDraw: 0
|
| 49 |
-
}
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
// Katakana + Latin + Numbers
|
| 53 |
-
const katakana = 'アァカサタナハマヤャラワガザダバパイィキシチニヒミリヰギジヂビピウゥクスツヌフムユュルグズブヅプエェケセテネヘメレヱゲゼデベペオォコソトノホモヨョロヲゴゾドボポヴッン'
|
| 54 |
-
const latin = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
| 55 |
-
const nums = '0123456789'
|
| 56 |
-
const alphabet = katakana + latin + nums
|
| 57 |
-
|
| 58 |
-
let lastTime = 0
|
| 59 |
-
const fps = 30
|
| 60 |
-
const frameInterval = 1000 / fps
|
| 61 |
-
|
| 62 |
-
const draw = (timestamp: number) => {
|
| 63 |
-
if (!ctx || !canvas) return
|
| 64 |
-
|
| 65 |
-
const deltaTime = timestamp - lastTime
|
| 66 |
-
|
| 67 |
-
if (deltaTime > frameInterval) {
|
| 68 |
-
lastTime = timestamp - (deltaTime % frameInterval)
|
| 69 |
-
|
| 70 |
-
// Semi-transparent black to create fade effect
|
| 71 |
-
// Adjust opacity for trail length (lower = longer trails)
|
| 72 |
-
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'
|
| 73 |
-
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
| 74 |
-
|
| 75 |
-
ctx.font = `${fontSize}px monospace`
|
| 76 |
-
ctx.textAlign = 'center'
|
| 77 |
-
|
| 78 |
-
for (let i = 0; i < drops.length; i++) {
|
| 79 |
-
const drop = drops[i]
|
| 80 |
-
|
| 81 |
-
// Draw character
|
| 82 |
-
const text = alphabet.charAt(Math.floor(Math.random() * alphabet.length))
|
| 83 |
-
const x = i * fontSize
|
| 84 |
-
|
| 85 |
-
// Glowing head effect
|
| 86 |
-
ctx.fillStyle = '#FFF' // White head
|
| 87 |
-
ctx.shadowBlur = 8
|
| 88 |
-
ctx.shadowColor = '#FFF'
|
| 89 |
-
ctx.fillText(text, x, drop.y * fontSize)
|
| 90 |
-
|
| 91 |
-
// Reset shadow for next characters (though we are clearing screen, this is for the trail effect if we were drawing full columns)
|
| 92 |
-
ctx.shadowBlur = 0
|
| 93 |
-
|
| 94 |
-
// Draw the trail character (slightly above the head)
|
| 95 |
-
// Actually, the fade effect handles the trail. We just need to draw the new head.
|
| 96 |
-
// To make it look "better", we can draw a green character at the same spot in the NEXT frame,
|
| 97 |
-
// but the fade rect handles the "turning green" part naturally if we just draw white.
|
| 98 |
-
// However, standard matrix is: Head is white, immediate trail is bright green, tail is dark green.
|
| 99 |
-
// With the fade rect method, everything drawn turns darker green over time.
|
| 100 |
-
// So drawing White is correct for the head.
|
| 101 |
-
|
| 102 |
-
// Let's also draw a green character strictly at the previous position to ensure it stays green and not just faded white
|
| 103 |
-
ctx.fillStyle = '#0F0'
|
| 104 |
-
ctx.fillText(text, x, (drop.y - 1) * fontSize)
|
| 105 |
-
|
| 106 |
-
// Move drop
|
| 107 |
-
if (drop.y * fontSize > canvas.height && Math.random() > 0.975) {
|
| 108 |
-
drop.y = 0
|
| 109 |
-
drop.speed = Math.random() * 0.5 + 0.5 // New random speed
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
drop.y += drop.speed
|
| 113 |
-
}
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
requestRef.current = requestAnimationFrame(draw)
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
requestRef.current = requestAnimationFrame(draw)
|
| 120 |
-
|
| 121 |
-
// Special text timer
|
| 122 |
-
const specialWordTimer = setTimeout(() => {
|
| 123 |
-
setShowText(true)
|
| 124 |
-
}, 1500)
|
| 125 |
-
|
| 126 |
-
return () => {
|
| 127 |
-
window.removeEventListener('resize', resizeCanvas)
|
| 128 |
-
if (requestRef.current) cancelAnimationFrame(requestRef.current)
|
| 129 |
-
clearTimeout(specialWordTimer)
|
| 130 |
-
}
|
| 131 |
-
}, [isActive])
|
| 132 |
-
|
| 133 |
-
// Auto-complete
|
| 134 |
-
useEffect(() => {
|
| 135 |
-
if (!isActive) return
|
| 136 |
-
const timer = setTimeout(() => {
|
| 137 |
-
onComplete?.()
|
| 138 |
-
}, 8000) // Extended to 8s to enjoy the view
|
| 139 |
-
return () => clearTimeout(timer)
|
| 140 |
-
}, [isActive, onComplete])
|
| 141 |
-
|
| 142 |
-
return (
|
| 143 |
-
<AnimatePresence>
|
| 144 |
-
{isActive && (
|
| 145 |
-
<motion.div
|
| 146 |
-
initial={{ opacity: 0 }}
|
| 147 |
-
animate={{ opacity: 1 }}
|
| 148 |
-
exit={{ opacity: 0 }}
|
| 149 |
-
transition={{ duration: 1 }}
|
| 150 |
-
className="fixed inset-0 z-[9999] bg-black cursor-pointer"
|
| 151 |
-
onClick={onComplete}
|
| 152 |
-
>
|
| 153 |
-
<canvas
|
| 154 |
-
ref={canvasRef}
|
| 155 |
-
className="absolute inset-0 block"
|
| 156 |
-
/>
|
| 157 |
-
|
| 158 |
-
<AnimatePresence>
|
| 159 |
-
{showText && (
|
| 160 |
-
<motion.div
|
| 161 |
-
initial={{ opacity: 0, scale: 0.8, filter: 'blur(10px)' }}
|
| 162 |
-
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
|
| 163 |
-
exit={{ opacity: 0, scale: 1.2, filter: 'blur(20px)' }}
|
| 164 |
-
transition={{ duration: 1.5, ease: "easeOut" }}
|
| 165 |
-
className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none z-10"
|
| 166 |
-
>
|
| 167 |
-
<div className="relative">
|
| 168 |
-
<motion.h1
|
| 169 |
-
className="text-transparent font-mono font-bold tracking-[0.2em] text-6xl md:text-8xl lg:text-9xl"
|
| 170 |
-
style={{
|
| 171 |
-
WebkitTextStroke: '2px #0F0',
|
| 172 |
-
textShadow: '0 0 20px rgba(0, 255, 0, 0.5)'
|
| 173 |
-
}}
|
| 174 |
-
animate={{
|
| 175 |
-
textShadow: [
|
| 176 |
-
'0 0 20px rgba(0, 255, 0, 0.5)',
|
| 177 |
-
'0 0 40px rgba(0, 255, 0, 0.8)',
|
| 178 |
-
'0 0 20px rgba(0, 255, 0, 0.5)'
|
| 179 |
-
]
|
| 180 |
-
}}
|
| 181 |
-
transition={{ duration: 2, repeat: Infinity }}
|
| 182 |
-
>
|
| 183 |
-
REUBENOS
|
| 184 |
-
</motion.h1>
|
| 185 |
-
|
| 186 |
-
{/* Glitch effect layers */}
|
| 187 |
-
<motion.h1
|
| 188 |
-
className="absolute top-0 left-0 text-[#0F0] font-mono font-bold tracking-[0.2em] text-6xl md:text-8xl lg:text-9xl opacity-50 mix-blend-screen"
|
| 189 |
-
animate={{
|
| 190 |
-
x: [-2, 2, -2],
|
| 191 |
-
y: [1, -1, 1],
|
| 192 |
-
opacity: [0.5, 0.3, 0.5]
|
| 193 |
-
}}
|
| 194 |
-
transition={{ duration: 0.2, repeat: Infinity, repeatType: "mirror" }}
|
| 195 |
-
>
|
| 196 |
-
REUBENOS
|
| 197 |
-
</motion.h1>
|
| 198 |
-
<motion.h1
|
| 199 |
-
className="absolute top-0 left-0 text-[#F00] font-mono font-bold tracking-[0.2em] text-6xl md:text-8xl lg:text-9xl opacity-30 mix-blend-screen"
|
| 200 |
-
animate={{
|
| 201 |
-
x: [2, -2, 2],
|
| 202 |
-
y: [-1, 1, -1],
|
| 203 |
-
}}
|
| 204 |
-
transition={{ duration: 0.3, repeat: Infinity, repeatType: "mirror" }}
|
| 205 |
-
>
|
| 206 |
-
REUBENOS
|
| 207 |
-
</motion.h1>
|
| 208 |
-
</div>
|
| 209 |
-
|
| 210 |
-
<motion.div
|
| 211 |
-
initial={{ opacity: 0, y: 20 }}
|
| 212 |
-
animate={{ opacity: 1, y: 0 }}
|
| 213 |
-
transition={{ delay: 0.5, duration: 0.8 }}
|
| 214 |
-
className="mt-8 flex flex-col items-center gap-2"
|
| 215 |
-
>
|
| 216 |
-
<div className="text-[#0F0] font-mono text-xl tracking-widest uppercase">
|
| 217 |
-
System Breach Detected
|
| 218 |
-
</div>
|
| 219 |
-
<div className="h-px w-64 bg-gradient-to-r from-transparent via-[#0F0] to-transparent animate-pulse" />
|
| 220 |
-
<div className="text-[#0F0] font-mono text-xs opacity-70 mt-2">
|
| 221 |
-
ACCESS GRANTED_
|
| 222 |
-
</div>
|
| 223 |
-
</motion.div>
|
| 224 |
-
</motion.div>
|
| 225 |
-
)}
|
| 226 |
-
</AnimatePresence>
|
| 227 |
-
|
| 228 |
-
<div className="absolute bottom-8 left-0 right-0 text-center">
|
| 229 |
-
<p className="text-[#0F0] font-mono text-xs opacity-40 animate-pulse">
|
| 230 |
-
[ CLICK TO INITIALIZE ]
|
| 231 |
-
</p>
|
| 232 |
-
</div>
|
| 233 |
-
</motion.div>
|
| 234 |
-
)}
|
| 235 |
-
</AnimatePresence>
|
| 236 |
-
)
|
| 237 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/components/SystemPowerOverlay.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import React, { useEffect, useState } from 'react'
|
| 4 |
+
import { motion, AnimatePresence } from 'framer-motion'
|
| 5 |
+
import { Power } from '@phosphor-icons/react'
|
| 6 |
+
|
| 7 |
+
interface SystemPowerOverlayProps {
|
| 8 |
+
state: 'active' | 'sleep' | 'restart' | 'shutdown'
|
| 9 |
+
onWake: () => void
|
| 10 |
+
onRestartComplete: () => void
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function SystemPowerOverlay({ state, onWake, onRestartComplete }: SystemPowerOverlayProps) {
|
| 14 |
+
const [showPowerButton, setShowPowerButton] = useState(false)
|
| 15 |
+
|
| 16 |
+
// Handle Sleep Wake
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
if (state === 'sleep') {
|
| 19 |
+
// Small delay to prevent the click that triggered sleep from immediately waking it
|
| 20 |
+
const timer = setTimeout(() => {
|
| 21 |
+
const handleWake = () => onWake()
|
| 22 |
+
window.addEventListener('keydown', handleWake)
|
| 23 |
+
window.addEventListener('mousedown', handleWake)
|
| 24 |
+
window.addEventListener('touchstart', handleWake)
|
| 25 |
+
}, 500)
|
| 26 |
+
|
| 27 |
+
return () => {
|
| 28 |
+
clearTimeout(timer)
|
| 29 |
+
const handleWake = () => onWake()
|
| 30 |
+
window.removeEventListener('keydown', handleWake)
|
| 31 |
+
window.removeEventListener('mousedown', handleWake)
|
| 32 |
+
window.removeEventListener('touchstart', handleWake)
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
}, [state, onWake])
|
| 36 |
+
|
| 37 |
+
// Handle Restart
|
| 38 |
+
useEffect(() => {
|
| 39 |
+
if (state === 'restart') {
|
| 40 |
+
const timer = setTimeout(() => {
|
| 41 |
+
onRestartComplete()
|
| 42 |
+
}, 4000)
|
| 43 |
+
return () => clearTimeout(timer)
|
| 44 |
+
}
|
| 45 |
+
}, [state, onRestartComplete])
|
| 46 |
+
|
| 47 |
+
// Handle Shutdown
|
| 48 |
+
useEffect(() => {
|
| 49 |
+
if (state === 'shutdown') {
|
| 50 |
+
setShowPowerButton(false)
|
| 51 |
+
const timer = setTimeout(() => {
|
| 52 |
+
setShowPowerButton(true)
|
| 53 |
+
}, 3000)
|
| 54 |
+
return () => clearTimeout(timer)
|
| 55 |
+
}
|
| 56 |
+
}, [state])
|
| 57 |
+
|
| 58 |
+
if (state === 'active') return null
|
| 59 |
+
|
| 60 |
+
return (
|
| 61 |
+
<AnimatePresence>
|
| 62 |
+
<motion.div
|
| 63 |
+
initial={{ opacity: 0 }}
|
| 64 |
+
animate={{ opacity: 1 }}
|
| 65 |
+
exit={{ opacity: 0 }}
|
| 66 |
+
transition={{ duration: 0.8 }}
|
| 67 |
+
className="fixed inset-0 z-[9999] bg-black flex items-center justify-center text-white cursor-none"
|
| 68 |
+
>
|
| 69 |
+
{state === 'sleep' && (
|
| 70 |
+
<div className="hidden">Sleeping...</div>
|
| 71 |
+
)}
|
| 72 |
+
|
| 73 |
+
{state === 'restart' && (
|
| 74 |
+
<div className="flex flex-col items-center gap-12">
|
| 75 |
+
<div className="w-12 h-12 border-4 border-gray-700 border-t-gray-300 rounded-full animate-spin" />
|
| 76 |
+
<div className="w-48 h-1 bg-gray-800 rounded-full overflow-hidden">
|
| 77 |
+
<motion.div
|
| 78 |
+
className="h-full bg-white"
|
| 79 |
+
initial={{ width: "0%" }}
|
| 80 |
+
animate={{ width: "100%" }}
|
| 81 |
+
transition={{ duration: 3.5, ease: "easeInOut" }}
|
| 82 |
+
/>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
)}
|
| 86 |
+
|
| 87 |
+
{state === 'shutdown' && (
|
| 88 |
+
<div className="flex flex-col items-center w-full h-full justify-center cursor-auto">
|
| 89 |
+
<AnimatePresence mode='wait'>
|
| 90 |
+
{!showPowerButton ? (
|
| 91 |
+
<motion.div
|
| 92 |
+
key="shutdown-text"
|
| 93 |
+
initial={{ opacity: 0, scale: 0.95 }}
|
| 94 |
+
animate={{ opacity: 1, scale: 1 }}
|
| 95 |
+
exit={{ opacity: 0, scale: 1.05, filter: 'blur(10px)' }}
|
| 96 |
+
transition={{ duration: 1 }}
|
| 97 |
+
className="text-center"
|
| 98 |
+
>
|
| 99 |
+
<h1 className="text-5xl font-extralight tracking-[0.2em] mb-4 text-white/90">Reuben OS</h1>
|
| 100 |
+
<div className="h-px w-32 bg-gradient-to-r from-transparent via-white/50 to-transparent mx-auto mb-4" />
|
| 101 |
+
<p className="text-white/40 text-xs tracking-[0.3em] uppercase">System Shutdown</p>
|
| 102 |
+
</motion.div>
|
| 103 |
+
) : (
|
| 104 |
+
<motion.button
|
| 105 |
+
key="power-button"
|
| 106 |
+
initial={{ opacity: 0, scale: 0.8 }}
|
| 107 |
+
animate={{ opacity: 1, scale: 1 }}
|
| 108 |
+
whileHover={{ scale: 1.1 }}
|
| 109 |
+
whileTap={{ scale: 0.95 }}
|
| 110 |
+
onClick={onWake}
|
| 111 |
+
className="flex flex-col items-center gap-6 group cursor-pointer"
|
| 112 |
+
>
|
| 113 |
+
<div className="relative">
|
| 114 |
+
<div className="absolute inset-0 bg-white/20 rounded-full blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
| 115 |
+
<Power size={80} weight="thin" className="text-white/50 group-hover:text-white transition-colors duration-300 relative z-10" />
|
| 116 |
+
</div>
|
| 117 |
+
<span className="text-white/30 text-xs tracking-[0.2em] group-hover:text-white/80 transition-colors duration-300">SYSTEM OFF</span>
|
| 118 |
+
</motion.button>
|
| 119 |
+
)}
|
| 120 |
+
</AnimatePresence>
|
| 121 |
+
</div>
|
| 122 |
+
)}
|
| 123 |
+
</motion.div>
|
| 124 |
+
</AnimatePresence>
|
| 125 |
+
)
|
| 126 |
+
}
|
app/components/TopBar.tsx
CHANGED
|
@@ -15,12 +15,14 @@ import { motion, AnimatePresence } from 'framer-motion'
|
|
| 15 |
import { CalendarWidget } from './CalendarWidget'
|
| 16 |
|
| 17 |
interface TopBarProps {
|
| 18 |
-
onPowerAction?: () => void
|
| 19 |
activeAppName?: string
|
| 20 |
onAboutClick?: () => void
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
-
export function TopBar({
|
| 24 |
const [activeMenu, setActiveMenu] = useState<'apple' | 'battery' | 'wifi' | 'clock' | null>(null)
|
| 25 |
const [currentTime, setCurrentTime] = useState('')
|
| 26 |
const menuRef = useRef<HTMLDivElement>(null)
|
|
@@ -93,15 +95,27 @@ export function TopBar({ onPowerAction, activeAppName = 'Finder', onAboutClick }
|
|
| 93 |
System Settings...
|
| 94 |
</button>
|
| 95 |
<div className="h-px bg-gray-300/50 my-1 mx-2" />
|
| 96 |
-
<button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
Sleep
|
| 98 |
</button>
|
| 99 |
-
<button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
Restart...
|
| 101 |
</button>
|
| 102 |
<button
|
| 103 |
onClick={() => {
|
| 104 |
-
if (
|
| 105 |
setActiveMenu(null)
|
| 106 |
}}
|
| 107 |
className="text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors text-gray-800"
|
|
@@ -111,7 +125,6 @@ export function TopBar({ onPowerAction, activeAppName = 'Finder', onAboutClick }
|
|
| 111 |
<div className="h-px bg-gray-300/50 my-1 mx-2" />
|
| 112 |
<button
|
| 113 |
onClick={() => {
|
| 114 |
-
if (onPowerAction) onPowerAction()
|
| 115 |
setActiveMenu(null)
|
| 116 |
}}
|
| 117 |
className="text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors text-gray-800"
|
|
|
|
| 15 |
import { CalendarWidget } from './CalendarWidget'
|
| 16 |
|
| 17 |
interface TopBarProps {
|
|
|
|
| 18 |
activeAppName?: string
|
| 19 |
onAboutClick?: () => void
|
| 20 |
+
onSleep?: () => void
|
| 21 |
+
onRestart?: () => void
|
| 22 |
+
onShutdown?: () => void
|
| 23 |
}
|
| 24 |
|
| 25 |
+
export function TopBar({ activeAppName = 'Finder', onAboutClick, onSleep, onRestart, onShutdown }: TopBarProps) {
|
| 26 |
const [activeMenu, setActiveMenu] = useState<'apple' | 'battery' | 'wifi' | 'clock' | null>(null)
|
| 27 |
const [currentTime, setCurrentTime] = useState('')
|
| 28 |
const menuRef = useRef<HTMLDivElement>(null)
|
|
|
|
| 95 |
System Settings...
|
| 96 |
</button>
|
| 97 |
<div className="h-px bg-gray-300/50 my-1 mx-2" />
|
| 98 |
+
<button
|
| 99 |
+
onClick={() => {
|
| 100 |
+
if (onSleep) onSleep()
|
| 101 |
+
setActiveMenu(null)
|
| 102 |
+
}}
|
| 103 |
+
className="text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors text-gray-800"
|
| 104 |
+
>
|
| 105 |
Sleep
|
| 106 |
</button>
|
| 107 |
+
<button
|
| 108 |
+
onClick={() => {
|
| 109 |
+
if (onRestart) onRestart()
|
| 110 |
+
setActiveMenu(null)
|
| 111 |
+
}}
|
| 112 |
+
className="text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors text-gray-800"
|
| 113 |
+
>
|
| 114 |
Restart...
|
| 115 |
</button>
|
| 116 |
<button
|
| 117 |
onClick={() => {
|
| 118 |
+
if (onShutdown) onShutdown()
|
| 119 |
setActiveMenu(null)
|
| 120 |
}}
|
| 121 |
className="text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors text-gray-800"
|
|
|
|
| 125 |
<div className="h-px bg-gray-300/50 my-1 mx-2" />
|
| 126 |
<button
|
| 127 |
onClick={() => {
|
|
|
|
| 128 |
setActiveMenu(null)
|
| 129 |
}}
|
| 130 |
className="text-left px-4 py-1.5 hover:bg-blue-500 hover:text-white transition-colors text-gray-800"
|