Update favicon and enhance UI components for improved user experience
- Replaced PNG favicon with SVG for better scalability and visual quality across devices. - Updated Vite configuration to include the new SVG favicon and adjusted asset paths. - Enhanced various UI components with improved focus management and accessibility features. - Introduced new styles to hide scrollbars while maintaining scroll functionality for a cleaner interface.
This commit is contained in:
@@ -100,9 +100,22 @@ function onUserActivity() {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const isMac = navigator.platform.toUpperCase().includes('MAC')
|
||||
const mod = isMac ? e.metaKey : e.ctrlKey
|
||||
if (mod && e.key === 'k') {
|
||||
// Cmd+K / Ctrl+K or plain K (when not typing in input)
|
||||
const target = e.target as HTMLElement
|
||||
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
|
||||
if ((mod && e.key === 'k') || ((e.key === 'k' || e.key === 'K') && !isInput)) {
|
||||
e.preventDefault()
|
||||
spotlightStore.toggle()
|
||||
return
|
||||
}
|
||||
// 's' key activates screensaver when authenticated (skip if typing in input)
|
||||
if (e.key === 's' || e.key === 'S') {
|
||||
const target = e.target as HTMLElement
|
||||
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
|
||||
if (!isInput && appStore.isAuthenticated && !screensaverStore.isActive) {
|
||||
e.preventDefault()
|
||||
screensaverStore.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<div class="logo-gradient-border flex-shrink-0 inline-block overflow-hidden" :class="sizeClass">
|
||||
<div
|
||||
class="flex-shrink-0 inline-block overflow-hidden"
|
||||
:class="[
|
||||
sizeClass,
|
||||
!noBorder && 'logo-gradient-border'
|
||||
]"
|
||||
>
|
||||
<!-- Neode logo - always white -->
|
||||
<svg
|
||||
class="block logo-svg"
|
||||
class="block w-full h-full logo-svg"
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -27,9 +33,18 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
size?: 'sm' | 'lg' | 'xl'
|
||||
}>(), { size: 'sm' })
|
||||
noBorder?: boolean
|
||||
/** When true, fit to container (w-full h-full) instead of fixed size - for use inside logo-gradient-border */
|
||||
fit?: boolean
|
||||
}>(), { size: 'sm', noBorder: false, fit: false })
|
||||
|
||||
const sizeClass = props.size === 'xl' ? 'w-48 h-48 sm:w-64 sm:h-64 md:w-80 md:h-80' : props.size === 'lg' ? 'w-32 h-32 sm:w-48 sm:h-48' : 'w-14 h-14'
|
||||
const sizeClass = props.fit
|
||||
? 'w-full h-full max-w-full max-h-full'
|
||||
: props.size === 'xl'
|
||||
? 'w-48 h-48 sm:w-64 sm:h-64 md:w-80 md:h-80'
|
||||
: props.size === 'lg'
|
||||
? 'w-32 h-32 sm:w-48 sm:h-48'
|
||||
: 'w-14 h-14'
|
||||
|
||||
// Parsed from favico-black.svg path - 20 rects
|
||||
const rects = [
|
||||
@@ -56,7 +71,6 @@ const rects = [
|
||||
]
|
||||
|
||||
// Stagger delays (ms) - row-by-row top-to-bottom, left-to-right for a clean reveal
|
||||
// Row 1 (y~318): 0,1,2,3 | Row 2 (y~396): 4,5 | Row 3 (y~476): 6-11 | Row 4 (y~555): 12-15 | Row 5 (y~634): 16-19
|
||||
const delays = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -36,13 +36,15 @@
|
||||
<kbd class="hidden sm:inline-flex px-2 py-1 text-xs text-white/50 bg-white/10 rounded">Esc</kbd>
|
||||
</div>
|
||||
|
||||
<!-- Iframe container -->
|
||||
<div class="relative flex-1 min-h-0 bg-black/40">
|
||||
<!-- Iframe container - overflow hidden to clip inner scrollbars -->
|
||||
<div class="relative flex-1 min-h-0 bg-black/40 overflow-hidden">
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
v-if="store.url"
|
||||
:src="store.url"
|
||||
class="absolute inset-0 w-full h-full border-0"
|
||||
class="absolute inset-0 w-full h-full border-0 iframe-scrollbar-hide"
|
||||
title="App content"
|
||||
@load="injectScrollbarHideIfSameOrigin"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,6 +59,22 @@ import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
|
||||
const store = useAppLauncherStore()
|
||||
const closeBtnRef = ref<HTMLButtonElement | null>(null)
|
||||
const iframeRef = ref<HTMLIFrameElement | null>(null)
|
||||
|
||||
function injectScrollbarHideIfSameOrigin() {
|
||||
try {
|
||||
const doc = iframeRef.value?.contentDocument
|
||||
if (!doc) return
|
||||
const style = doc.createElement('style')
|
||||
style.textContent = `
|
||||
* { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
*::-webkit-scrollbar { display: none; }
|
||||
`
|
||||
doc.head.appendChild(style)
|
||||
} catch {
|
||||
/* Cross-origin: cannot access iframe document */
|
||||
}
|
||||
}
|
||||
|
||||
const panelClasses = [
|
||||
'glass-card',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div
|
||||
ref="modalRef"
|
||||
@click.stop
|
||||
class="glass-card p-6 max-w-lg w-full relative z-10 max-h-[80vh] overflow-y-auto"
|
||||
>
|
||||
@@ -45,14 +46,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
import { ref, computed } from 'vue'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
title: string
|
||||
content: string
|
||||
relatedPath?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const modalRef = ref<HTMLElement | null>(null)
|
||||
useModalKeyboard(modalRef, computed(() => props.show), () => emit('close'))
|
||||
</script>
|
||||
|
||||
@@ -1,39 +1,59 @@
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="showUpdatePrompt"
|
||||
class="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-96 z-[9999]"
|
||||
>
|
||||
<div class="glass-card p-4 flex items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<p class="text-white/90 text-sm font-medium mb-1">Update Available</p>
|
||||
<p class="text-white/70 text-xs">A new version is available. Click to update.</p>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div
|
||||
v-if="showUpdatePrompt"
|
||||
class="fixed inset-0 z-[9999] flex items-center justify-center p-4"
|
||||
@click.self="dismissUpdate"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div
|
||||
ref="modalRef"
|
||||
class="glass-card p-6 max-w-md w-full relative z-10"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4 mb-4">
|
||||
<h3 class="text-xl font-semibold text-white">Update Available</h3>
|
||||
<button
|
||||
@click="dismissUpdate"
|
||||
class="p-2 rounded-lg hover:bg-white/10 text-white/70 hover:text-white transition-colors"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-white/80 mb-6">
|
||||
A new version of Archipelago is available. Update now to get the latest features and fixes.
|
||||
</p>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button
|
||||
@click="dismissUpdate"
|
||||
class="px-4 py-2 glass-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
Later
|
||||
</button>
|
||||
<button
|
||||
@click="handleUpdate"
|
||||
class="px-4 py-2 gradient-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
Update Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="handleUpdate"
|
||||
class="glass-button px-4 py-2 rounded-lg text-sm font-medium transition-all hover:bg-black/70 hover:border-white/30"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<button
|
||||
@click="dismissUpdate"
|
||||
class="text-white/50 hover:text-white/80 transition-colors p-2"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const showUpdatePrompt = ref(false)
|
||||
let updateCallback: (() => Promise<void>) | null = null
|
||||
const modalRef = ref<HTMLElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
// Listen for service worker updates
|
||||
@@ -54,6 +74,13 @@ onMounted(() => {
|
||||
// Check for updates every 5 minutes
|
||||
setInterval(checkForUpdates, 5 * 60 * 1000)
|
||||
|
||||
// Check when user returns to tab (helps with cached PWA)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
checkForUpdates()
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for updatefound event
|
||||
navigator.serviceWorker.getRegistration().then((registration) => {
|
||||
if (registration) {
|
||||
@@ -79,6 +106,8 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
useModalKeyboard(modalRef, showUpdatePrompt, dismissUpdate)
|
||||
|
||||
function dismissUpdate() {
|
||||
showUpdatePrompt.value = false
|
||||
}
|
||||
@@ -89,17 +118,3 @@ async function handleUpdate() {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
/>
|
||||
</div>
|
||||
<!-- Logo in center -->
|
||||
<div class="screensaver-logo-wrapper relative z-10">
|
||||
<AnimatedLogo size="xl" />
|
||||
<div class="screensaver-logo-wrapper">
|
||||
<ScreensaverLogo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeUnmount } from 'vue'
|
||||
import AnimatedLogo from '@/components/AnimatedLogo.vue'
|
||||
import ScreensaverLogo from '@/components/ScreensaverLogo.vue'
|
||||
import { useScreensaverStore } from '@/stores/screensaver'
|
||||
|
||||
const store = useScreensaverStore()
|
||||
@@ -100,12 +100,14 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* Ring of segments around the logo - audio viz style */
|
||||
/* Ring of segments around the logo - audio viz style (behind logo) */
|
||||
.screensaver-viz-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
--viz-radius: 140px;
|
||||
}
|
||||
|
||||
@@ -175,6 +177,7 @@ onBeforeUnmount(() => {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
filter: drop-shadow(0 0 40px rgba(255, 255, 255, 0.15));
|
||||
}
|
||||
</style>
|
||||
|
||||
20
neode-ui/src/components/ScreensaverLogo.vue
Normal file
20
neode-ui/src/components/ScreensaverLogo.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="logo-gradient-border screensaver-logo-cycle relative w-48 h-48 sm:w-64 sm:h-64 md:w-80 md:h-80 flex items-center justify-center overflow-hidden">
|
||||
<!-- Squares logo -->
|
||||
<div class="screensaver-logo-squares absolute inset-[3px] flex items-center justify-center">
|
||||
<AnimatedLogo size="xl" no-border fit />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AnimatedLogo from '@/components/AnimatedLogo.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.screensaver-logo-squares {
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -218,6 +218,16 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// My Apps: Enter = launch (click Launch button when app is runnable)
|
||||
if (el.hasAttribute('data-controller-launch')) {
|
||||
const launchBtn = el.querySelector<HTMLButtonElement>('[data-controller-launch-btn]:not([disabled])')
|
||||
if (launchBtn) {
|
||||
playNavSound('action')
|
||||
launchBtn.click()
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
// My Apps, etc: Enter = focus first inner control
|
||||
const inner = getInnerFocusables(el)
|
||||
const firstInner = inner[0]
|
||||
|
||||
74
neode-ui/src/composables/useModalKeyboard.ts
Normal file
74
neode-ui/src/composables/useModalKeyboard.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Modal keyboard navigation: Escape to close, Arrow keys to move between buttons.
|
||||
* Restores focus to the previously active element when closing via Escape.
|
||||
*/
|
||||
|
||||
import { onMounted, onBeforeUnmount, watch, type Ref } from 'vue'
|
||||
|
||||
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
|
||||
export interface UseModalKeyboardOptions {
|
||||
restoreFocusRef?: Ref<HTMLElement | null>
|
||||
}
|
||||
|
||||
export function useModalKeyboard(
|
||||
containerRef: Ref<HTMLElement | null>,
|
||||
isOpen: Ref<boolean>,
|
||||
onClose: () => void,
|
||||
options?: UseModalKeyboardOptions
|
||||
) {
|
||||
const restoreFocusRef = options?.restoreFocusRef
|
||||
|
||||
// Save the element that had focus when modal opens (before focus moves to modal)
|
||||
watch(isOpen, (open) => {
|
||||
if (open && restoreFocusRef) {
|
||||
restoreFocusRef.value = document.activeElement as HTMLElement | null
|
||||
}
|
||||
})
|
||||
function getFocusables(): HTMLElement[] {
|
||||
const el = containerRef.value
|
||||
if (!el) return []
|
||||
return Array.from(el.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(
|
||||
(e) => e.offsetParent !== null
|
||||
)
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (!isOpen.value) return
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
restoreFocusRef?.value?.focus?.()
|
||||
onClose()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
||||
const focusables = getFocusables()
|
||||
if (focusables.length === 0) return
|
||||
|
||||
const current = document.activeElement as HTMLElement | null
|
||||
const idx = current ? focusables.indexOf(current) : -1
|
||||
|
||||
let nextIdx: number
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
||||
nextIdx = idx < focusables.length - 1 ? idx + 1 : 0
|
||||
} else {
|
||||
nextIdx = idx > 0 ? idx - 1 : focusables.length - 1
|
||||
}
|
||||
|
||||
focusables[nextIdx]?.focus()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleKeydown, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeydown, true)
|
||||
})
|
||||
}
|
||||
@@ -70,8 +70,10 @@ export const useSpotlightStore = defineStore('spotlight', () => {
|
||||
content: '',
|
||||
relatedPath: undefined as string | undefined,
|
||||
})
|
||||
const helpModalRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function showHelpModal(payload: { title: string; content: string; relatedPath?: string }) {
|
||||
helpModalRestoreFocusRef.value = document.activeElement as HTMLElement | null
|
||||
helpModal.show = true
|
||||
helpModal.title = payload.title
|
||||
helpModal.content = payload.content
|
||||
@@ -79,6 +81,8 @@ export const useSpotlightStore = defineStore('spotlight', () => {
|
||||
}
|
||||
|
||||
function closeHelpModal() {
|
||||
helpModalRestoreFocusRef.value?.focus?.()
|
||||
helpModalRestoreFocusRef.value = null
|
||||
helpModal.show = false
|
||||
}
|
||||
|
||||
|
||||
@@ -444,6 +444,28 @@ body {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0.2) 100%);
|
||||
}
|
||||
|
||||
/* Hide scrollbar but keep scroll functionality - applied globally to all scrollable content */
|
||||
.scrollbar-hide,
|
||||
.overflow-y-auto,
|
||||
.overflow-auto,
|
||||
.overflow-y-scroll,
|
||||
.iframe-scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar,
|
||||
.overflow-y-auto::-webkit-scrollbar,
|
||||
.overflow-auto::-webkit-scrollbar,
|
||||
.overflow-y-scroll::-webkit-scrollbar,
|
||||
.iframe-scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Iframe scrollbar hide - targets iframe element; inner doc scrollbars need same-origin injection */
|
||||
iframe.iframe-scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeUpIn {
|
||||
0% {
|
||||
|
||||
@@ -383,10 +383,11 @@
|
||||
<div
|
||||
v-if="uninstallModal.show"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
@click="uninstallModal.show = false"
|
||||
@click="closeUninstallModal()"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div
|
||||
ref="uninstallModalRef"
|
||||
@click.stop
|
||||
class="glass-card p-6 md:p-8 max-w-md w-full relative z-10"
|
||||
>
|
||||
@@ -407,7 +408,7 @@
|
||||
|
||||
<div class="flex flex-col-reverse md:flex-row gap-3 md:justify-end">
|
||||
<button
|
||||
@click="uninstallModal.show = false"
|
||||
@click="closeUninstallModal()"
|
||||
class="w-full md:w-auto px-6 py-3 glass-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
Cancel
|
||||
@@ -432,6 +433,7 @@ import { useAppStore } from '../stores/app'
|
||||
import { useAppLauncherStore } from '../stores/appLauncher'
|
||||
import { PackageState } from '../types/api'
|
||||
import { useMobileBackButton } from '../composables/useMobileBackButton'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
import { dummyApps } from '../utils/dummyApps'
|
||||
|
||||
const { bottomPosition } = useMobileBackButton()
|
||||
@@ -510,6 +512,18 @@ const uninstallModal = ref({
|
||||
show: false,
|
||||
appTitle: ''
|
||||
})
|
||||
const uninstallModalRef = ref<HTMLElement | null>(null)
|
||||
const uninstallRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
function closeUninstallModal() {
|
||||
uninstallRestoreFocusRef.value?.focus?.()
|
||||
uninstallModal.value.show = false
|
||||
}
|
||||
useModalKeyboard(
|
||||
uninstallModalRef,
|
||||
computed(() => uninstallModal.value.show),
|
||||
closeUninstallModal,
|
||||
{ restoreFocusRef: uninstallRestoreFocusRef }
|
||||
)
|
||||
|
||||
// Determine back button text based on where user came from
|
||||
const backButtonText = computed(() => {
|
||||
@@ -607,6 +621,10 @@ function launchApp() {
|
||||
dev: 'http://localhost:8103',
|
||||
prod: 'http://localhost:8103' // Self-hosted splash screen
|
||||
},
|
||||
'indeedhub': {
|
||||
dev: 'http://localhost:7777',
|
||||
prod: 'http://localhost:7777' // Containerized indeehub prototype
|
||||
},
|
||||
// Dummy apps - replace with real URLs when packaged
|
||||
'bitcoin': {
|
||||
dev: 'http://localhost:8332',
|
||||
@@ -663,7 +681,11 @@ function launchApp() {
|
||||
}
|
||||
|
||||
if (appUrls[id]) {
|
||||
const url = isDev ? appUrls[id].dev : appUrls[id].prod
|
||||
let url = isDev ? appUrls[id].dev : appUrls[id].prod
|
||||
// Replace localhost with current hostname for remote access
|
||||
if (url.includes('localhost')) {
|
||||
url = url.replace('localhost', window.location.hostname)
|
||||
}
|
||||
useAppLauncherStore().open({ url, title: pkg.value.manifest.title })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
v-for="[id, pkg] in sortedPackageEntries"
|
||||
:key="id"
|
||||
data-controller-container
|
||||
:data-controller-launch="canLaunch(pkg) ? '' : undefined"
|
||||
tabindex="0"
|
||||
class="glass-card p-6 transition-all hover:-translate-y-1 cursor-pointer relative min-w-0 overflow-hidden"
|
||||
@click="goToApp(id as string)"
|
||||
@@ -75,6 +76,7 @@
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button
|
||||
v-if="canLaunch(pkg)"
|
||||
data-controller-launch-btn
|
||||
@click.stop="launchApp(id as string)"
|
||||
class="flex-1 px-4 py-2 gradient-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
@@ -125,10 +127,11 @@
|
||||
<div
|
||||
v-if="uninstallModal.show"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
@click="uninstallModal.show = false"
|
||||
@click="closeUninstallModal()"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div
|
||||
ref="uninstallModalRef"
|
||||
@click.stop
|
||||
class="glass-card p-6 max-w-md w-full relative z-10"
|
||||
>
|
||||
@@ -149,7 +152,7 @@
|
||||
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button
|
||||
@click="uninstallModal.show = false"
|
||||
@click="closeUninstallModal()"
|
||||
class="px-4 py-2 glass-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
Cancel
|
||||
@@ -173,6 +176,7 @@ import { useRouter, RouterLink } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useAppLauncherStore } from '../stores/appLauncher'
|
||||
import { PackageState } from '../types/api'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
@@ -200,6 +204,18 @@ const uninstallModal = ref({
|
||||
appId: '',
|
||||
appTitle: ''
|
||||
})
|
||||
const uninstallModalRef = ref<HTMLElement | null>(null)
|
||||
const uninstallRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
function closeUninstallModal() {
|
||||
uninstallRestoreFocusRef.value?.focus?.()
|
||||
uninstallModal.value.show = false
|
||||
}
|
||||
useModalKeyboard(
|
||||
uninstallModalRef,
|
||||
computed(() => uninstallModal.value.show),
|
||||
closeUninstallModal,
|
||||
{ restoreFocusRef: uninstallRestoreFocusRef }
|
||||
)
|
||||
|
||||
function canLaunch(pkg: any): boolean {
|
||||
// For dummy apps, allow launch if running (they have interface addresses)
|
||||
@@ -237,6 +253,10 @@ function launchApp(id: string) {
|
||||
'k484': {
|
||||
dev: 'http://localhost:8103',
|
||||
prod: 'http://localhost:8103' // Self-hosted splash screen
|
||||
},
|
||||
'indeedhub': {
|
||||
dev: 'http://localhost:7777',
|
||||
prod: 'http://localhost:7777' // Containerized indeehub prototype
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,12 +218,12 @@
|
||||
<div
|
||||
v-if="showSideloadModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
@click.self="showSideloadModal = false"
|
||||
@click.self="closeSideloadModal()"
|
||||
>
|
||||
<div class="glass-card p-8 max-w-2xl w-full relative">
|
||||
<div ref="sideloadModalRef" class="glass-card p-8 max-w-2xl w-full relative">
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
@click="showSideloadModal = false"
|
||||
@click="closeSideloadModal()"
|
||||
class="absolute top-4 right-4 text-white/60 hover:text-white transition-colors"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -289,14 +289,14 @@
|
||||
<div
|
||||
v-if="showFilterModal"
|
||||
class="fixed inset-0 z-50 flex items-end justify-center md:hidden bg-black/60 backdrop-blur-sm"
|
||||
@click.self="showFilterModal = false"
|
||||
@click.self="closeFilterModal()"
|
||||
>
|
||||
<div class="glass-card p-6 w-full rounded-t-3xl max-h-[80vh] overflow-y-auto">
|
||||
<div ref="filterModalRef" class="glass-card p-6 w-full rounded-t-3xl max-h-[80vh] overflow-y-auto">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-bold text-white">Filter by Category</h2>
|
||||
<button
|
||||
@click="showFilterModal = false"
|
||||
@click="closeFilterModal()"
|
||||
class="text-white/60 hover:text-white transition-colors"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -310,7 +310,7 @@
|
||||
<button
|
||||
v-for="category in categoriesWithApps"
|
||||
:key="category.id"
|
||||
@click="selectedCategory = category.id; showFilterModal = false"
|
||||
@click="selectedCategory = category.id; closeFilterModal()"
|
||||
:class="[
|
||||
'p-4 rounded-xl font-medium transition-all text-left',
|
||||
selectedCategory === category.id
|
||||
@@ -372,6 +372,7 @@ import { useAppStore } from '@/stores/app'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useMarketplaceApp } from '@/composables/useMarketplaceApp'
|
||||
import { useMobileBackButton } from '@/composables/useMobileBackButton'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
@@ -408,6 +409,13 @@ const maxAttempts = ref(60)
|
||||
|
||||
// Sideload modal state
|
||||
const showSideloadModal = ref(false)
|
||||
const sideloadModalRef = ref<HTMLElement | null>(null)
|
||||
const sideloadRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
function closeSideloadModal() {
|
||||
sideloadRestoreFocusRef.value?.focus?.()
|
||||
showSideloadModal.value = false
|
||||
}
|
||||
useModalKeyboard(sideloadModalRef, showSideloadModal, closeSideloadModal, { restoreFocusRef: sideloadRestoreFocusRef })
|
||||
const sideloadUrl = ref('')
|
||||
const sideloading = ref(false)
|
||||
const sideloadError = ref('')
|
||||
@@ -415,6 +423,13 @@ const sideloadSuccess = ref('')
|
||||
|
||||
// Filter modal state (for mobile)
|
||||
const showFilterModal = ref(false)
|
||||
const filterModalRef = ref<HTMLElement | null>(null)
|
||||
const filterRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
function closeFilterModal() {
|
||||
filterRestoreFocusRef.value?.focus?.()
|
||||
showFilterModal.value = false
|
||||
}
|
||||
useModalKeyboard(filterModalRef, showFilterModal, closeFilterModal, { restoreFocusRef: filterRestoreFocusRef })
|
||||
|
||||
// Community marketplace state
|
||||
const loadingCommunity = ref(false)
|
||||
|
||||
@@ -108,9 +108,9 @@
|
||||
<div
|
||||
v-if="showChangePasswordModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
@click.self="showChangePasswordModal = false"
|
||||
@click.self="closeChangePasswordModal()"
|
||||
>
|
||||
<div class="glass-card p-6 max-w-md w-full">
|
||||
<div ref="changePasswordModalRef" class="glass-card p-6 max-w-md w-full">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Change Password</h3>
|
||||
<p class="text-white/70 text-sm mb-4">Updates both web login and SSH access. Use a strong password (12+ chars, upper, lower, digit, special).</p>
|
||||
<form @submit.prevent="handleChangePassword" class="space-y-4">
|
||||
@@ -206,6 +206,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import ControllerIndicator from '@/components/ControllerIndicator.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
@@ -225,6 +226,9 @@ const userDid = computed(() => {
|
||||
|
||||
const copiedOnion = ref(false)
|
||||
const showChangePasswordModal = ref(false)
|
||||
const changePasswordModalRef = ref<HTMLElement | null>(null)
|
||||
const changePasswordRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
useModalKeyboard(changePasswordModalRef, showChangePasswordModal, closeChangePasswordModal, { restoreFocusRef: changePasswordRestoreFocusRef })
|
||||
const changingPassword = ref(false)
|
||||
const changePasswordError = ref('')
|
||||
const changePasswordSuccess = ref('')
|
||||
@@ -301,6 +305,7 @@ async function copyOnionAddress() {
|
||||
}
|
||||
|
||||
function closeChangePasswordModal() {
|
||||
changePasswordRestoreFocusRef.value?.focus?.()
|
||||
showChangePasswordModal.value = false
|
||||
changePasswordError.value = ''
|
||||
changePasswordSuccess.value = ''
|
||||
|
||||
@@ -108,8 +108,8 @@
|
||||
|
||||
<!-- Send Message Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showSendMessageModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showSendMessageModal = false">
|
||||
<div class="glass-card p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||
<div v-if="showSendMessageModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="closeSendMessageModal()">
|
||||
<div ref="sendMessageModalRef" class="glass-card p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Send Message (over Tor)</h3>
|
||||
<p class="text-white/70 text-sm mb-4">Messages are sent over the Tor network to the selected peer.</p>
|
||||
<div class="space-y-4">
|
||||
@@ -144,7 +144,7 @@
|
||||
{{ sendingMessage ? 'Sending...' : 'Send' }}
|
||||
</button>
|
||||
<button
|
||||
@click="showSendMessageModal = false"
|
||||
@click="closeSendMessageModal()"
|
||||
class="px-4 py-2 rounded-lg bg-white/10 text-white font-medium hover:bg-white/20 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
@@ -630,6 +630,7 @@ import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useMessageToast } from '@/composables/useMessageToast'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const route = useRoute()
|
||||
const messageToast = useMessageToast()
|
||||
@@ -666,6 +667,13 @@ const connectedNodesCount = computed(() => peers.value.length)
|
||||
|
||||
// Send Message modal
|
||||
const showSendMessageModal = ref(false)
|
||||
const sendMessageModalRef = ref<HTMLElement | null>(null)
|
||||
const sendMessageRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
function closeSendMessageModal() {
|
||||
sendMessageRestoreFocusRef.value?.focus?.()
|
||||
showSendMessageModal.value = false
|
||||
}
|
||||
useModalKeyboard(sendMessageModalRef, showSendMessageModal, closeSendMessageModal, { restoreFocusRef: sendMessageRestoreFocusRef })
|
||||
const sendMessageTo = ref('')
|
||||
const sendMessageText = ref('')
|
||||
const sendingMessage = ref(false)
|
||||
|
||||
Reference in New Issue
Block a user