@stacksjs/defaults 0.70.54
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/LICENSE.md +21 -0
- package/README.md +2 -0
- package/package.json +20 -0
- package/resources/assets/fonts/013717e9.woff +0 -0
- package/resources/assets/fonts/03355817.woff2 +0 -0
- package/resources/assets/fonts/0380de7e.woff2 +0 -0
- package/resources/assets/fonts/0ba5d32a.woff +0 -0
- package/resources/assets/fonts/0bd57ddf.woff2 +0 -0
- package/resources/assets/fonts/19de0a60.woff2 +0 -0
- package/resources/assets/fonts/20b87e84.woff +0 -0
- package/resources/assets/fonts/4bbc67e1.woff +0 -0
- package/resources/assets/fonts/576fb0d7.woff +0 -0
- package/resources/assets/fonts/5ea10ee5.woff2 +0 -0
- package/resources/assets/fonts/6413fc0a.woff +0 -0
- package/resources/assets/fonts/72c114cf.woff +0 -0
- package/resources/assets/fonts/73c57e73.woff2 +0 -0
- package/resources/assets/fonts/813dca6c.woff +0 -0
- package/resources/assets/fonts/8a9b0995.woff2 +0 -0
- package/resources/assets/fonts/90b4832f.woff +0 -0
- package/resources/assets/fonts/9394ab85.woff2 +0 -0
- package/resources/assets/fonts/9b3c7d7f.woff2 +0 -0
- package/resources/assets/fonts/Monaco.ttf +0 -0
- package/resources/assets/fonts/b1b36d74.woff +0 -0
- package/resources/assets/fonts/b5d74798.woff2 +0 -0
- package/resources/assets/fonts/b9f81a4d.woff +0 -0
- package/resources/assets/fonts/c9784008.woff2 +0 -0
- package/resources/assets/fonts/cf692227.woff +0 -0
- package/resources/assets/fonts/d1c6ab51.woff2 +0 -0
- package/resources/assets/fonts/e2eb62fc.woff +0 -0
- package/resources/assets/fonts/fe21f8d8.woff2 +0 -0
- package/resources/assets/fonts/nps/CampmateScript-Regular.woff2 +0 -0
- package/resources/assets/fonts/nps/NPS_2026-variable.woff2 +0 -0
- package/resources/assets/fonts/nps/RedwoodSerif-Regular.woff2 +0 -0
- package/resources/assets/fonts/nps/SequoiaSans-Regular.woff2 +0 -0
- package/resources/assets/fonts/nps/Switchback-Regular.woff2 +0 -0
- package/resources/assets/images/app-icon.png +0 -0
- package/resources/assets/images/bg-img.png +0 -0
- package/resources/assets/images/park-ridge.svg +15 -0
- package/resources/assets/images/topography.svg +1 -0
- package/resources/assets/scripts/main.ts +2153 -0
- package/resources/assets/styles/README.md +19 -0
- package/resources/assets/styles/docs/main.css +1 -0
- package/resources/assets/styles/docs/overrides.css +1 -0
- package/resources/assets/styles/docs/vars.css +8 -0
- package/resources/assets/styles/main.css +1419 -0
- package/resources/components/Button.stx +70 -0
- package/resources/components/Buttons/BaseButton.stx +39 -0
- package/resources/components/Buttons/Counter.stx +28 -0
- package/resources/components/Buttons/ToggleDark.stx +11 -0
- package/resources/components/Container.stx +10 -0
- package/resources/components/Dashboard/.gitkeep +0 -0
- package/resources/components/Dashboard/Auth/ForgotPassword.stx +127 -0
- package/resources/components/Dashboard/Auth/Login.stx +212 -0
- package/resources/components/Dashboard/Auth/Register.stx +306 -0
- package/resources/components/Dashboard/Avatar.stx +123 -0
- package/resources/components/Dashboard/Billing/ActivePlan.stx +57 -0
- package/resources/components/Dashboard/Billing/CardBrands.stx +32 -0
- package/resources/components/Dashboard/Billing/CardForm.stx +34 -0
- package/resources/components/Dashboard/Billing/OneTimePayment.stx +28 -0
- package/resources/components/Dashboard/Billing/PaymentForm.stx +33 -0
- package/resources/components/Dashboard/Billing/PaymentMethod.stx +66 -0
- package/resources/components/Dashboard/Billing/PaymentMethodList.stx +52 -0
- package/resources/components/Dashboard/Billing/Plans.stx +136 -0
- package/resources/components/Dashboard/Buttons/AppButton.stx +48 -0
- package/resources/components/Dashboard/Buttons/IconButton.stx +20 -0
- package/resources/components/Dashboard/Card.stx +78 -0
- package/resources/components/Dashboard/Chart.stx +75 -0
- package/resources/components/Dashboard/CodeEditor.stx +41 -0
- package/resources/components/Dashboard/Commerce/BulkActionBar.stx +70 -0
- package/resources/components/Dashboard/Commerce/CategoriesTable.stx +133 -0
- package/resources/components/Dashboard/Commerce/CouponsTable.stx +161 -0
- package/resources/components/Dashboard/Commerce/CustomersTable.stx +142 -0
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +80 -0
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryTable.stx +85 -0
- package/resources/components/Dashboard/Commerce/Delivery/DriversTable.stx +99 -0
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysTable.stx +92 -0
- package/resources/components/Dashboard/Commerce/Delivery/LicenseTemplatesTable.stx +88 -0
- package/resources/components/Dashboard/Commerce/Delivery/Pagination.stx +122 -0
- package/resources/components/Dashboard/Commerce/Delivery/SearchFilter.stx +23 -0
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +80 -0
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +59 -0
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesTable.stx +71 -0
- package/resources/components/Dashboard/Commerce/Delivery/TabNavigation.stx +36 -0
- package/resources/components/Dashboard/Commerce/Forms/CouponsForm.stx +191 -0
- package/resources/components/Dashboard/Commerce/OrdersTable.stx +131 -0
- package/resources/components/Dashboard/Commerce/ProductsTable.stx +170 -0
- package/resources/components/Dashboard/CreateRecordModal.stx +363 -0
- package/resources/components/Dashboard/DashboardLayout.stx +155 -0
- package/resources/components/Dashboard/Deployments/ActivityFeed.stx +54 -0
- package/resources/components/Dashboard/Deployments/DeployScript.stx +23 -0
- package/resources/components/Dashboard/Deployments/DeploymentHistory.stx +97 -0
- package/resources/components/Dashboard/Deployments/DeploymentList.stx +94 -0
- package/resources/components/Dashboard/Deployments/LiveTerminalOutput.stx +18 -0
- package/resources/components/Dashboard/Elements/Dropdown.stx +57 -0
- package/resources/components/Dashboard/Email/EmailActivityChart.stx +111 -0
- package/resources/components/Dashboard/Email/EmailCompose.stx +72 -0
- package/resources/components/Dashboard/Email/EmailDetail.stx +105 -0
- package/resources/components/Dashboard/Email/EmailList.stx +117 -0
- package/resources/components/Dashboard/Email/EmailSearch.stx +40 -0
- package/resources/components/Dashboard/Email/EmailSidebar.stx +133 -0
- package/resources/components/Dashboard/GlobalSearch.stx +124 -0
- package/resources/components/Dashboard/MobileSidebar.stx +47 -0
- package/resources/components/Dashboard/Modals/BaseModal.stx +50 -0
- package/resources/components/Dashboard/Modals/Popups/Alert.stx +112 -0
- package/resources/components/Dashboard/Modals/Popups/Toast.stx +70 -0
- package/resources/components/Dashboard/Modals/ToastWrapper.stx +35 -0
- package/resources/components/Dashboard/Monitoring/ErrorDetailModal.stx +228 -0
- package/resources/components/Dashboard/Monitoring/ErrorsTable.stx +184 -0
- package/resources/components/Dashboard/Navbar.stx +194 -0
- package/resources/components/Dashboard/NavbarModern.stx +188 -0
- package/resources/components/Dashboard/NotificationErrorModal.stx +105 -0
- package/resources/components/Dashboard/NotificationStatusBadge.stx +20 -0
- package/resources/components/Dashboard/Pagination.stx +90 -0
- package/resources/components/Dashboard/Queue/QueueTable.stx +127 -0
- package/resources/components/Dashboard/Settings/Forms/AIForm.stx +126 -0
- package/resources/components/Dashboard/Settings/Forms/AnalyticsForm.stx +52 -0
- package/resources/components/Dashboard/Settings/Forms/AppForm.stx +157 -0
- package/resources/components/Dashboard/Settings/Forms/CLIForm.stx +66 -0
- package/resources/components/Dashboard/Settings/Forms/CacheForm.stx +253 -0
- package/resources/components/Dashboard/Settings/Forms/CloudForm.stx +441 -0
- package/resources/components/Dashboard/Settings/Forms/DNSForm.stx +307 -0
- package/resources/components/Dashboard/Settings/Forms/DatabaseForm.stx +168 -0
- package/resources/components/Dashboard/Settings/Forms/EmailForm.stx +100 -0
- package/resources/components/Dashboard/Settings/Forms/FileSystemForm.stx +259 -0
- package/resources/components/Dashboard/Settings/Forms/HashingForm.stx +119 -0
- package/resources/components/Dashboard/Settings/Forms/LibraryForm.stx +389 -0
- package/resources/components/Dashboard/Settings/Forms/QueueForm.stx +219 -0
- package/resources/components/Dashboard/Settings/Forms/SearchEngineForm.stx +48 -0
- package/resources/components/Dashboard/Settings/Forms/SecurityForm.stx +143 -0
- package/resources/components/Dashboard/Settings/Forms/ServicesForm.stx +194 -0
- package/resources/components/Dashboard/Settings/Forms/StorageForm.stx +191 -0
- package/resources/components/Dashboard/Settings/Forms/TeamsForm.stx +33 -0
- package/resources/components/Dashboard/Settings/SettingsFormManager.stx +181 -0
- package/resources/components/Dashboard/SettingsHeader.stx +88 -0
- package/resources/components/Dashboard/SettingsSidebar.stx +234 -0
- package/resources/components/Dashboard/Sidebar.stx +23 -0
- package/resources/components/Dashboard/SidebarModern.stx +249 -0
- package/resources/components/Dashboard/Skeleton/LoadingCard.stx +15 -0
- package/resources/components/Dashboard/Skeleton/LoadingDetails.stx +26 -0
- package/resources/components/Dashboard/Table.stx +82 -0
- package/resources/components/Dashboard/TerminalContainer.stx +97 -0
- package/resources/components/Dashboard/Tooltip.stx +166 -0
- package/resources/components/Dashboard/Transaction/index.stx +88 -0
- package/resources/components/Dashboard/UI/ActivityTable.stx +54 -0
- package/resources/components/Dashboard/UI/Avatar.stx +102 -0
- package/resources/components/Dashboard/UI/Badge.stx +66 -0
- package/resources/components/Dashboard/UI/Button.stx +73 -0
- package/resources/components/Dashboard/UI/Card.stx +51 -0
- package/resources/components/Dashboard/UI/ChartCard.stx +36 -0
- package/resources/components/Dashboard/UI/Checkbox.stx +74 -0
- package/resources/components/Dashboard/UI/ConfirmDialog.stx +74 -0
- package/resources/components/Dashboard/UI/DataTable.stx +45 -0
- package/resources/components/Dashboard/UI/Dropdown.stx +72 -0
- package/resources/components/Dashboard/UI/DropdownItem.stx +80 -0
- package/resources/components/Dashboard/UI/EmptyState.stx +50 -0
- package/resources/components/Dashboard/UI/FilterBar.stx +48 -0
- package/resources/components/Dashboard/UI/Input.stx +77 -0
- package/resources/components/Dashboard/UI/Modal.stx +75 -0
- package/resources/components/Dashboard/UI/PageHeader.stx +69 -0
- package/resources/components/Dashboard/UI/PageLayout.stx +84 -0
- package/resources/components/Dashboard/UI/Pagination.stx +126 -0
- package/resources/components/Dashboard/UI/QuickLinks.stx +31 -0
- package/resources/components/Dashboard/UI/RouterLink.stx +31 -0
- package/resources/components/Dashboard/UI/Select.stx +84 -0
- package/resources/components/Dashboard/UI/ServiceHealth.stx +38 -0
- package/resources/components/Dashboard/UI/StatsCard.stx +79 -0
- package/resources/components/Dashboard/UI/Table.stx +143 -0
- package/resources/components/Dashboard/UI/Tabs.stx +45 -0
- package/resources/components/Dashboard/UI/Textarea.stx +93 -0
- package/resources/components/Dashboard/UI/Toggle.stx +84 -0
- package/resources/components/Dashboard/UI/WindowControls.stx +135 -0
- package/resources/components/Dashboard/Widget.stx +89 -0
- package/resources/components/Dashboard/WindowControls.stx +119 -0
- package/resources/components/Dashboard/index.ts +106 -0
- package/resources/components/Docs/Demo/ComboboxCode.md +114 -0
- package/resources/components/Docs/Demo/ComboboxDemo.stx +91 -0
- package/resources/components/Docs/Demo/DialogCode.md +42 -0
- package/resources/components/Docs/Demo/DialogDemo.stx +76 -0
- package/resources/components/Docs/Demo/DocsPlayground.stx +39 -0
- package/resources/components/Docs/Demo/DropdownCode.md +109 -0
- package/resources/components/Docs/Demo/DropdownDemo.stx +82 -0
- package/resources/components/Docs/Demo/Hero.stx +100 -0
- package/resources/components/Docs/Demo/ListboxCode.md +87 -0
- package/resources/components/Docs/Demo/ListboxDemo.stx +59 -0
- package/resources/components/Docs/Demo/NotificationCode.md +173 -0
- package/resources/components/Docs/Demo/NotificationDemo.stx +99 -0
- package/resources/components/Docs/Demo/PopoverCode.md +161 -0
- package/resources/components/Docs/Demo/PopoverDemo.stx +96 -0
- package/resources/components/Docs/Demo/RadioGroupCode.md +98 -0
- package/resources/components/Docs/Demo/RadioGroupDemo.stx +53 -0
- package/resources/components/Docs/Demo/StepperCode.md +216 -0
- package/resources/components/Docs/Demo/StepperDemo.stx +137 -0
- package/resources/components/Docs/Demo/SwitchCode.md +24 -0
- package/resources/components/Docs/Demo/SwitchDemo.stx +28 -0
- package/resources/components/Docs/Home.stx +7 -0
- package/resources/components/Docs/HomeContributors.stx +23 -0
- package/resources/components/Docs/HomeSponsors.stx +17 -0
- package/resources/components/Docs/HomeTeam.stx +18 -0
- package/resources/components/Docs/TeamMember.stx +79 -0
- package/resources/components/Email/EmailButton.stx +32 -0
- package/resources/components/Email/EmailDivider.stx +20 -0
- package/resources/components/Email/EmailImage.stx +24 -0
- package/resources/components/Email/EmailLayout.stx +55 -0
- package/resources/components/Email/EmailLink.stx +22 -0
- package/resources/components/Email/EmailSection.stx +20 -0
- package/resources/components/Email/EmailText.stx +30 -0
- package/resources/components/Forum/ForumBreadcrumb.stx +38 -0
- package/resources/components/Forum/ForumCategory.stx +16 -0
- package/resources/components/Forum/ForumCategoryItem.stx +46 -0
- package/resources/components/Forum/ForumLayout.stx +36 -0
- package/resources/components/Forum/ForumPagination.stx +39 -0
- package/resources/components/Forum/ForumPost.stx +71 -0
- package/resources/components/Forum/ForumReplyForm.stx +76 -0
- package/resources/components/Forum/ForumSidebar.stx +86 -0
- package/resources/components/Forum/ForumTopicItem.stx +77 -0
- package/resources/components/Forum/ForumTopicList.stx +23 -0
- package/resources/components/HelloWorld.stx +253 -0
- package/resources/components/Logo.stx +10 -0
- package/resources/components/Marketing/CallToAction.stx +30 -0
- package/resources/components/Marketing/ComingSoon.stx +155 -0
- package/resources/components/Marketing/Faqs.stx +99 -0
- package/resources/components/Marketing/Feature.stx +32 -0
- package/resources/components/Marketing/Fields.stx +28 -0
- package/resources/components/Marketing/Footer.stx +33 -0
- package/resources/components/Marketing/Header.stx +51 -0
- package/resources/components/Marketing/Hero.stx +46 -0
- package/resources/components/Marketing/Layout.stx +26 -0
- package/resources/components/Marketing/NavLink.stx +13 -0
- package/resources/components/Marketing/Plan.stx +43 -0
- package/resources/components/Marketing/Pricing.stx +120 -0
- package/resources/components/Marketing/PrimaryFeatures.stx +80 -0
- package/resources/components/Marketing/PrimaryFeatures2.stx +98 -0
- package/resources/components/Marketing/SecondaryFeatures.stx +80 -0
- package/resources/components/Marketing/Testimonials.stx +120 -0
- package/resources/components/Pagination.stx +149 -0
- package/resources/components/Playground.stx +9 -0
- package/resources/components/README.md +87 -0
- package/resources/components/Storefront/AnnouncementBar.stx +17 -0
- package/resources/components/Storefront/CartDrawer.stx +227 -0
- package/resources/components/Storefront/CheckoutSteps.stx +50 -0
- package/resources/components/Storefront/Footer.stx +81 -0
- package/resources/components/Storefront/Header.stx +66 -0
- package/resources/components/Storefront/OrderSummary.stx +79 -0
- package/resources/components/Storefront/ProductCard.stx +63 -0
- package/resources/components/Storefront/ProductGallery.stx +88 -0
- package/resources/components/Storefront/QuantitySelector.stx +97 -0
- package/resources/components/index.ts +13 -0
- package/resources/emails/email-verification.stx +46 -0
- package/resources/emails/layouts/base.html +113 -0
- package/resources/emails/password-changed.stx +55 -0
- package/resources/emails/password-reset.stx +43 -0
- package/resources/emails/subscription-confirmation.stx +99 -0
- package/resources/emails/welcome.stx +48 -0
- package/resources/functions/README.md +28 -0
- package/resources/functions/counter.ts +13 -0
- package/resources/functions/dark.ts +4 -0
- package/resources/functions/dashboard/analytics.ts +238 -0
- package/resources/functions/dashboard/config-io.ts +316 -0
- package/resources/functions/dashboard/data.ts +375 -0
- package/resources/functions/dashboard/library.ts +250 -0
- package/resources/functions/dashboard/sidebar.ts +537 -0
- package/resources/functions/dashboard/system.ts +150 -0
- package/resources/layouts/403.stx +188 -0
- package/resources/layouts/404.stx +34 -0
- package/resources/layouts/README.md +14 -0
- package/resources/layouts/dashboard/default.stx +20 -0
- package/resources/layouts/dashboard/guest.stx +10 -0
- package/resources/layouts/default.stx +3 -0
- package/resources/layouts/maintenance.stx +11 -0
- package/resources/layouts/storefront.stx +73 -0
- package/resources/layouts/system-tray/default.stx +3 -0
- package/resources/modules/.gitkeep +0 -0
- package/resources/plugins/.gitkeep +0 -0
- package/resources/plugins/preloader.ts +308 -0
- package/resources/types/README.md +3 -0
- package/resources/views/cart.stx +149 -0
- package/resources/views/checkout/contact.stx +101 -0
- package/resources/views/checkout/payment.stx +114 -0
- package/resources/views/checkout/shipping.stx +120 -0
- package/resources/views/coming-soon.stx +9 -0
- package/resources/views/dashboard/custom-page.stx +45 -0
- package/resources/views/dashboard/index.stx +24 -0
- package/resources/views/emails/welcome.stx +0 -0
- package/resources/views/errors/tester.stx +26 -0
- package/resources/views/index.stx +2865 -0
- package/resources/views/orders/[id].stx +130 -0
|
@@ -0,0 +1,2153 @@
|
|
|
1
|
+
console.log('=== JAVASCRIPT STARTING ===')
|
|
2
|
+
|
|
3
|
+
// Global variables
|
|
4
|
+
let taskbarContextMenu: HTMLElement | null = null
|
|
5
|
+
let taskbarContextWindowId: string | null = null
|
|
6
|
+
|
|
7
|
+
// Login System
|
|
8
|
+
const loginScreen = document.getElementById('login-screen')
|
|
9
|
+
const emailLoginForm = document.getElementById('email-login-form')
|
|
10
|
+
const licenseLoginForm = document.getElementById('license-login-form')
|
|
11
|
+
|
|
12
|
+
// Show login screen when Login app is clicked
|
|
13
|
+
function showLoginScreen() {
|
|
14
|
+
if (loginScreen)
|
|
15
|
+
loginScreen.classList.remove('hidden')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Show email login form
|
|
19
|
+
const emailLoginBtn = document.getElementById('email-login-btn')
|
|
20
|
+
if (emailLoginBtn) {
|
|
21
|
+
emailLoginBtn.addEventListener('click', () => {
|
|
22
|
+
if (loginScreen)
|
|
23
|
+
loginScreen.classList.add('hidden')
|
|
24
|
+
if (emailLoginForm)
|
|
25
|
+
emailLoginForm.classList.remove('hidden')
|
|
26
|
+
// Focus email input
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
const emailInput = document.getElementById('email-input') as HTMLInputElement
|
|
29
|
+
if (emailInput)
|
|
30
|
+
emailInput.focus()
|
|
31
|
+
}, 100)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Show license login form
|
|
36
|
+
const licenseLoginBtn = document.getElementById('license-login-btn')
|
|
37
|
+
if (licenseLoginBtn) {
|
|
38
|
+
licenseLoginBtn.addEventListener('click', () => {
|
|
39
|
+
if (loginScreen)
|
|
40
|
+
loginScreen.classList.add('hidden')
|
|
41
|
+
if (licenseLoginForm)
|
|
42
|
+
licenseLoginForm.classList.remove('hidden')
|
|
43
|
+
// Focus license input
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
const licenseInput = document.getElementById('license-input') as HTMLInputElement
|
|
46
|
+
if (licenseInput)
|
|
47
|
+
licenseInput.focus()
|
|
48
|
+
}, 100)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Hide login forms and show user selection
|
|
53
|
+
function hideLoginForms() {
|
|
54
|
+
if (emailLoginForm)
|
|
55
|
+
emailLoginForm.classList.add('hidden')
|
|
56
|
+
if (licenseLoginForm)
|
|
57
|
+
licenseLoginForm.classList.add('hidden')
|
|
58
|
+
if (loginScreen)
|
|
59
|
+
loginScreen.classList.remove('hidden')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Close login screens
|
|
63
|
+
function closeLoginScreens() {
|
|
64
|
+
if (loginScreen)
|
|
65
|
+
loginScreen.classList.add('hidden')
|
|
66
|
+
if (emailLoginForm)
|
|
67
|
+
emailLoginForm.classList.add('hidden')
|
|
68
|
+
if (licenseLoginForm)
|
|
69
|
+
licenseLoginForm.classList.add('hidden')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle email/password login
|
|
73
|
+
function handleEmailLogin() {
|
|
74
|
+
const emailInput = document.getElementById('email-input') as HTMLInputElement
|
|
75
|
+
const passwordInput = document.getElementById('password-input') as HTMLInputElement
|
|
76
|
+
|
|
77
|
+
const email = emailInput?.value.trim()
|
|
78
|
+
const password = passwordInput?.value.trim()
|
|
79
|
+
|
|
80
|
+
if (!email || !password) {
|
|
81
|
+
alert('Please enter both email and password')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For demo purposes, accept any non-empty credentials
|
|
86
|
+
// In production, this would validate against a backend
|
|
87
|
+
console.log('Email login successful:', email)
|
|
88
|
+
alert(`Login successful! Welcome ${email}`)
|
|
89
|
+
|
|
90
|
+
// Close login screens
|
|
91
|
+
closeLoginScreens()
|
|
92
|
+
|
|
93
|
+
// Clear form
|
|
94
|
+
emailInput.value = ''
|
|
95
|
+
passwordInput.value = ''
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle license key login
|
|
99
|
+
function handleLicenseLogin() {
|
|
100
|
+
const licenseInput = document.getElementById('license-input') as HTMLInputElement
|
|
101
|
+
const licenseKey = licenseInput?.value.trim()
|
|
102
|
+
|
|
103
|
+
if (!licenseKey) {
|
|
104
|
+
alert('Please enter a license key')
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// For demo purposes, accept any non-empty license key
|
|
109
|
+
// In production, this would validate against a backend
|
|
110
|
+
console.log('License login successful:', licenseKey)
|
|
111
|
+
alert(`License key activated successfully!`)
|
|
112
|
+
|
|
113
|
+
// Close login screens
|
|
114
|
+
closeLoginScreens()
|
|
115
|
+
|
|
116
|
+
// Clear form
|
|
117
|
+
licenseInput.value = ''
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Make functions available globally for onclick handlers
|
|
121
|
+
(window as any).showLoginScreen = showLoginScreen;
|
|
122
|
+
(window as any).hideLoginForms = hideLoginForms;
|
|
123
|
+
(window as any).closeLoginScreens = closeLoginScreens;
|
|
124
|
+
(window as any).handleEmailLogin = handleEmailLogin;
|
|
125
|
+
(window as any).handleLicenseLogin = handleLicenseLogin
|
|
126
|
+
|
|
127
|
+
// Enter key handlers for login forms
|
|
128
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
129
|
+
const emailInput = document.getElementById('email-input')
|
|
130
|
+
const passwordInput = document.getElementById('password-input')
|
|
131
|
+
const licenseInput = document.getElementById('license-input')
|
|
132
|
+
|
|
133
|
+
if (emailInput || passwordInput) {
|
|
134
|
+
[emailInput, passwordInput].forEach((input) => {
|
|
135
|
+
input?.addEventListener('keydown', (e) => {
|
|
136
|
+
if (e.key === 'Enter') {
|
|
137
|
+
handleEmailLogin()
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (licenseInput) {
|
|
144
|
+
licenseInput.addEventListener('keydown', (e) => {
|
|
145
|
+
if (e.key === 'Enter') {
|
|
146
|
+
handleLicenseLogin()
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Clock
|
|
153
|
+
function updateClock() {
|
|
154
|
+
const now = new Date()
|
|
155
|
+
const hours = now.getHours() % 12 || 12
|
|
156
|
+
const minutes = String(now.getMinutes()).padStart(2, '0')
|
|
157
|
+
const ampm = now.getHours() >= 12 ? 'PM' : 'AM'
|
|
158
|
+
const month = now.getMonth() + 1
|
|
159
|
+
const day = now.getDate()
|
|
160
|
+
const year = now.getFullYear()
|
|
161
|
+
const clockElement = document.getElementById('clock')
|
|
162
|
+
if (clockElement) {
|
|
163
|
+
clockElement.textContent = `${hours}:${minutes} ${ampm} ${month}/${day}/${year}`
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
updateClock()
|
|
167
|
+
setInterval(updateClock, 1000) // Update every second
|
|
168
|
+
|
|
169
|
+
// Desktop Icon Positioning and Dragging
|
|
170
|
+
let draggedIcon: HTMLElement | null = null
|
|
171
|
+
const iconDragOffset = { x: 0, y: 0 }
|
|
172
|
+
let iconDragStartX = 0
|
|
173
|
+
let iconDragStartY = 0
|
|
174
|
+
let iconDragThresholdMet = false
|
|
175
|
+
let hasMove = false
|
|
176
|
+
let selectedIcon: HTMLElement | null = null
|
|
177
|
+
let activeEditInput: HTMLInputElement | null = null
|
|
178
|
+
let activeEditLabel: HTMLElement | null = null
|
|
179
|
+
let activeEditOriginalName: string = ''
|
|
180
|
+
const DRAG_THRESHOLD = 5 // pixels to move before drag starts
|
|
181
|
+
|
|
182
|
+
// Grid settings for icon snapping
|
|
183
|
+
const GRID_SIZE = 120 // spacing between icons
|
|
184
|
+
const GRID_OFFSET = 20 // offset from edge
|
|
185
|
+
|
|
186
|
+
// Background images collection
|
|
187
|
+
const BACKGROUND_IMAGES = [
|
|
188
|
+
'bg-img.png', // Original
|
|
189
|
+
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&q=80', // Mountain landscape
|
|
190
|
+
'https://images.unsplash.com/photo-1519681393784-d120267933ba?w=1920&q=80', // Mountain peaks
|
|
191
|
+
'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1920&q=80', // Foggy forest
|
|
192
|
+
'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1920&q=80', // Nature path
|
|
193
|
+
'https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=1920&q=80', // Lake and mountains
|
|
194
|
+
'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=1920&q=80', // Rolling hills
|
|
195
|
+
'https://images.unsplash.com/photo-1501594907352-04cda38ebc29?w=1920&q=80', // Tropical beach
|
|
196
|
+
'https://images.unsplash.com/photo-1518837695005-2083093ee35b?w=1920&q=80', // Ocean waves
|
|
197
|
+
'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=1920&q=80', // Beach sunset
|
|
198
|
+
]
|
|
199
|
+
let currentBackgroundIndex = 0
|
|
200
|
+
|
|
201
|
+
// Selection rectangle
|
|
202
|
+
let selectionRect: HTMLElement | null = null
|
|
203
|
+
let isSelecting = false
|
|
204
|
+
let selectionStartX = 0
|
|
205
|
+
let selectionStartY = 0
|
|
206
|
+
|
|
207
|
+
// Check if a grid position is occupied by another icon
|
|
208
|
+
function isGridPositionOccupied(x: number, y: number, excludeIcon?: HTMLElement): boolean {
|
|
209
|
+
const icons = document.querySelectorAll('.desktop-icon')
|
|
210
|
+
const tolerance = 10 // pixels of tolerance for position comparison
|
|
211
|
+
|
|
212
|
+
for (const icon of icons) {
|
|
213
|
+
if (excludeIcon && icon === excludeIcon)
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
const iconX = Number.parseInt((icon as HTMLElement).style.left || '0')
|
|
217
|
+
const iconY = Number.parseInt((icon as HTMLElement).style.top || '0')
|
|
218
|
+
|
|
219
|
+
if (Math.abs(iconX - x) < tolerance && Math.abs(iconY - y) < tolerance) {
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return false
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Find next available grid position
|
|
228
|
+
function findNextAvailableGridPosition(startX: number, startY: number, excludeIcon?: HTMLElement): { x: number, y: number } {
|
|
229
|
+
const startCol = Math.round((startX - GRID_OFFSET) / GRID_SIZE)
|
|
230
|
+
const startRow = Math.round((startY - GRID_OFFSET) / GRID_SIZE)
|
|
231
|
+
|
|
232
|
+
// Search in a spiral pattern from the starting position
|
|
233
|
+
const maxSearch = 100 // Maximum positions to check
|
|
234
|
+
|
|
235
|
+
for (let distance = 0; distance < maxSearch; distance++) {
|
|
236
|
+
// Try positions at increasing distances from the start
|
|
237
|
+
for (let rowOffset = -distance; rowOffset <= distance; rowOffset++) {
|
|
238
|
+
for (let colOffset = -distance; colOffset <= distance; colOffset++) {
|
|
239
|
+
// Only check positions on the perimeter of the current distance
|
|
240
|
+
if (Math.abs(rowOffset) !== distance && Math.abs(colOffset) !== distance) {
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const col = startCol + colOffset
|
|
245
|
+
const row = startRow + rowOffset
|
|
246
|
+
|
|
247
|
+
// Skip negative positions
|
|
248
|
+
if (col < 0 || row < 0)
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
const x = col * GRID_SIZE + GRID_OFFSET
|
|
252
|
+
const y = row * GRID_SIZE + GRID_OFFSET
|
|
253
|
+
|
|
254
|
+
if (!isGridPositionOccupied(x, y, excludeIcon)) {
|
|
255
|
+
return { x, y }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Fallback to original position if no space found
|
|
262
|
+
return { x: startX, y: startY }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Snap icon to grid
|
|
266
|
+
function snapToGrid(x: number, y: number, icon?: HTMLElement): { x: number, y: number } {
|
|
267
|
+
const col = Math.round((x - GRID_OFFSET) / GRID_SIZE)
|
|
268
|
+
const row = Math.round((y - GRID_OFFSET) / GRID_SIZE)
|
|
269
|
+
|
|
270
|
+
const targetX = col * GRID_SIZE + GRID_OFFSET
|
|
271
|
+
const targetY = row * GRID_SIZE + GRID_OFFSET
|
|
272
|
+
|
|
273
|
+
// Check if target position is occupied
|
|
274
|
+
if (isGridPositionOccupied(targetX, targetY, icon)) {
|
|
275
|
+
// Find next available position
|
|
276
|
+
return findNextAvailableGridPosition(targetX, targetY, icon)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { x: targetX, y: targetY }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Initialize icons when DOM is ready
|
|
283
|
+
function initializeIcons() {
|
|
284
|
+
// Position icons initially in a grid
|
|
285
|
+
const icons = document.querySelectorAll('.desktop-icon')
|
|
286
|
+
console.log('Positioning', icons.length, 'icons')
|
|
287
|
+
|
|
288
|
+
icons.forEach((icon, index) => {
|
|
289
|
+
const row = Math.floor(index / 6)
|
|
290
|
+
const col = index % 6
|
|
291
|
+
const left = col * GRID_SIZE + GRID_OFFSET
|
|
292
|
+
const top = row * GRID_SIZE + GRID_OFFSET;
|
|
293
|
+
(icon as HTMLElement).style.left = `${left}px`;
|
|
294
|
+
(icon as HTMLElement).style.top = `${top}px`
|
|
295
|
+
console.log(`Icon ${index} positioned at (${left}, ${top})`)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// Deselect icons when clicking desktop
|
|
299
|
+
const desktopIcons = document.querySelector('.desktop-icons')
|
|
300
|
+
desktopIcons?.addEventListener('click', (e) => {
|
|
301
|
+
if ((e.target as HTMLElement).classList.contains('desktop-icons')) {
|
|
302
|
+
// Exit edit mode if active
|
|
303
|
+
exitEditMode()
|
|
304
|
+
|
|
305
|
+
if (selectedIcon) {
|
|
306
|
+
selectedIcon.classList.remove('selected')
|
|
307
|
+
selectedIcon = null
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
// Desktop selection rectangle
|
|
313
|
+
desktopIcons?.addEventListener('mousedown', (e) => {
|
|
314
|
+
const event = e as MouseEvent
|
|
315
|
+
// Only start selection if clicking directly on desktop (not on icon)
|
|
316
|
+
if ((event.target as HTMLElement).classList.contains('desktop-icons')) {
|
|
317
|
+
isSelecting = true
|
|
318
|
+
const rect = desktopIcons.getBoundingClientRect()
|
|
319
|
+
selectionStartX = event.clientX - rect.left
|
|
320
|
+
selectionStartY = event.clientY - rect.top
|
|
321
|
+
|
|
322
|
+
// Clear previous selections
|
|
323
|
+
document.querySelectorAll('.desktop-icon.selected').forEach((icon) => {
|
|
324
|
+
icon.classList.remove('selected')
|
|
325
|
+
})
|
|
326
|
+
selectedIcon = null
|
|
327
|
+
|
|
328
|
+
// Create selection rectangle
|
|
329
|
+
if (!selectionRect) {
|
|
330
|
+
selectionRect = document.createElement('div')
|
|
331
|
+
selectionRect.className = 'selection-rectangle'
|
|
332
|
+
desktopIcons.appendChild(selectionRect)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
selectionRect.style.left = `${selectionStartX}px`
|
|
336
|
+
selectionRect.style.top = `${selectionStartY}px`
|
|
337
|
+
selectionRect.style.width = '0px'
|
|
338
|
+
selectionRect.style.height = '0px'
|
|
339
|
+
selectionRect.style.display = 'block'
|
|
340
|
+
|
|
341
|
+
event.preventDefault()
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
document.addEventListener('mousemove', (e) => {
|
|
346
|
+
if (!isSelecting || !selectionRect || !desktopIcons)
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
const rect = desktopIcons.getBoundingClientRect()
|
|
350
|
+
const currentX = e.clientX - rect.left
|
|
351
|
+
const currentY = e.clientY - rect.top
|
|
352
|
+
|
|
353
|
+
const left = Math.min(selectionStartX, currentX)
|
|
354
|
+
const top = Math.min(selectionStartY, currentY)
|
|
355
|
+
const width = Math.abs(currentX - selectionStartX)
|
|
356
|
+
const height = Math.abs(currentY - selectionStartY)
|
|
357
|
+
|
|
358
|
+
selectionRect.style.left = `${left}px`
|
|
359
|
+
selectionRect.style.top = `${top}px`
|
|
360
|
+
selectionRect.style.width = `${width}px`
|
|
361
|
+
selectionRect.style.height = `${height}px`
|
|
362
|
+
|
|
363
|
+
// Check which icons are in the selection
|
|
364
|
+
const selectionRectBounds = {
|
|
365
|
+
left,
|
|
366
|
+
top,
|
|
367
|
+
right: left + width,
|
|
368
|
+
bottom: top + height,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
document.querySelectorAll('.desktop-icon').forEach((icon) => {
|
|
372
|
+
const iconEl = icon as HTMLElement
|
|
373
|
+
const iconLeft = Number.parseFloat(iconEl.style.left || '0')
|
|
374
|
+
const iconTop = Number.parseFloat(iconEl.style.top || '0')
|
|
375
|
+
const iconRight = iconLeft + iconEl.offsetWidth
|
|
376
|
+
const iconBottom = iconTop + iconEl.offsetHeight
|
|
377
|
+
|
|
378
|
+
// Check if icon intersects with selection rectangle
|
|
379
|
+
const intersects = !(
|
|
380
|
+
iconRight < selectionRectBounds.left
|
|
381
|
+
|| iconLeft > selectionRectBounds.right
|
|
382
|
+
|| iconBottom < selectionRectBounds.top
|
|
383
|
+
|| iconTop > selectionRectBounds.bottom
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if (intersects) {
|
|
387
|
+
iconEl.classList.add('selected')
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
iconEl.classList.remove('selected')
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
document.addEventListener('mouseup', () => {
|
|
396
|
+
if (isSelecting && selectionRect) {
|
|
397
|
+
isSelecting = false
|
|
398
|
+
selectionRect.style.display = 'none'
|
|
399
|
+
|
|
400
|
+
// Update selectedIcon to last selected if any
|
|
401
|
+
const selected = document.querySelectorAll('.desktop-icon.selected')
|
|
402
|
+
if (selected.length > 0) {
|
|
403
|
+
selectedIcon = selected[selected.length - 1] as HTMLElement
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
// Desktop Icon Clicks and Drag
|
|
409
|
+
document.querySelectorAll('.desktop-icon').forEach((icon) => {
|
|
410
|
+
let clickTimeout: number
|
|
411
|
+
let _clickCount = 0
|
|
412
|
+
|
|
413
|
+
icon.addEventListener('mousedown', (e) => {
|
|
414
|
+
const event = e as MouseEvent
|
|
415
|
+
// Don't start drag if clicking on label input
|
|
416
|
+
if ((event.target as HTMLElement).tagName === 'INPUT')
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
if ((event.target as HTMLElement).closest('.desktop-icon-label') || (event.target as HTMLElement).closest('.desktop-icon-image')) {
|
|
420
|
+
draggedIcon = icon as HTMLElement
|
|
421
|
+
hasMove = false
|
|
422
|
+
iconDragThresholdMet = false
|
|
423
|
+
iconDragStartX = event.clientX
|
|
424
|
+
iconDragStartY = event.clientY
|
|
425
|
+
|
|
426
|
+
const rect = icon.getBoundingClientRect()
|
|
427
|
+
iconDragOffset.x = event.clientX - rect.left
|
|
428
|
+
iconDragOffset.y = event.clientY - rect.top
|
|
429
|
+
|
|
430
|
+
event.preventDefault()
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
icon.addEventListener('click', (e) => {
|
|
435
|
+
const event = e as MouseEvent
|
|
436
|
+
if (hasMove)
|
|
437
|
+
return // Don't trigger click if we were dragging
|
|
438
|
+
if ((event.target as HTMLElement).tagName === 'INPUT')
|
|
439
|
+
return // Don't interfere with input editing
|
|
440
|
+
|
|
441
|
+
clearTimeout(clickTimeout)
|
|
442
|
+
_clickCount++
|
|
443
|
+
|
|
444
|
+
// Immediately show selection on first click
|
|
445
|
+
const wasAlreadySelected = selectedIcon === icon && icon.classList.contains('selected')
|
|
446
|
+
|
|
447
|
+
// Exit edit mode if clicking on a different icon
|
|
448
|
+
if (selectedIcon && selectedIcon !== icon) {
|
|
449
|
+
exitEditMode()
|
|
450
|
+
selectedIcon.classList.remove('selected')
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
icon.classList.add('selected')
|
|
454
|
+
selectedIcon = icon as HTMLElement
|
|
455
|
+
|
|
456
|
+
// If clicking on an already selected icon, enter edit mode after short delay
|
|
457
|
+
if (wasAlreadySelected) {
|
|
458
|
+
clickTimeout = window.setTimeout(() => {
|
|
459
|
+
enterEditMode(icon as HTMLElement)
|
|
460
|
+
_clickCount = 0
|
|
461
|
+
}, 500)
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
// Set timeout to reset click count
|
|
465
|
+
clickTimeout = window.setTimeout(() => {
|
|
466
|
+
_clickCount = 0
|
|
467
|
+
}, 250)
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
icon.addEventListener('dblclick', (e) => {
|
|
472
|
+
const event = e as MouseEvent
|
|
473
|
+
clearTimeout(clickTimeout)
|
|
474
|
+
_clickCount = 0
|
|
475
|
+
event.preventDefault()
|
|
476
|
+
|
|
477
|
+
// Double click - open
|
|
478
|
+
const type = (icon as HTMLElement).dataset.iconType
|
|
479
|
+
const section = (icon as HTMLElement).dataset.iconSection
|
|
480
|
+
const url = (icon as HTMLElement).dataset.iconUrl
|
|
481
|
+
|
|
482
|
+
if (type === 'window' && section) {
|
|
483
|
+
openWindow(section)
|
|
484
|
+
}
|
|
485
|
+
else if (type === 'link' && url) {
|
|
486
|
+
window.open(url, '_blank')
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
// Icon drag move
|
|
492
|
+
document.addEventListener('mousemove', (e) => {
|
|
493
|
+
if (!draggedIcon)
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
// Check if we've moved enough to start dragging
|
|
497
|
+
if (!iconDragThresholdMet) {
|
|
498
|
+
const deltaX = Math.abs(e.clientX - iconDragStartX)
|
|
499
|
+
const deltaY = Math.abs(e.clientY - iconDragStartY)
|
|
500
|
+
|
|
501
|
+
if (deltaX > DRAG_THRESHOLD || deltaY > DRAG_THRESHOLD) {
|
|
502
|
+
iconDragThresholdMet = true
|
|
503
|
+
hasMove = true
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
return // Don't drag yet, threshold not met
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
hasMove = true
|
|
511
|
+
|
|
512
|
+
const desktopIcons = document.querySelector('.desktop-icons')
|
|
513
|
+
if (!desktopIcons)
|
|
514
|
+
return
|
|
515
|
+
const desktopRect = desktopIcons.getBoundingClientRect()
|
|
516
|
+
|
|
517
|
+
const x = e.clientX - desktopRect.left - iconDragOffset.x
|
|
518
|
+
const y = e.clientY - desktopRect.top - iconDragOffset.y
|
|
519
|
+
|
|
520
|
+
draggedIcon.style.left = `${Math.max(0, Math.min(x, desktopRect.width - 100))}px`
|
|
521
|
+
draggedIcon.style.top = `${Math.max(0, Math.min(y, desktopRect.height - 100))}px`
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
// Icon drag end
|
|
525
|
+
document.addEventListener('mouseup', () => {
|
|
526
|
+
if (draggedIcon) {
|
|
527
|
+
// Snap to grid if icon was moved
|
|
528
|
+
if (iconDragThresholdMet || hasMove) {
|
|
529
|
+
const currentX = Number.parseInt(draggedIcon.style.left || '0')
|
|
530
|
+
const currentY = Number.parseInt(draggedIcon.style.top || '0')
|
|
531
|
+
|
|
532
|
+
const snapped = snapToGrid(currentX, currentY, draggedIcon)
|
|
533
|
+
|
|
534
|
+
draggedIcon.style.left = `${snapped.x}px`
|
|
535
|
+
draggedIcon.style.top = `${snapped.y}px`
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Reset drag state immediately (not in timeout)
|
|
539
|
+
draggedIcon = null
|
|
540
|
+
iconDragThresholdMet = false
|
|
541
|
+
|
|
542
|
+
// Only delay hasMove reset to prevent click after drag
|
|
543
|
+
setTimeout(() => {
|
|
544
|
+
hasMove = false
|
|
545
|
+
}, 100)
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Call initialization when DOM is ready
|
|
551
|
+
// window management
|
|
552
|
+
let activewindow = null
|
|
553
|
+
let windowZIndex = 100
|
|
554
|
+
|
|
555
|
+
if (document.readyState === 'loading') {
|
|
556
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
557
|
+
initializeIcons()
|
|
558
|
+
initializeWindowDragging()
|
|
559
|
+
// Set the welcome window as active initially
|
|
560
|
+
activewindow = document.getElementById('window-welcome')
|
|
561
|
+
// Update taskbar to show welcome window
|
|
562
|
+
updateTaskbar()
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
initializeIcons()
|
|
567
|
+
initializeWindowDragging()
|
|
568
|
+
// Set the welcome window as active initially
|
|
569
|
+
activewindow = document.getElementById('window-welcome')
|
|
570
|
+
// Update taskbar to show welcome window
|
|
571
|
+
updateTaskbar()
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function openWindow(windowId, fromTaskbarButton = null) {
|
|
575
|
+
console.log('Opening window:', windowId)
|
|
576
|
+
const windowEl = document.getElementById(`window-${windowId}`)
|
|
577
|
+
if (!windowEl) {
|
|
578
|
+
console.warn('window not found:', windowId)
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Check if window was previously minimized
|
|
583
|
+
const wasMinimized = windowEl.classList.contains('minimized')
|
|
584
|
+
|
|
585
|
+
if (wasMinimized) {
|
|
586
|
+
// If restoring from minimized, let the taskbar handler do the animation
|
|
587
|
+
windowEl.classList.remove('minimized')
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Add a smooth entry animation for first-time opens
|
|
591
|
+
if (!wasMinimized && !windowEl.classList.contains('active')) {
|
|
592
|
+
// If opened from a specific location (like taskbar), animate from there
|
|
593
|
+
if (fromTaskbarButton) {
|
|
594
|
+
// Make window visible but transparent to get correct position
|
|
595
|
+
windowEl.classList.add('restoring')
|
|
596
|
+
windowEl.style.opacity = '0'
|
|
597
|
+
windowEl.style.visibility = 'visible'
|
|
598
|
+
|
|
599
|
+
// Force reflow to get actual position
|
|
600
|
+
void windowEl.offsetHeight
|
|
601
|
+
|
|
602
|
+
const buttonRect = fromTaskbarButton.getBoundingClientRect()
|
|
603
|
+
const windowRect = windowEl.getBoundingClientRect()
|
|
604
|
+
const windowCenterX = windowRect.left + windowRect.width / 2
|
|
605
|
+
const windowCenterY = windowRect.top + windowRect.height / 2
|
|
606
|
+
const buttonCenterX = buttonRect.left + buttonRect.width / 2
|
|
607
|
+
const buttonCenterY = buttonRect.top + buttonRect.height / 2
|
|
608
|
+
|
|
609
|
+
const translateX = buttonCenterX - windowCenterX
|
|
610
|
+
const translateY = buttonCenterY - windowCenterY
|
|
611
|
+
|
|
612
|
+
// Start from button position
|
|
613
|
+
windowEl.style.transition = 'none'
|
|
614
|
+
windowEl.style.transformOrigin = 'center center'
|
|
615
|
+
windowEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(0.1)`
|
|
616
|
+
windowEl.style.opacity = '0'
|
|
617
|
+
windowEl.style.zIndex = ++windowZIndex
|
|
618
|
+
activewindow = windowEl
|
|
619
|
+
|
|
620
|
+
// Force reflow
|
|
621
|
+
void windowEl.offsetHeight
|
|
622
|
+
|
|
623
|
+
// Animate to window position
|
|
624
|
+
requestAnimationFrame(() => {
|
|
625
|
+
windowEl.style.transition = 'opacity 0.2s ease, transform 0.2s ease'
|
|
626
|
+
windowEl.style.transform = 'translate(0, 0) scale(1)'
|
|
627
|
+
windowEl.style.opacity = '1'
|
|
628
|
+
|
|
629
|
+
setTimeout(() => {
|
|
630
|
+
windowEl.classList.remove('restoring')
|
|
631
|
+
windowEl.classList.add('active')
|
|
632
|
+
windowEl.style.transition = ''
|
|
633
|
+
windowEl.style.transform = ''
|
|
634
|
+
windowEl.style.opacity = ''
|
|
635
|
+
windowEl.style.visibility = ''
|
|
636
|
+
windowEl.style.transformOrigin = ''
|
|
637
|
+
updateTaskbar()
|
|
638
|
+
}, 200)
|
|
639
|
+
})
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
// Simple fade-in for regular opens
|
|
643
|
+
windowEl.style.opacity = '0'
|
|
644
|
+
windowEl.style.transform = 'scale(0.95)'
|
|
645
|
+
windowEl.classList.add('active')
|
|
646
|
+
windowEl.style.zIndex = ++windowZIndex
|
|
647
|
+
activewindow = windowEl
|
|
648
|
+
|
|
649
|
+
// Trigger animation
|
|
650
|
+
requestAnimationFrame(() => {
|
|
651
|
+
windowEl.style.transition = 'opacity 0.15s ease, transform 0.15s ease'
|
|
652
|
+
windowEl.style.opacity = '1'
|
|
653
|
+
windowEl.style.transform = 'scale(1)'
|
|
654
|
+
|
|
655
|
+
setTimeout(() => {
|
|
656
|
+
windowEl.style.transition = ''
|
|
657
|
+
windowEl.style.transform = ''
|
|
658
|
+
windowEl.style.opacity = ''
|
|
659
|
+
}, 150)
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
windowEl.classList.add('active')
|
|
665
|
+
windowEl.style.zIndex = ++windowZIndex
|
|
666
|
+
activewindow = windowEl
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (!fromTaskbarButton) {
|
|
670
|
+
updateTaskbar()
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function closeWindow(windowId) {
|
|
675
|
+
console.log('Closing window:', windowId)
|
|
676
|
+
const windowEl = document.getElementById(`window-${windowId}`)
|
|
677
|
+
if (!windowEl)
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
windowEl.classList.remove('active', 'minimized', 'maximized')
|
|
681
|
+
if (activewindow === windowEl) {
|
|
682
|
+
activewindow = null
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
updateTaskbar()
|
|
686
|
+
|
|
687
|
+
// Show notification when welcome window is closed
|
|
688
|
+
if (windowId === 'welcome') {
|
|
689
|
+
showNotification()
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function minimizeWindow(windowId) {
|
|
694
|
+
console.log('Minimizing window:', windowId)
|
|
695
|
+
const windowEl = document.getElementById(`window-${windowId}`)
|
|
696
|
+
if (!windowEl)
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
// Remove active, add minimizing to keep visible
|
|
700
|
+
windowEl.classList.remove('active')
|
|
701
|
+
windowEl.classList.add('minimizing')
|
|
702
|
+
|
|
703
|
+
// Update taskbar first
|
|
704
|
+
updateTaskbar()
|
|
705
|
+
|
|
706
|
+
// Find the taskbar button for this window after a small delay
|
|
707
|
+
requestAnimationFrame(() => {
|
|
708
|
+
const taskbarButtons = document.querySelectorAll('.taskbar-task')
|
|
709
|
+
let targetButton = null
|
|
710
|
+
taskbarButtons.forEach((btn) => {
|
|
711
|
+
const btnText = btn.querySelector('.taskbar-task-label')?.textContent
|
|
712
|
+
const windowTitle = windowEl.querySelector('.window-title')?.textContent
|
|
713
|
+
if (btnText === windowTitle) {
|
|
714
|
+
targetButton = btn
|
|
715
|
+
}
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
if (targetButton) {
|
|
719
|
+
// Get positions
|
|
720
|
+
const windowRect = windowEl.getBoundingClientRect()
|
|
721
|
+
const buttonRect = targetButton.getBoundingClientRect()
|
|
722
|
+
|
|
723
|
+
// Calculate translation to button center (from window center)
|
|
724
|
+
const windowCenterX = windowRect.left + windowRect.width / 2
|
|
725
|
+
const windowCenterY = windowRect.top + windowRect.height / 2
|
|
726
|
+
const buttonCenterX = buttonRect.left + buttonRect.width / 2
|
|
727
|
+
const buttonCenterY = buttonRect.top + buttonRect.height / 2
|
|
728
|
+
|
|
729
|
+
const translateX = buttonCenterX - windowCenterX
|
|
730
|
+
const translateY = buttonCenterY - windowCenterY
|
|
731
|
+
|
|
732
|
+
// Set initial state (current position)
|
|
733
|
+
windowEl.style.transition = 'opacity 0.2s ease, transform 0.2s ease'
|
|
734
|
+
windowEl.style.transformOrigin = 'center center'
|
|
735
|
+
windowEl.style.transform = 'translate(0, 0) scale(1)'
|
|
736
|
+
windowEl.style.opacity = '1'
|
|
737
|
+
|
|
738
|
+
// Force reflow
|
|
739
|
+
void windowEl.offsetHeight
|
|
740
|
+
|
|
741
|
+
// Animate to taskbar button center
|
|
742
|
+
windowEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(0.1)`
|
|
743
|
+
windowEl.style.opacity = '0'
|
|
744
|
+
|
|
745
|
+
// After animation completes, hide window
|
|
746
|
+
setTimeout(() => {
|
|
747
|
+
windowEl.classList.remove('minimizing')
|
|
748
|
+
windowEl.classList.add('minimized')
|
|
749
|
+
windowEl.style.transition = ''
|
|
750
|
+
windowEl.style.transform = ''
|
|
751
|
+
windowEl.style.opacity = ''
|
|
752
|
+
windowEl.style.transformOrigin = ''
|
|
753
|
+
}, 200)
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
// Fallback - just fade out
|
|
757
|
+
windowEl.style.transition = 'opacity 0.2s ease, transform 0.2s ease'
|
|
758
|
+
windowEl.style.opacity = '1'
|
|
759
|
+
void windowEl.offsetHeight
|
|
760
|
+
windowEl.style.opacity = '0'
|
|
761
|
+
windowEl.style.transform = 'scale(0.8)'
|
|
762
|
+
|
|
763
|
+
setTimeout(() => {
|
|
764
|
+
windowEl.classList.remove('minimizing')
|
|
765
|
+
windowEl.classList.add('minimized')
|
|
766
|
+
windowEl.style.transition = ''
|
|
767
|
+
windowEl.style.transform = ''
|
|
768
|
+
windowEl.style.opacity = ''
|
|
769
|
+
windowEl.style.transformOrigin = ''
|
|
770
|
+
}, 200)
|
|
771
|
+
}
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
// Show notification when welcome window is minimized
|
|
775
|
+
if (windowId === 'welcome') {
|
|
776
|
+
showNotification()
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function toggleMaximizeWindow(windowId) {
|
|
781
|
+
console.log('Toggling maximize for window:', windowId)
|
|
782
|
+
const windowEl = document.getElementById(`window-${windowId}`)
|
|
783
|
+
if (!windowEl)
|
|
784
|
+
return
|
|
785
|
+
|
|
786
|
+
// Store original position and size before maximizing
|
|
787
|
+
if (!windowEl.classList.contains('maximized')) {
|
|
788
|
+
windowEl.dataset.originalLeft = windowEl.style.left
|
|
789
|
+
windowEl.dataset.originalTop = windowEl.style.top
|
|
790
|
+
windowEl.dataset.originalWidth = windowEl.style.width
|
|
791
|
+
windowEl.dataset.originalHeight = windowEl.style.height
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
windowEl.classList.toggle('maximized')
|
|
795
|
+
|
|
796
|
+
// Restore original position if unmaximizing
|
|
797
|
+
if (!windowEl.classList.contains('maximized')) {
|
|
798
|
+
if (windowEl.dataset.originalLeft)
|
|
799
|
+
windowEl.style.left = windowEl.dataset.originalLeft
|
|
800
|
+
if (windowEl.dataset.originalTop)
|
|
801
|
+
windowEl.style.top = windowEl.dataset.originalTop
|
|
802
|
+
if (windowEl.dataset.originalWidth)
|
|
803
|
+
windowEl.style.width = windowEl.dataset.originalWidth
|
|
804
|
+
if (windowEl.dataset.originalHeight)
|
|
805
|
+
windowEl.style.height = windowEl.dataset.originalHeight
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function updateTaskbar() {
|
|
810
|
+
const taskbarTasks = document.getElementById('taskbar-tasks')
|
|
811
|
+
taskbarTasks.innerHTML = ''
|
|
812
|
+
|
|
813
|
+
const windows = document.querySelectorAll('.window')
|
|
814
|
+
windows.forEach((windowEl) => {
|
|
815
|
+
const isOpen = windowEl.classList.contains('active')
|
|
816
|
+
|| windowEl.classList.contains('minimized')
|
|
817
|
+
|| windowEl.classList.contains('minimizing')
|
|
818
|
+
|| windowEl.classList.contains('restoring')
|
|
819
|
+
if (!isOpen)
|
|
820
|
+
return
|
|
821
|
+
|
|
822
|
+
const windowId = windowEl.id.replace('window-', '')
|
|
823
|
+
const icon = windowEl.querySelector('.window-icon').textContent
|
|
824
|
+
const title = windowEl.querySelector('.window-title').textContent
|
|
825
|
+
const isActive = windowEl.classList.contains('active') || windowEl.classList.contains('restoring')
|
|
826
|
+
|
|
827
|
+
const taskBtn = document.createElement('button')
|
|
828
|
+
taskBtn.className = `taskbar-task${isActive ? ' active' : ''}`
|
|
829
|
+
taskBtn.innerHTML = `
|
|
830
|
+
<div class="taskbar-task-icon">${icon}</div>
|
|
831
|
+
<div class="taskbar-task-label">${title}</div>
|
|
832
|
+
`
|
|
833
|
+
|
|
834
|
+
taskBtn.addEventListener('click', () => {
|
|
835
|
+
if (windowEl.classList.contains('minimized')) {
|
|
836
|
+
// Remove minimized and add restoring
|
|
837
|
+
windowEl.classList.remove('minimized')
|
|
838
|
+
windowEl.classList.add('restoring')
|
|
839
|
+
windowEl.style.zIndex = ++windowZIndex
|
|
840
|
+
|
|
841
|
+
// Get button and window centers
|
|
842
|
+
const buttonRect = taskBtn.getBoundingClientRect()
|
|
843
|
+
const windowRect = windowEl.getBoundingClientRect()
|
|
844
|
+
const windowCenterX = windowRect.left + windowRect.width / 2
|
|
845
|
+
const windowCenterY = windowRect.top + windowRect.height / 2
|
|
846
|
+
const buttonCenterX = buttonRect.left + buttonRect.width / 2
|
|
847
|
+
const buttonCenterY = buttonRect.top + buttonRect.height / 2
|
|
848
|
+
|
|
849
|
+
const translateX = buttonCenterX - windowCenterX
|
|
850
|
+
const translateY = buttonCenterY - windowCenterY
|
|
851
|
+
|
|
852
|
+
// Start from button position (no transition yet)
|
|
853
|
+
windowEl.style.transition = 'none'
|
|
854
|
+
windowEl.style.transformOrigin = 'center center'
|
|
855
|
+
windowEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(0.1)`
|
|
856
|
+
windowEl.style.opacity = '0'
|
|
857
|
+
|
|
858
|
+
// Force reflow
|
|
859
|
+
void windowEl.offsetHeight
|
|
860
|
+
|
|
861
|
+
// Enable transitions and animate to window position
|
|
862
|
+
requestAnimationFrame(() => {
|
|
863
|
+
windowEl.style.transition = 'opacity 0.2s ease, transform 0.2s ease'
|
|
864
|
+
windowEl.style.transform = 'translate(0, 0) scale(1)'
|
|
865
|
+
windowEl.style.opacity = '1'
|
|
866
|
+
|
|
867
|
+
// After animation, clean up
|
|
868
|
+
setTimeout(() => {
|
|
869
|
+
windowEl.classList.remove('restoring')
|
|
870
|
+
windowEl.classList.add('active')
|
|
871
|
+
windowEl.style.transition = ''
|
|
872
|
+
windowEl.style.transform = ''
|
|
873
|
+
windowEl.style.opacity = ''
|
|
874
|
+
windowEl.style.transformOrigin = ''
|
|
875
|
+
activewindow = windowEl
|
|
876
|
+
updateTaskbar()
|
|
877
|
+
}, 200)
|
|
878
|
+
})
|
|
879
|
+
}
|
|
880
|
+
else if (windowEl.classList.contains('active')) {
|
|
881
|
+
minimizeWindow(windowId)
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
windowEl.classList.add('active')
|
|
885
|
+
windowEl.style.zIndex = ++windowZIndex
|
|
886
|
+
activewindow = windowEl
|
|
887
|
+
updateTaskbar()
|
|
888
|
+
}
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
// Right-click context menu for taskbar buttons
|
|
892
|
+
taskBtn.addEventListener('contextmenu', (e) => {
|
|
893
|
+
e.preventDefault()
|
|
894
|
+
showTaskbarContextMenu(e, windowId, windowEl)
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
taskbarTasks.appendChild(taskBtn)
|
|
898
|
+
})
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Keyboard shortcuts for window management
|
|
902
|
+
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
903
|
+
// Escape key to close active window
|
|
904
|
+
if (e.key === 'Escape' || e.key === 'Esc') {
|
|
905
|
+
const openWindow = document.querySelector('.window.active:not(.minimized)')
|
|
906
|
+
|
|
907
|
+
if (openWindow) {
|
|
908
|
+
e.preventDefault()
|
|
909
|
+
const windowId = openWindow.id.replace('window-', '')
|
|
910
|
+
closeWindow(windowId)
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
// window dragging
|
|
916
|
+
let draggedwindow = null
|
|
917
|
+
let dragOffsetX = 0
|
|
918
|
+
let dragOffsetY = 0
|
|
919
|
+
|
|
920
|
+
function initializeWindowDragging() {
|
|
921
|
+
document.querySelectorAll('.window').forEach((windowEl) => {
|
|
922
|
+
const titlebar = windowEl.querySelector('.window-titlebar')
|
|
923
|
+
|
|
924
|
+
titlebar.addEventListener('mousedown', (e) => {
|
|
925
|
+
if (e.target.classList.contains('window-control'))
|
|
926
|
+
return
|
|
927
|
+
if (windowEl.classList.contains('maximized'))
|
|
928
|
+
return
|
|
929
|
+
|
|
930
|
+
draggedwindow = windowEl
|
|
931
|
+
dragOffsetX = e.clientX - windowEl.offsetLeft
|
|
932
|
+
dragOffsetY = e.clientY - windowEl.offsetTop
|
|
933
|
+
|
|
934
|
+
windowEl.style.zIndex = ++windowZIndex
|
|
935
|
+
windowEl.classList.add('active')
|
|
936
|
+
activewindow = windowEl
|
|
937
|
+
|
|
938
|
+
updateTaskbar()
|
|
939
|
+
})
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
document.addEventListener('mousemove', (e) => {
|
|
943
|
+
if (!draggedwindow)
|
|
944
|
+
return
|
|
945
|
+
|
|
946
|
+
const newX = e.clientX - dragOffsetX
|
|
947
|
+
const newY = e.clientY - dragOffsetY
|
|
948
|
+
|
|
949
|
+
draggedwindow.style.left = `${newX}px`
|
|
950
|
+
draggedwindow.style.top = `${newY}px`
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
document.addEventListener('mouseup', () => {
|
|
954
|
+
draggedwindow = null
|
|
955
|
+
})
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Feature switching
|
|
959
|
+
const featureContent = {
|
|
960
|
+
backend: {
|
|
961
|
+
auth: {
|
|
962
|
+
title: 'Authentication',
|
|
963
|
+
code: `import { Auth } from '@stacksjs/auth'
|
|
964
|
+
|
|
965
|
+
// Login user
|
|
966
|
+
const user = await Auth.attempt({
|
|
967
|
+
email: 'user@example.com',
|
|
968
|
+
password: 'secret'
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
// Get authenticated user
|
|
972
|
+
const current = await Auth.user()`,
|
|
973
|
+
},
|
|
974
|
+
orm: {
|
|
975
|
+
title: 'ORM',
|
|
976
|
+
code: `import { User } from './models/User'
|
|
977
|
+
|
|
978
|
+
// Eloquent-style queries
|
|
979
|
+
const users = await User.where('active', true)
|
|
980
|
+
.orderBy('created_at', 'desc')
|
|
981
|
+
.limit(10)
|
|
982
|
+
.get()
|
|
983
|
+
|
|
984
|
+
// Create new records
|
|
985
|
+
await User.create({
|
|
986
|
+
name: 'John Doe',
|
|
987
|
+
email: 'john@example.com'
|
|
988
|
+
})`,
|
|
989
|
+
},
|
|
990
|
+
validation: {
|
|
991
|
+
title: 'Validation',
|
|
992
|
+
code: `import { validate } from '@stacksjs/validation'
|
|
993
|
+
|
|
994
|
+
const result = await validate(request.body, {
|
|
995
|
+
email: 'required|email',
|
|
996
|
+
password: 'required|min:8',
|
|
997
|
+
age: 'numeric|min:18'
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
if (result.fails()) {
|
|
1001
|
+
return result.errors()
|
|
1002
|
+
}`,
|
|
1003
|
+
},
|
|
1004
|
+
storage: {
|
|
1005
|
+
title: 'Storage',
|
|
1006
|
+
code: `import { Storage } from '@stacksjs/storage'
|
|
1007
|
+
|
|
1008
|
+
// Store file
|
|
1009
|
+
await Storage.put('avatars/user.jpg', fileData)
|
|
1010
|
+
|
|
1011
|
+
// Retrieve file
|
|
1012
|
+
const file = await Storage.get('avatars/user.jpg')
|
|
1013
|
+
|
|
1014
|
+
// Delete file
|
|
1015
|
+
await Storage.delete('avatars/user.jpg')`,
|
|
1016
|
+
},
|
|
1017
|
+
queue: {
|
|
1018
|
+
title: 'Queues',
|
|
1019
|
+
code: `import { Queue } from '@stacksjs/queue'
|
|
1020
|
+
|
|
1021
|
+
// Dispatch job
|
|
1022
|
+
await Queue.dispatch('SendEmail', {
|
|
1023
|
+
to: 'user@example.com',
|
|
1024
|
+
subject: 'Welcome!'
|
|
1025
|
+
})
|
|
1026
|
+
|
|
1027
|
+
// Process jobs
|
|
1028
|
+
Queue.process('SendEmail', async (job) => {
|
|
1029
|
+
await sendEmail(job.data)
|
|
1030
|
+
})`,
|
|
1031
|
+
},
|
|
1032
|
+
events: {
|
|
1033
|
+
title: 'Events',
|
|
1034
|
+
code: `import { Event } from '@stacksjs/events'
|
|
1035
|
+
|
|
1036
|
+
// Dispatch event
|
|
1037
|
+
Event.dispatch('user.registered', { user })
|
|
1038
|
+
|
|
1039
|
+
// Listen for event
|
|
1040
|
+
Event.listen('user.registered', (data) => {
|
|
1041
|
+
console.log('New user:', data.user)
|
|
1042
|
+
})`,
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
frontend: {
|
|
1046
|
+
'components': {
|
|
1047
|
+
title: 'Components',
|
|
1048
|
+
code: `<template>
|
|
1049
|
+
<Card title="Hello">
|
|
1050
|
+
<p>Reusable components</p>
|
|
1051
|
+
</Card>
|
|
1052
|
+
</template>
|
|
1053
|
+
|
|
1054
|
+
<script setup>
|
|
1055
|
+
// Auto-imported
|
|
1056
|
+
</script>`,
|
|
1057
|
+
},
|
|
1058
|
+
'ui-kit': {
|
|
1059
|
+
title: 'UI Kit',
|
|
1060
|
+
code: `import { Button, Input, Modal } from '@stacksjs/ui'
|
|
1061
|
+
|
|
1062
|
+
<Button variant="primary">Click me</Button>
|
|
1063
|
+
<Input placeholder="Enter text..." />
|
|
1064
|
+
<Modal title="Welcome">
|
|
1065
|
+
<p>Hello there!</p>
|
|
1066
|
+
</Modal>`,
|
|
1067
|
+
},
|
|
1068
|
+
'routing': {
|
|
1069
|
+
title: 'Routing',
|
|
1070
|
+
code: `import { Router } from '@stacksjs/router'
|
|
1071
|
+
|
|
1072
|
+
Router.get('/', () => {
|
|
1073
|
+
return view('home')
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
Router.post('/api/users', async (request) => {
|
|
1077
|
+
const user = await User.create(request.body)
|
|
1078
|
+
return user
|
|
1079
|
+
})`,
|
|
1080
|
+
},
|
|
1081
|
+
'desktop': {
|
|
1082
|
+
title: 'Desktop',
|
|
1083
|
+
code: `import { app, window } from '@stacksjs/desktop'
|
|
1084
|
+
|
|
1085
|
+
// Create desktop app
|
|
1086
|
+
const win = window.create({
|
|
1087
|
+
title: 'My App',
|
|
1088
|
+
width: 800,
|
|
1089
|
+
height: 600
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
// Native features
|
|
1093
|
+
app.notification('Hello from desktop!')`,
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
cloud: {
|
|
1097
|
+
deployment: {
|
|
1098
|
+
title: 'Deployment',
|
|
1099
|
+
code: `export default {
|
|
1100
|
+
provider: 'aws',
|
|
1101
|
+
region: 'us-east-1',
|
|
1102
|
+
|
|
1103
|
+
compute: {
|
|
1104
|
+
type: 'serverless',
|
|
1105
|
+
memory: 1024
|
|
1106
|
+
}
|
|
1107
|
+
}`,
|
|
1108
|
+
},
|
|
1109
|
+
storage: {
|
|
1110
|
+
title: 'Storage',
|
|
1111
|
+
code: `export default {
|
|
1112
|
+
storage: {
|
|
1113
|
+
driver: 's3',
|
|
1114
|
+
bucket: 'my-bucket',
|
|
1115
|
+
region: 'us-east-1'
|
|
1116
|
+
}
|
|
1117
|
+
}`,
|
|
1118
|
+
},
|
|
1119
|
+
cdn: {
|
|
1120
|
+
title: 'CDN',
|
|
1121
|
+
code: `export default {
|
|
1122
|
+
cdn: {
|
|
1123
|
+
enabled: true,
|
|
1124
|
+
provider: 'cloudfront',
|
|
1125
|
+
domain: 'cdn.example.com'
|
|
1126
|
+
}
|
|
1127
|
+
}`,
|
|
1128
|
+
},
|
|
1129
|
+
dns: {
|
|
1130
|
+
title: 'DNS',
|
|
1131
|
+
code: `export default {
|
|
1132
|
+
dns: {
|
|
1133
|
+
provider: 'route53',
|
|
1134
|
+
domain: 'example.com',
|
|
1135
|
+
records: [
|
|
1136
|
+
{ type: 'A', name: '@', value: '1.2.3.4' }
|
|
1137
|
+
]
|
|
1138
|
+
}
|
|
1139
|
+
}`,
|
|
1140
|
+
},
|
|
1141
|
+
},
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function showFeature(section, featureId) {
|
|
1145
|
+
console.log('Showing feature:', section, featureId)
|
|
1146
|
+
|
|
1147
|
+
const buttons = document.querySelectorAll(`#window-${section} .feature-btn`)
|
|
1148
|
+
buttons.forEach(btn => btn.classList.remove('active'))
|
|
1149
|
+
event.target.classList.add('active')
|
|
1150
|
+
|
|
1151
|
+
const content = featureContent[section][featureId]
|
|
1152
|
+
if (!content)
|
|
1153
|
+
return
|
|
1154
|
+
|
|
1155
|
+
const contentEl = document.getElementById(`${section}-feature-content`)
|
|
1156
|
+
contentEl.innerHTML = `
|
|
1157
|
+
<h3>${content.title}</h3>
|
|
1158
|
+
<div class="code-display">${content.code}</div>
|
|
1159
|
+
`
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Start menu toggle
|
|
1163
|
+
const startButton = document.querySelector('.start-button')
|
|
1164
|
+
const startMenu = document.getElementById('start-menu')
|
|
1165
|
+
|
|
1166
|
+
startButton.addEventListener('click', (e) => {
|
|
1167
|
+
e.stopPropagation()
|
|
1168
|
+
startMenu.classList.toggle('active')
|
|
1169
|
+
})
|
|
1170
|
+
|
|
1171
|
+
document.addEventListener('click', (e) => {
|
|
1172
|
+
if (!e.target.closest('.start-menu') && !e.target.closest('.start-button')) {
|
|
1173
|
+
startMenu.classList.remove('active')
|
|
1174
|
+
}
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
// Notification
|
|
1178
|
+
const notification = document.getElementById('notification-popup')
|
|
1179
|
+
|
|
1180
|
+
function closeNotification() {
|
|
1181
|
+
notification.classList.add('hidden')
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function showNotification() {
|
|
1185
|
+
if (notification) {
|
|
1186
|
+
notification.classList.remove('hidden')
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Email signup helper
|
|
1191
|
+
async function subscribeEmail(email: string): Promise<{ success: boolean, message?: string }> {
|
|
1192
|
+
const response = await fetch('/api/email/subscribe', {
|
|
1193
|
+
method: 'POST',
|
|
1194
|
+
headers: {
|
|
1195
|
+
'Content-Type': 'application/json',
|
|
1196
|
+
},
|
|
1197
|
+
body: JSON.stringify({ email }),
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
if (response.ok) {
|
|
1201
|
+
return { success: true }
|
|
1202
|
+
}
|
|
1203
|
+
else {
|
|
1204
|
+
const data = await response.json().catch(() => ({}))
|
|
1205
|
+
return { success: false, message: data.message || 'Failed to subscribe. Please try again later.' }
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Email signup (notification popup)
|
|
1210
|
+
async function handleEmailSignup() {
|
|
1211
|
+
const emailInput = document.getElementById('notification-email') as HTMLInputElement
|
|
1212
|
+
const email = emailInput?.value.trim()
|
|
1213
|
+
|
|
1214
|
+
if (!email || !email.includes('@')) {
|
|
1215
|
+
alert('Please enter a valid email address')
|
|
1216
|
+
return
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
try {
|
|
1220
|
+
const result = await subscribeEmail(email)
|
|
1221
|
+
|
|
1222
|
+
if (result.success) {
|
|
1223
|
+
const signupEl = document.getElementById('notification-signup')
|
|
1224
|
+
const successEl = document.getElementById('notification-success')
|
|
1225
|
+
if (signupEl) signupEl.style.display = 'none'
|
|
1226
|
+
if (successEl) successEl.style.display = 'block'
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
alert(result.message)
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
console.error('Subscription error:', error)
|
|
1234
|
+
alert('Failed to subscribe. Please try again later.')
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Blog newsletter signup
|
|
1239
|
+
async function handleBlogNewsletterSignup() {
|
|
1240
|
+
const emailInput = document.getElementById('blog-newsletter-email') as HTMLInputElement
|
|
1241
|
+
const email = emailInput?.value.trim()
|
|
1242
|
+
|
|
1243
|
+
if (!email || !email.includes('@')) {
|
|
1244
|
+
alert('Please enter a valid email address')
|
|
1245
|
+
return
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
try {
|
|
1249
|
+
const result = await subscribeEmail(email)
|
|
1250
|
+
|
|
1251
|
+
if (result.success) {
|
|
1252
|
+
const formEl = document.getElementById('blog-newsletter-form')
|
|
1253
|
+
const successEl = document.getElementById('blog-newsletter-success')
|
|
1254
|
+
if (formEl) formEl.style.display = 'none'
|
|
1255
|
+
if (successEl) successEl.style.display = 'block'
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
alert(result.message)
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
console.error('Subscription error:', error)
|
|
1263
|
+
alert('Failed to subscribe. Please try again later.')
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Context menu
|
|
1268
|
+
const contextMenu = document.getElementById('context-menu')
|
|
1269
|
+
const iconContextMenu = document.getElementById('icon-context-menu')
|
|
1270
|
+
let contextMenuIcon: HTMLElement | null = null
|
|
1271
|
+
|
|
1272
|
+
document.addEventListener('contextmenu', (e) => {
|
|
1273
|
+
// Check if right-click is on a taskbar button
|
|
1274
|
+
const target = e.target as HTMLElement
|
|
1275
|
+
const isTaskbarButton = target.closest('.taskbar-task')
|
|
1276
|
+
|
|
1277
|
+
// If it's a taskbar button, let its own handler deal with it
|
|
1278
|
+
if (isTaskbarButton) {
|
|
1279
|
+
return
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
e.preventDefault()
|
|
1283
|
+
|
|
1284
|
+
// Check if right-click is on an icon
|
|
1285
|
+
const icon = target.closest('.desktop-icon') as HTMLElement
|
|
1286
|
+
|
|
1287
|
+
// Hide both menus first
|
|
1288
|
+
contextMenu.style.display = 'none'
|
|
1289
|
+
iconContextMenu.style.display = 'none'
|
|
1290
|
+
|
|
1291
|
+
// Also hide taskbar context menu
|
|
1292
|
+
if (taskbarContextMenu) {
|
|
1293
|
+
taskbarContextMenu.remove()
|
|
1294
|
+
taskbarContextMenu = null
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (icon) {
|
|
1298
|
+
// Select the icon first if it's not already selected
|
|
1299
|
+
if (selectedIcon && selectedIcon !== icon) {
|
|
1300
|
+
selectedIcon.classList.remove('selected')
|
|
1301
|
+
}
|
|
1302
|
+
icon.classList.add('selected')
|
|
1303
|
+
selectedIcon = icon
|
|
1304
|
+
|
|
1305
|
+
// Show icon context menu
|
|
1306
|
+
contextMenuIcon = icon
|
|
1307
|
+
|
|
1308
|
+
// Check if icon is on the right side of the screen
|
|
1309
|
+
const screenWidth = window.innerWidth
|
|
1310
|
+
const menuWidth = 200 // Approximate context menu width
|
|
1311
|
+
const isRightSide = e.clientX > screenWidth / 2
|
|
1312
|
+
|
|
1313
|
+
// Position menu to the left if icon is on right side
|
|
1314
|
+
const menuLeft = isRightSide ? e.clientX - menuWidth : e.clientX
|
|
1315
|
+
|
|
1316
|
+
iconContextMenu.style.left = `${menuLeft}px`
|
|
1317
|
+
iconContextMenu.style.top = `${e.clientY}px`
|
|
1318
|
+
iconContextMenu.style.display = 'flex'
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
// Show desktop context menu
|
|
1322
|
+
contextMenuIcon = null
|
|
1323
|
+
|
|
1324
|
+
// Check if click is on the right side of the screen
|
|
1325
|
+
const screenWidth = window.innerWidth
|
|
1326
|
+
const menuWidth = 200 // Approximate context menu width
|
|
1327
|
+
const isRightSide = e.clientX > screenWidth / 2
|
|
1328
|
+
|
|
1329
|
+
// Position menu to the left if on right side
|
|
1330
|
+
const menuLeft = isRightSide ? e.clientX - menuWidth : e.clientX
|
|
1331
|
+
|
|
1332
|
+
contextMenu.style.left = `${menuLeft}px`
|
|
1333
|
+
contextMenu.style.top = `${e.clientY}px`
|
|
1334
|
+
contextMenu.style.display = 'flex'
|
|
1335
|
+
}
|
|
1336
|
+
})
|
|
1337
|
+
|
|
1338
|
+
document.addEventListener('click', () => {
|
|
1339
|
+
contextMenu.style.display = 'none'
|
|
1340
|
+
iconContextMenu.style.display = 'none'
|
|
1341
|
+
})
|
|
1342
|
+
|
|
1343
|
+
// Share functions
|
|
1344
|
+
function shareVia(platform) {
|
|
1345
|
+
const url = 'https://stacksjs.com'
|
|
1346
|
+
const text = 'Check out Stacks - A TypeScript framework for modern development'
|
|
1347
|
+
|
|
1348
|
+
switch (platform) {
|
|
1349
|
+
case 'twitter':
|
|
1350
|
+
window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`, '_blank')
|
|
1351
|
+
break
|
|
1352
|
+
case 'bluesky':
|
|
1353
|
+
window.open(`https://bsky.app/intent/compose?text=${encodeURIComponent(`${text} ${url}`)}`, '_blank')
|
|
1354
|
+
break
|
|
1355
|
+
case 'linkedin':
|
|
1356
|
+
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`, '_blank')
|
|
1357
|
+
break
|
|
1358
|
+
case 'email':
|
|
1359
|
+
window.location.href = `mailto:?subject=${encodeURIComponent(text)}&body=${encodeURIComponent(url)}`
|
|
1360
|
+
break
|
|
1361
|
+
case 'copy':
|
|
1362
|
+
navigator.clipboard.writeText(url).then(() => {
|
|
1363
|
+
alert('Link copied to clipboard!')
|
|
1364
|
+
})
|
|
1365
|
+
break
|
|
1366
|
+
}
|
|
1367
|
+
contextMenu.style.display = 'none'
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function copyLogoSVG() {
|
|
1371
|
+
const logoSVG = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
1372
|
+
<rect width="100" height="100" fill="#0831d9"/>
|
|
1373
|
+
<text x="50" y="65" font-family="Arial" font-size="40" font-weight="bold" fill="white" text-anchor="middle">S</text>
|
|
1374
|
+
</svg>`
|
|
1375
|
+
|
|
1376
|
+
navigator.clipboard.writeText(logoSVG).then(() => {
|
|
1377
|
+
alert('Logo SVG copied to clipboard!')
|
|
1378
|
+
}).catch((err) => {
|
|
1379
|
+
console.error('Failed to copy:', err)
|
|
1380
|
+
})
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function copyWordmarkSVG() {
|
|
1384
|
+
const wordmarkSVG = `<svg width="200" height="50" viewBox="0 0 200 50" xmlns="http://www.w3.org/2000/svg">
|
|
1385
|
+
<text x="10" y="35" font-family="Arial" font-size="30" font-weight="bold" fill="#0831d9">STACKS</text>
|
|
1386
|
+
</svg>`
|
|
1387
|
+
|
|
1388
|
+
navigator.clipboard.writeText(wordmarkSVG).then(() => {
|
|
1389
|
+
alert('Wordmark SVG copied to clipboard!')
|
|
1390
|
+
}).catch((err) => {
|
|
1391
|
+
console.error('Failed to copy:', err)
|
|
1392
|
+
})
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function changeBackgroundImage() {
|
|
1396
|
+
// Move to next background
|
|
1397
|
+
currentBackgroundIndex = (currentBackgroundIndex + 1) % BACKGROUND_IMAGES.length
|
|
1398
|
+
const newBackground = BACKGROUND_IMAGES[currentBackgroundIndex]
|
|
1399
|
+
|
|
1400
|
+
// Get the desktop element
|
|
1401
|
+
const desktop = document.querySelector('.desktop') as HTMLElement
|
|
1402
|
+
if (desktop) {
|
|
1403
|
+
desktop.style.backgroundImage = `url('${newBackground}')`
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Close context menu
|
|
1407
|
+
const contextMenu = document.getElementById('context-menu')
|
|
1408
|
+
if (contextMenu) {
|
|
1409
|
+
contextMenu.style.display = 'none'
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function _submitContactForm(event: Event) {
|
|
1414
|
+
event.preventDefault()
|
|
1415
|
+
|
|
1416
|
+
const nameInput = document.getElementById('contact-name') as HTMLInputElement
|
|
1417
|
+
const emailInput = document.getElementById('contact-email') as HTMLInputElement
|
|
1418
|
+
const subjectInput = document.getElementById('contact-subject') as HTMLInputElement
|
|
1419
|
+
const messageInput = document.getElementById('contact-message') as HTMLTextAreaElement
|
|
1420
|
+
|
|
1421
|
+
const name = nameInput.value
|
|
1422
|
+
const email = emailInput.value
|
|
1423
|
+
const subject = subjectInput.value
|
|
1424
|
+
const _message = messageInput.value
|
|
1425
|
+
|
|
1426
|
+
// Simulate form submission
|
|
1427
|
+
alert(`✅ Thank you, ${name}!\n\nYour message has been received. We'll get back to you at ${email} soon.\n\nSubject: ${subject}\n\nThis is a demo - in production, this would send your message to our team.`)
|
|
1428
|
+
|
|
1429
|
+
// Clear form
|
|
1430
|
+
nameInput.value = ''
|
|
1431
|
+
emailInput.value = ''
|
|
1432
|
+
subjectInput.value = ''
|
|
1433
|
+
messageInput.value = ''
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Icon context menu functions
|
|
1437
|
+
function openIconFromMenu() {
|
|
1438
|
+
if (!contextMenuIcon)
|
|
1439
|
+
return
|
|
1440
|
+
|
|
1441
|
+
const iconType = contextMenuIcon.dataset.iconType
|
|
1442
|
+
const iconId = contextMenuIcon.dataset.iconId
|
|
1443
|
+
const iconUrl = contextMenuIcon.dataset.iconUrl
|
|
1444
|
+
|
|
1445
|
+
if (iconType === 'window') {
|
|
1446
|
+
openWindow(iconId)
|
|
1447
|
+
}
|
|
1448
|
+
else if (iconType === 'link' && iconUrl) {
|
|
1449
|
+
window.open(iconUrl, '_blank')
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function exitEditMode(save: boolean = true) {
|
|
1454
|
+
if (!activeEditInput || !activeEditLabel)
|
|
1455
|
+
return
|
|
1456
|
+
|
|
1457
|
+
if (save) {
|
|
1458
|
+
const newName = activeEditInput.value.trim()
|
|
1459
|
+
if (newName) {
|
|
1460
|
+
activeEditLabel.textContent = newName
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
activeEditLabel.textContent = activeEditOriginalName
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
else {
|
|
1467
|
+
activeEditLabel.textContent = activeEditOriginalName
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
activeEditInput = null
|
|
1471
|
+
activeEditLabel = null
|
|
1472
|
+
activeEditOriginalName = ''
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function enterEditMode(icon: HTMLElement) {
|
|
1476
|
+
// Exit any existing edit mode first
|
|
1477
|
+
exitEditMode()
|
|
1478
|
+
|
|
1479
|
+
const labelElement = icon.querySelector('.desktop-icon-label') as HTMLElement
|
|
1480
|
+
if (!labelElement)
|
|
1481
|
+
return
|
|
1482
|
+
|
|
1483
|
+
const currentName = labelElement.textContent || ''
|
|
1484
|
+
|
|
1485
|
+
// Store edit state
|
|
1486
|
+
activeEditLabel = labelElement
|
|
1487
|
+
activeEditOriginalName = currentName
|
|
1488
|
+
|
|
1489
|
+
// Create an input element
|
|
1490
|
+
const input = document.createElement('input')
|
|
1491
|
+
input.type = 'text'
|
|
1492
|
+
input.value = currentName
|
|
1493
|
+
input.style.cssText = `
|
|
1494
|
+
width: 100%;
|
|
1495
|
+
background: white;
|
|
1496
|
+
border: 1px solid #0066cc;
|
|
1497
|
+
padding: 2px 4px;
|
|
1498
|
+
font-family: 'Segoe UI', Tahoma, sans-serif;
|
|
1499
|
+
font-size: 11px;
|
|
1500
|
+
text-align: center;
|
|
1501
|
+
outline: none;
|
|
1502
|
+
`
|
|
1503
|
+
|
|
1504
|
+
activeEditInput = input
|
|
1505
|
+
|
|
1506
|
+
// Replace label with input
|
|
1507
|
+
labelElement.textContent = ''
|
|
1508
|
+
labelElement.appendChild(input)
|
|
1509
|
+
input.focus()
|
|
1510
|
+
input.select()
|
|
1511
|
+
|
|
1512
|
+
// Handle finish editing
|
|
1513
|
+
const finishEdit = () => {
|
|
1514
|
+
exitEditMode(true)
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
input.addEventListener('blur', finishEdit)
|
|
1518
|
+
input.addEventListener('keydown', (e) => {
|
|
1519
|
+
if (e.key === 'Enter') {
|
|
1520
|
+
exitEditMode(true)
|
|
1521
|
+
}
|
|
1522
|
+
else if (e.key === 'Escape') {
|
|
1523
|
+
exitEditMode(false)
|
|
1524
|
+
}
|
|
1525
|
+
})
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function renameIcon() {
|
|
1529
|
+
if (!contextMenuIcon)
|
|
1530
|
+
return
|
|
1531
|
+
|
|
1532
|
+
const labelElement = contextMenuIcon.querySelector('.desktop-icon-label') as HTMLElement
|
|
1533
|
+
const currentName = labelElement.textContent || ''
|
|
1534
|
+
|
|
1535
|
+
const newName = prompt('Enter new name:', currentName)
|
|
1536
|
+
if (newName && newName.trim()) {
|
|
1537
|
+
labelElement.textContent = newName.trim()
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function deleteIcon() {
|
|
1542
|
+
if (!contextMenuIcon)
|
|
1543
|
+
return
|
|
1544
|
+
|
|
1545
|
+
const _iconId = contextMenuIcon.dataset.iconId
|
|
1546
|
+
const confirmed = confirm(`Are you sure you want to delete this icon?`)
|
|
1547
|
+
|
|
1548
|
+
if (confirmed) {
|
|
1549
|
+
contextMenuIcon.remove()
|
|
1550
|
+
contextMenuIcon = null
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// License window functions
|
|
1555
|
+
function closeLicenseWindow() {
|
|
1556
|
+
closeWindow('license')
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Payment form formatting and validation
|
|
1560
|
+
function formatCardNumber(input: HTMLInputElement) {
|
|
1561
|
+
const value = input.value.replace(/\s/g, '').replace(/\D/g, '')
|
|
1562
|
+
const formattedValue = value.match(/.{1,4}/g)?.join(' ') || value
|
|
1563
|
+
input.value = formattedValue
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function formatExpiry(input: HTMLInputElement) {
|
|
1567
|
+
const value = input.value.replace(/\D/g, '')
|
|
1568
|
+
|
|
1569
|
+
if (value.length >= 2) {
|
|
1570
|
+
let month = value.substring(0, 2)
|
|
1571
|
+
const year = value.substring(2, 4)
|
|
1572
|
+
|
|
1573
|
+
// Validate month
|
|
1574
|
+
const monthNum = Number.parseInt(month)
|
|
1575
|
+
if (monthNum > 12) {
|
|
1576
|
+
month = '12'
|
|
1577
|
+
}
|
|
1578
|
+
if (monthNum < 1 && month.length === 2) {
|
|
1579
|
+
month = '01'
|
|
1580
|
+
}
|
|
1581
|
+
if (monthNum === 0 && month.length === 2) {
|
|
1582
|
+
month = '01'
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
input.value = month + (year ? `/${year}` : '')
|
|
1586
|
+
}
|
|
1587
|
+
else {
|
|
1588
|
+
input.value = value
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function formatCVV(input: HTMLInputElement) {
|
|
1593
|
+
input.value = input.value.replace(/\D/g, '')
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function togglePaymentSection() {
|
|
1597
|
+
const paymentForm = document.getElementById('payment-form')
|
|
1598
|
+
if (paymentForm) {
|
|
1599
|
+
paymentForm.style.display = paymentForm.style.display === 'none' ? 'block' : 'none'
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function validatePaymentForm(): boolean {
|
|
1604
|
+
const cardNumber = (document.getElementById('card-number') as HTMLInputElement)?.value.replace(/\s/g, '')
|
|
1605
|
+
const cardExpiry = (document.getElementById('card-expiry') as HTMLInputElement)?.value
|
|
1606
|
+
const cardCVV = (document.getElementById('card-cvv') as HTMLInputElement)?.value
|
|
1607
|
+
const cardName = (document.getElementById('card-name') as HTMLInputElement)?.value
|
|
1608
|
+
const billingEmail = (document.getElementById('billing-email') as HTMLInputElement)?.value
|
|
1609
|
+
const termsAgree = (document.getElementById('terms-agree') as HTMLInputElement)?.checked
|
|
1610
|
+
|
|
1611
|
+
if (!cardNumber || cardNumber.length < 13 || cardNumber.length > 19) {
|
|
1612
|
+
alert('❌ Please enter a valid card number.')
|
|
1613
|
+
return false
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
if (!cardExpiry || !/^\d{2}\/\d{2}$/.test(cardExpiry)) {
|
|
1617
|
+
alert('❌ Please enter a valid expiration date (MM/YY).')
|
|
1618
|
+
return false
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Validate expiry date is in the future
|
|
1622
|
+
const [month, year] = cardExpiry.split('/').map(num => Number.parseInt(num))
|
|
1623
|
+
const currentDate = new Date()
|
|
1624
|
+
const _currentYear = currentDate.getFullYear() % 100 // Last 2 digits of current year
|
|
1625
|
+
const currentMonth = currentDate.getMonth() + 1
|
|
1626
|
+
const fullCurrentYear = currentDate.getFullYear()
|
|
1627
|
+
|
|
1628
|
+
// Check if month is valid
|
|
1629
|
+
if (month < 1 || month > 12) {
|
|
1630
|
+
alert('❌ Invalid month. Please enter a month between 01 and 12.')
|
|
1631
|
+
return false
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// Determine if the year is meant to be 20XX or 19XX
|
|
1635
|
+
// Assume years 00-49 are 2000-2049, and 50-99 are 1950-1999
|
|
1636
|
+
let fullYear = year
|
|
1637
|
+
if (year >= 0 && year <= 49) {
|
|
1638
|
+
fullYear = 2000 + year
|
|
1639
|
+
}
|
|
1640
|
+
else if (year >= 50 && year <= 99) {
|
|
1641
|
+
fullYear = 1900 + year
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Check if card is expired
|
|
1645
|
+
if (fullYear < fullCurrentYear) {
|
|
1646
|
+
alert(`❌ Card expired in ${month.toString().padStart(2, '0')}/${year.toString().padStart(2, '0')}. Please use a valid card.`)
|
|
1647
|
+
return false
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
if (fullYear === fullCurrentYear && month < currentMonth) {
|
|
1651
|
+
alert(`❌ Card expired in ${month.toString().padStart(2, '0')}/${year.toString().padStart(2, '0')}. Please use a valid card.`)
|
|
1652
|
+
return false
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// Warn about cards expiring too far in the future (likely a typo)
|
|
1656
|
+
if (fullYear > fullCurrentYear + 20) {
|
|
1657
|
+
alert(`❌ Expiration date seems invalid (${month.toString().padStart(2, '0')}/${year.toString().padStart(2, '0')}). Please check and try again.`)
|
|
1658
|
+
return false
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
if (!cardCVV || cardCVV.length < 3 || cardCVV.length > 4) {
|
|
1662
|
+
alert('❌ Please enter a valid CVV code (3-4 digits).')
|
|
1663
|
+
return false
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (!cardName || cardName.trim().length < 3) {
|
|
1667
|
+
alert('❌ Please enter the cardholder name.')
|
|
1668
|
+
return false
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
if (!billingEmail || !/^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/.test(billingEmail)) {
|
|
1672
|
+
alert('❌ Please enter a valid email address.')
|
|
1673
|
+
return false
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (!termsAgree) {
|
|
1677
|
+
alert('❌ Please agree to the terms and conditions to continue.')
|
|
1678
|
+
return false
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
return true
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function purchaseLicense() {
|
|
1685
|
+
// Check if payment form is visible
|
|
1686
|
+
const paymentForm = document.getElementById('payment-form')
|
|
1687
|
+
const isPaymentVisible = paymentForm && paymentForm.style.display !== 'none'
|
|
1688
|
+
|
|
1689
|
+
if (isPaymentVisible) {
|
|
1690
|
+
// Validate payment form
|
|
1691
|
+
if (!validatePaymentForm()) {
|
|
1692
|
+
return
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// Get selected edition (hobby or professional)
|
|
1696
|
+
const selectedEdition = (window as any).selectedPlanType || 'professional'
|
|
1697
|
+
const editionName = selectedEdition === 'hobby' ? 'Hobby Edition' : 'Professional Edition'
|
|
1698
|
+
|
|
1699
|
+
// Get selected pricing plan (monthly or lifetime)
|
|
1700
|
+
const selectedInterval = (document.querySelector('input[name="pricing-plan"]:checked') as HTMLInputElement)?.value
|
|
1701
|
+
const intervalType = selectedInterval === 'lifetime' ? 'Lifetime License' : 'Monthly Subscription'
|
|
1702
|
+
|
|
1703
|
+
// Determine pricing based on edition and interval
|
|
1704
|
+
let planAmount: string
|
|
1705
|
+
if (selectedEdition === 'hobby') {
|
|
1706
|
+
planAmount = selectedInterval === 'lifetime' ? '$179' : '$9/month'
|
|
1707
|
+
}
|
|
1708
|
+
else {
|
|
1709
|
+
planAmount = selectedInterval === 'lifetime' ? '$279' : '$29/month'
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Get payment details
|
|
1713
|
+
const cardNumber = (document.getElementById('card-number') as HTMLInputElement)?.value
|
|
1714
|
+
const cardName = (document.getElementById('card-name') as HTMLInputElement)?.value
|
|
1715
|
+
const billingEmail = (document.getElementById('billing-email') as HTMLInputElement)?.value
|
|
1716
|
+
|
|
1717
|
+
// Show processing message
|
|
1718
|
+
const maskedCard = `****${cardNumber.replace(/\s/g, '').slice(-4)}`
|
|
1719
|
+
alert(`✅ Processing payment with Stripe...\n\nEdition: ${editionName}\nPlan: ${intervalType}\nAmount: ${planAmount}\nCard: ${maskedCard}\nName: ${cardName}\nEmail: ${billingEmail}\n\nThis is a demo. In production, your payment data would be securely processed by Stripe without ever touching our servers.`)
|
|
1720
|
+
|
|
1721
|
+
// Clear form and show success
|
|
1722
|
+
setTimeout(() => {
|
|
1723
|
+
const licenseCode = selectedInterval === 'lifetime'
|
|
1724
|
+
? `STAC-KSJS-${selectedEdition.toUpperCase().substring(0, 4)}-LIFE-2025`
|
|
1725
|
+
: `STAC-KSJS-${selectedEdition.toUpperCase().substring(0, 4)}-MNTH-2025`
|
|
1726
|
+
|
|
1727
|
+
const successMessage = selectedInterval === 'lifetime'
|
|
1728
|
+
? `✅ Payment successful!\n\nYou now have LIFETIME access to STACKS.JS ${editionName}!\n\nYour license code has been sent to your email.\n\nLicense: ${licenseCode}\nType: Lifetime - ${editionName}\nStatus: Activated`
|
|
1729
|
+
: `✅ Payment successful!\n\nYour monthly subscription is now active!\n\nYou will be billed ${planAmount}. You can cancel anytime.\n\nLicense: ${licenseCode}\nType: Monthly - ${editionName}\nStatus: Active`
|
|
1730
|
+
|
|
1731
|
+
alert(successMessage);
|
|
1732
|
+
|
|
1733
|
+
// Clear payment form
|
|
1734
|
+
(document.getElementById('card-number') as HTMLInputElement).value = '';
|
|
1735
|
+
(document.getElementById('card-expiry') as HTMLInputElement).value = '';
|
|
1736
|
+
(document.getElementById('card-cvv') as HTMLInputElement).value = '';
|
|
1737
|
+
(document.getElementById('card-name') as HTMLInputElement).value = '';
|
|
1738
|
+
(document.getElementById('billing-email') as HTMLInputElement).value = '';
|
|
1739
|
+
(document.getElementById('terms-agree') as HTMLInputElement).checked = false
|
|
1740
|
+
|
|
1741
|
+
closeWindow('license')
|
|
1742
|
+
}, 1000)
|
|
1743
|
+
}
|
|
1744
|
+
else {
|
|
1745
|
+
// Fall back to sponsorship page
|
|
1746
|
+
const confirmed = confirm('This will open the Stacks.js sponsorship page in a new window. Continue?')
|
|
1747
|
+
if (confirmed) {
|
|
1748
|
+
window.open('https://github.com/sponsors/chrisbbreuer', '_blank')
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function activateLicense() {
|
|
1754
|
+
const input = document.getElementById('license-code-input') as HTMLInputElement
|
|
1755
|
+
const code = input.value.trim().toUpperCase()
|
|
1756
|
+
|
|
1757
|
+
if (!code) {
|
|
1758
|
+
alert('Please enter a license code.')
|
|
1759
|
+
return
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// Format the code with dashes if not already formatted
|
|
1763
|
+
const formattedCode = code.replace(/-/g, '').match(/.{1,4}/g)?.join('-') || code
|
|
1764
|
+
|
|
1765
|
+
// Check for special codes
|
|
1766
|
+
if (formattedCode === 'OPEN-SOUR-CE4E-VER' || formattedCode === 'STAC-KSJS-FORE-VER') {
|
|
1767
|
+
alert(`✅ License activated successfully!\n\nThank you for using STACKS.JS Professional Edition.\n\nLicense: ${formattedCode}\nType: Professional Edition\nStatus: Activated`)
|
|
1768
|
+
input.value = ''
|
|
1769
|
+
closeWindow('license')
|
|
1770
|
+
return
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Check for trial code
|
|
1774
|
+
if (formattedCode === 'TRIAL-2025-STAC-KSJS') {
|
|
1775
|
+
alert(`✅ Trial license activated!\n\nYou have 30 days to evaluate STACKS.JS Professional Edition.\n\nLicense: ${formattedCode}\nType: 30-Day Trial\nStatus: Activated`)
|
|
1776
|
+
input.value = ''
|
|
1777
|
+
closeWindow('license')
|
|
1778
|
+
return
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// Invalid code
|
|
1782
|
+
alert('❌ Invalid license code.\n\nPlease check your code and try again, or purchase a new license.\n\nTip: Try "OPEN-SOUR-CE4E-VER" or "TRIAL-2025-STAC-KSJS"')
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// Ecosystem tab switching
|
|
1786
|
+
function showEcosystemTab(tabName) {
|
|
1787
|
+
// Hide all tabs
|
|
1788
|
+
const tabs = document.querySelectorAll('.ecosystem-tab')
|
|
1789
|
+
tabs.forEach((tab) => {
|
|
1790
|
+
(tab as HTMLElement).style.display = 'none'
|
|
1791
|
+
})
|
|
1792
|
+
|
|
1793
|
+
// Remove active class from all buttons
|
|
1794
|
+
const buttons = document.querySelectorAll('#window-ecosystem .feature-btn')
|
|
1795
|
+
buttons.forEach(btn => btn.classList.remove('active'))
|
|
1796
|
+
|
|
1797
|
+
// Show selected tab
|
|
1798
|
+
const selectedTab = document.getElementById(`tab-${tabName}`)
|
|
1799
|
+
if (selectedTab) {
|
|
1800
|
+
selectedTab.style.display = 'block'
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// Add active class to clicked button
|
|
1804
|
+
event.target.classList.add('active')
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// Print Welcome PDF
|
|
1808
|
+
function printWelcome() {
|
|
1809
|
+
const welcomeContent = document.querySelector('#window-welcome .window-content')
|
|
1810
|
+
if (!welcomeContent)
|
|
1811
|
+
return
|
|
1812
|
+
|
|
1813
|
+
// Create a new window for printing
|
|
1814
|
+
const printWindow = window.open('', '', 'width=800,height=600')
|
|
1815
|
+
if (!printWindow)
|
|
1816
|
+
return
|
|
1817
|
+
|
|
1818
|
+
printWindow.document.write(`
|
|
1819
|
+
<!DOCTYPE html>
|
|
1820
|
+
<html>
|
|
1821
|
+
<head>
|
|
1822
|
+
<title>STACKS.JS - Welcome</title>
|
|
1823
|
+
<style>
|
|
1824
|
+
body { font-family: 'Tahoma', sans-serif; margin: 0; padding: 20px; }
|
|
1825
|
+
@media print {
|
|
1826
|
+
button { display: none; }
|
|
1827
|
+
}
|
|
1828
|
+
</style>
|
|
1829
|
+
</head>
|
|
1830
|
+
<body>
|
|
1831
|
+
${welcomeContent.innerHTML}
|
|
1832
|
+
</body>
|
|
1833
|
+
</html>
|
|
1834
|
+
`)
|
|
1835
|
+
printWindow.document.close()
|
|
1836
|
+
printWindow.focus()
|
|
1837
|
+
setTimeout(() => {
|
|
1838
|
+
printWindow.print()
|
|
1839
|
+
printWindow.close()
|
|
1840
|
+
}, 250)
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// Download Welcome PDF
|
|
1844
|
+
function downloadWelcome() {
|
|
1845
|
+
const welcomeContent = document.querySelector('#window-welcome .window-content')
|
|
1846
|
+
if (!welcomeContent)
|
|
1847
|
+
return
|
|
1848
|
+
|
|
1849
|
+
// Create HTML content
|
|
1850
|
+
const htmlContent = `
|
|
1851
|
+
<!DOCTYPE html>
|
|
1852
|
+
<html>
|
|
1853
|
+
<head>
|
|
1854
|
+
<title>STACKS.JS - Welcome</title>
|
|
1855
|
+
<meta charset="UTF-8">
|
|
1856
|
+
<style>
|
|
1857
|
+
body { font-family: 'Tahoma', sans-serif; margin: 0; padding: 20px; }
|
|
1858
|
+
</style>
|
|
1859
|
+
</head>
|
|
1860
|
+
<body>
|
|
1861
|
+
${welcomeContent.innerHTML}
|
|
1862
|
+
</body>
|
|
1863
|
+
</html>
|
|
1864
|
+
`
|
|
1865
|
+
|
|
1866
|
+
// Create blob and download
|
|
1867
|
+
const blob = new Blob([htmlContent], { type: 'text/html' })
|
|
1868
|
+
const url = URL.createObjectURL(blob)
|
|
1869
|
+
const a = document.createElement('a')
|
|
1870
|
+
a.href = url
|
|
1871
|
+
a.download = 'STACKS.JS-Welcome.html'
|
|
1872
|
+
document.body.appendChild(a)
|
|
1873
|
+
a.click()
|
|
1874
|
+
document.body.removeChild(a)
|
|
1875
|
+
URL.revokeObjectURL(url)
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// Plan selection
|
|
1879
|
+
function selectPlan(planType: string) {
|
|
1880
|
+
// Minimize the Welcome window
|
|
1881
|
+
minimizeWindow('welcome')
|
|
1882
|
+
|
|
1883
|
+
// Update License window based on selected plan
|
|
1884
|
+
const licenseTitle = document.querySelector('#window-license h2')
|
|
1885
|
+
const licenseTypeSpan = document.querySelectorAll('#window-license .license-type')
|
|
1886
|
+
const licensePriceSpan = document.querySelector('#window-license .license-price')
|
|
1887
|
+
|
|
1888
|
+
// Update pricing in radio buttons
|
|
1889
|
+
const lifetimeRadioLabel = document.querySelector('input[value="lifetime"]')?.parentElement
|
|
1890
|
+
const monthlyRadioLabel = document.querySelector('input[value="monthly"]')?.parentElement
|
|
1891
|
+
|
|
1892
|
+
// Update features list
|
|
1893
|
+
const featuresSpan = document.querySelector('#window-license .license-features')
|
|
1894
|
+
|
|
1895
|
+
if (planType === 'hobby') {
|
|
1896
|
+
if (licenseTitle)
|
|
1897
|
+
licenseTitle.textContent = 'STACKS.JS Hobby Edition'
|
|
1898
|
+
licenseTypeSpan.forEach(el => el.textContent = 'Hobby Edition')
|
|
1899
|
+
if (licensePriceSpan)
|
|
1900
|
+
licensePriceSpan.textContent = '$9/mo or $179 lifetime'
|
|
1901
|
+
if (featuresSpan)
|
|
1902
|
+
featuresSpan.innerHTML = 'Unlimited Projects<br>Priority Support<br>Lifetime Updates'
|
|
1903
|
+
|
|
1904
|
+
// Update radio button pricing for Hobby
|
|
1905
|
+
if (lifetimeRadioLabel) {
|
|
1906
|
+
const priceDiv = lifetimeRadioLabel.querySelector('div > div:first-child') as HTMLElement
|
|
1907
|
+
if (priceDiv)
|
|
1908
|
+
priceDiv.innerHTML = '🎉 Lifetime Access - $179'
|
|
1909
|
+
}
|
|
1910
|
+
if (monthlyRadioLabel) {
|
|
1911
|
+
const priceDiv = monthlyRadioLabel.querySelector('div > div:first-child') as HTMLElement
|
|
1912
|
+
if (priceDiv)
|
|
1913
|
+
priceDiv.innerHTML = 'Monthly Subscription - $9/mo'
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
else {
|
|
1917
|
+
if (licenseTitle)
|
|
1918
|
+
licenseTitle.textContent = 'STACKS.JS Professional Edition'
|
|
1919
|
+
licenseTypeSpan.forEach(el => el.textContent = 'Professional Edition')
|
|
1920
|
+
if (licensePriceSpan)
|
|
1921
|
+
licensePriceSpan.textContent = '$29/mo or $279 lifetime'
|
|
1922
|
+
if (featuresSpan)
|
|
1923
|
+
featuresSpan.innerHTML = 'Unlimited Projects<br>Unlimited Team Members<br>Priority Support<br>Lifetime Updates'
|
|
1924
|
+
|
|
1925
|
+
// Update radio button pricing for Professional
|
|
1926
|
+
if (lifetimeRadioLabel) {
|
|
1927
|
+
const priceDiv = lifetimeRadioLabel.querySelector('div > div:first-child') as HTMLElement
|
|
1928
|
+
if (priceDiv)
|
|
1929
|
+
priceDiv.innerHTML = '🎉 Lifetime Access - $279'
|
|
1930
|
+
}
|
|
1931
|
+
if (monthlyRadioLabel) {
|
|
1932
|
+
const priceDiv = monthlyRadioLabel.querySelector('div > div:first-child') as HTMLElement
|
|
1933
|
+
if (priceDiv)
|
|
1934
|
+
priceDiv.innerHTML = 'Monthly Subscription - $29/mo'
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// Store the selected plan type
|
|
1939
|
+
(window as any).selectedPlanType = planType
|
|
1940
|
+
|
|
1941
|
+
// Wait for Welcome window to finish minimizing, then open License from the taskbar
|
|
1942
|
+
setTimeout(() => {
|
|
1943
|
+
// The License window will get a taskbar button, so we need to open it first
|
|
1944
|
+
// to create the button, then animate it
|
|
1945
|
+
const licenseWindow = document.getElementById('window-license')
|
|
1946
|
+
if (!licenseWindow)
|
|
1947
|
+
return
|
|
1948
|
+
|
|
1949
|
+
// Temporarily mark as minimized to create taskbar button
|
|
1950
|
+
licenseWindow.classList.add('minimized')
|
|
1951
|
+
updateTaskbar()
|
|
1952
|
+
|
|
1953
|
+
// Find the newly created License taskbar button
|
|
1954
|
+
setTimeout(() => {
|
|
1955
|
+
const taskbarButtons = document.querySelectorAll('.taskbar-task')
|
|
1956
|
+
let licenseButton = null
|
|
1957
|
+
taskbarButtons.forEach((btn) => {
|
|
1958
|
+
const btnText = btn.querySelector('.taskbar-task-label')?.textContent
|
|
1959
|
+
if (btnText && btnText.includes('License')) {
|
|
1960
|
+
licenseButton = btn
|
|
1961
|
+
}
|
|
1962
|
+
})
|
|
1963
|
+
|
|
1964
|
+
// Now remove minimized and open with animation from the button
|
|
1965
|
+
licenseWindow.classList.remove('minimized')
|
|
1966
|
+
if (licenseButton) {
|
|
1967
|
+
openWindow('license', licenseButton)
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
openWindow('license')
|
|
1971
|
+
}
|
|
1972
|
+
}, 10)
|
|
1973
|
+
}, 250) // Wait for minimize animation to complete
|
|
1974
|
+
|
|
1975
|
+
// Show notification when a plan is selected
|
|
1976
|
+
showNotification()
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// Zoom functionality
|
|
1980
|
+
let currentZoom = 100
|
|
1981
|
+
|
|
1982
|
+
function setZoom(zoomLevel: number) {
|
|
1983
|
+
// Clamp zoom between 25 and 400
|
|
1984
|
+
zoomLevel = Math.max(25, Math.min(400, zoomLevel))
|
|
1985
|
+
currentZoom = zoomLevel
|
|
1986
|
+
|
|
1987
|
+
const content = document.getElementById('welcome-pdf-content')
|
|
1988
|
+
const zoomInput = document.getElementById('zoom-level') as HTMLInputElement
|
|
1989
|
+
|
|
1990
|
+
if (content) {
|
|
1991
|
+
const scale = zoomLevel / 100
|
|
1992
|
+
content.style.transform = `scale(${scale})`
|
|
1993
|
+
content.style.transformOrigin = 'top center'
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
if (zoomInput) {
|
|
1997
|
+
zoomInput.value = zoomLevel.toString()
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function increaseZoom() {
|
|
2002
|
+
setZoom(currentZoom + 25)
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
function decreaseZoom() {
|
|
2006
|
+
setZoom(currentZoom - 25)
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function setZoomFromInput() {
|
|
2010
|
+
const zoomInput = document.getElementById('zoom-level') as HTMLInputElement
|
|
2011
|
+
if (zoomInput) {
|
|
2012
|
+
const value = Number.parseInt(zoomInput.value)
|
|
2013
|
+
if (!Number.isNaN(value)) {
|
|
2014
|
+
setZoom(value)
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Taskbar context menu
|
|
2020
|
+
|
|
2021
|
+
function showTaskbarContextMenu(e: MouseEvent, windowId: string, windowEl: HTMLElement) {
|
|
2022
|
+
// Remove existing menu if any
|
|
2023
|
+
if (taskbarContextMenu) {
|
|
2024
|
+
taskbarContextMenu.remove()
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// Hide desktop context menus
|
|
2028
|
+
const contextMenu = document.getElementById('context-menu')
|
|
2029
|
+
const iconContextMenu = document.getElementById('icon-context-menu')
|
|
2030
|
+
if (contextMenu)
|
|
2031
|
+
contextMenu.style.display = 'none'
|
|
2032
|
+
if (iconContextMenu)
|
|
2033
|
+
iconContextMenu.style.display = 'none'
|
|
2034
|
+
|
|
2035
|
+
taskbarContextWindowId = windowId
|
|
2036
|
+
|
|
2037
|
+
// Create context menu
|
|
2038
|
+
taskbarContextMenu = document.createElement('div')
|
|
2039
|
+
taskbarContextMenu.className = 'taskbar-context-menu'
|
|
2040
|
+
taskbarContextMenu.style.position = 'fixed'
|
|
2041
|
+
taskbarContextMenu.style.left = `${e.clientX}px`
|
|
2042
|
+
|
|
2043
|
+
// Position above the taskbar
|
|
2044
|
+
const _menuHeight = 120 // approximate
|
|
2045
|
+
taskbarContextMenu.style.bottom = '45px' // Just above taskbar
|
|
2046
|
+
|
|
2047
|
+
const isMinimized = windowEl.classList.contains('minimized')
|
|
2048
|
+
const isMaximized = windowEl.classList.contains('maximized')
|
|
2049
|
+
|
|
2050
|
+
taskbarContextMenu.innerHTML = `
|
|
2051
|
+
<button class="context-menu-item" onclick="taskbarContextRestore()">
|
|
2052
|
+
<div class="context-menu-item-icon">🗗</div>
|
|
2053
|
+
<div>${isMinimized ? 'Restore' : 'Minimize'}</div>
|
|
2054
|
+
</button>
|
|
2055
|
+
<button class="context-menu-item" onclick="taskbarContextMaximize()" ${isMinimized ? 'disabled' : ''}>
|
|
2056
|
+
<div class="context-menu-item-icon">🗖</div>
|
|
2057
|
+
<div>${isMaximized ? 'Restore Down' : 'Maximize'}</div>
|
|
2058
|
+
</button>
|
|
2059
|
+
<div style="height: 1px; background: #ccc; margin: 4px 0;"></div>
|
|
2060
|
+
<button class="context-menu-item" onclick="taskbarContextClose()">
|
|
2061
|
+
<div class="context-menu-item-icon">✕</div>
|
|
2062
|
+
<div>Close</div>
|
|
2063
|
+
</button>
|
|
2064
|
+
`
|
|
2065
|
+
|
|
2066
|
+
document.body.appendChild(taskbarContextMenu)
|
|
2067
|
+
|
|
2068
|
+
// Close menu on click outside
|
|
2069
|
+
setTimeout(() => {
|
|
2070
|
+
document.addEventListener('click', closeTaskbarContextMenu)
|
|
2071
|
+
}, 0)
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
function closeTaskbarContextMenu() {
|
|
2075
|
+
if (taskbarContextMenu) {
|
|
2076
|
+
taskbarContextMenu.remove()
|
|
2077
|
+
taskbarContextMenu = null
|
|
2078
|
+
}
|
|
2079
|
+
document.removeEventListener('click', closeTaskbarContextMenu)
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function taskbarContextRestore() {
|
|
2083
|
+
if (!taskbarContextWindowId)
|
|
2084
|
+
return
|
|
2085
|
+
const windowEl = document.getElementById(`window-${taskbarContextWindowId}`)
|
|
2086
|
+
if (!windowEl)
|
|
2087
|
+
return
|
|
2088
|
+
|
|
2089
|
+
if (windowEl.classList.contains('minimized')) {
|
|
2090
|
+
// Click the taskbar button to restore
|
|
2091
|
+
const taskBtn = Array.from(document.querySelectorAll('.taskbar-task')).find((btn) => {
|
|
2092
|
+
const btnText = btn.querySelector('.taskbar-task-label')?.textContent
|
|
2093
|
+
const windowTitle = windowEl.querySelector('.window-title')?.textContent
|
|
2094
|
+
return btnText === windowTitle
|
|
2095
|
+
}) as HTMLElement
|
|
2096
|
+
if (taskBtn)
|
|
2097
|
+
taskBtn.click()
|
|
2098
|
+
}
|
|
2099
|
+
else {
|
|
2100
|
+
minimizeWindow(taskbarContextWindowId)
|
|
2101
|
+
}
|
|
2102
|
+
closeTaskbarContextMenu()
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function taskbarContextMaximize() {
|
|
2106
|
+
if (!taskbarContextWindowId)
|
|
2107
|
+
return
|
|
2108
|
+
toggleMaximizeWindow(taskbarContextWindowId)
|
|
2109
|
+
closeTaskbarContextMenu()
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
function taskbarContextClose() {
|
|
2113
|
+
if (!taskbarContextWindowId)
|
|
2114
|
+
return
|
|
2115
|
+
closeWindow(taskbarContextWindowId)
|
|
2116
|
+
closeTaskbarContextMenu()
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Make functions globally accessible for inline onclick handlers
|
|
2120
|
+
window.openWindow = openWindow
|
|
2121
|
+
window.closeWindow = closeWindow
|
|
2122
|
+
window.minimizeWindow = minimizeWindow
|
|
2123
|
+
window.toggleMaximizeWindow = toggleMaximizeWindow
|
|
2124
|
+
window.showFeature = showFeature
|
|
2125
|
+
window.closeNotification = closeNotification
|
|
2126
|
+
window.showNotification = showNotification
|
|
2127
|
+
window.handleEmailSignup = handleEmailSignup
|
|
2128
|
+
window.handleBlogNewsletterSignup = handleBlogNewsletterSignup
|
|
2129
|
+
window.shareVia = shareVia
|
|
2130
|
+
window.copyLogoSVG = copyLogoSVG
|
|
2131
|
+
window.copyWordmarkSVG = copyWordmarkSVG
|
|
2132
|
+
window.changeBackgroundImage = changeBackgroundImage
|
|
2133
|
+
window.openIconFromMenu = openIconFromMenu
|
|
2134
|
+
window.renameIcon = renameIcon
|
|
2135
|
+
window.deleteIcon = deleteIcon
|
|
2136
|
+
window.closeLicenseWindow = closeLicenseWindow
|
|
2137
|
+
window.purchaseLicense = purchaseLicense
|
|
2138
|
+
window.activateLicense = activateLicense
|
|
2139
|
+
window.formatCardNumber = formatCardNumber
|
|
2140
|
+
window.formatExpiry = formatExpiry
|
|
2141
|
+
window.formatCVV = formatCVV
|
|
2142
|
+
window.togglePaymentSection = togglePaymentSection
|
|
2143
|
+
window.showEcosystemTab = showEcosystemTab
|
|
2144
|
+
window.printWelcome = printWelcome
|
|
2145
|
+
window.downloadWelcome = downloadWelcome
|
|
2146
|
+
window.setZoom = setZoom
|
|
2147
|
+
window.increaseZoom = increaseZoom
|
|
2148
|
+
window.decreaseZoom = decreaseZoom
|
|
2149
|
+
window.setZoomFromInput = setZoomFromInput
|
|
2150
|
+
window.selectPlan = selectPlan
|
|
2151
|
+
window.taskbarContextRestore = taskbarContextRestore
|
|
2152
|
+
window.taskbarContextMaximize = taskbarContextMaximize
|
|
2153
|
+
window.taskbarContextClose = taskbarContextClose
|