2026-03-15 00:40:55 +00:00
|
|
|
<template>
|
|
|
|
|
<div class="app-session-root">
|
2026-03-16 12:58:35 +00:00
|
|
|
<Teleport to="body" :disabled="isInlinePanel">
|
2026-03-15 00:40:55 +00:00
|
|
|
<div
|
|
|
|
|
:class="backdropClasses"
|
2026-03-16 12:58:35 +00:00
|
|
|
@click.self="handleBackdropClick"
|
2026-03-15 00:40:55 +00:00
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
ref="sessionRef"
|
|
|
|
|
:class="panelClasses"
|
|
|
|
|
@click.stop
|
|
|
|
|
>
|
2026-03-22 03:30:21 +00:00
|
|
|
<AppSessionHeader
|
|
|
|
|
:app-title="appTitle"
|
|
|
|
|
:is-refreshing="isRefreshing"
|
|
|
|
|
:display-mode="displayMode"
|
|
|
|
|
@go-back="iframeGoBack"
|
|
|
|
|
@go-forward="iframeGoForward"
|
|
|
|
|
@refresh="refresh"
|
|
|
|
|
@open-new-tab="openNewTab"
|
|
|
|
|
@close="closeSession"
|
|
|
|
|
@set-mode="setMode"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<AppSessionFrame
|
|
|
|
|
ref="frameRef"
|
|
|
|
|
:app-url="appUrl"
|
|
|
|
|
:app-id="appId"
|
|
|
|
|
:app-title="appTitle"
|
|
|
|
|
:loading="loading"
|
|
|
|
|
:iframe-blocked="iframeBlocked"
|
|
|
|
|
:must-open-new-tab="mustOpenNewTab"
|
|
|
|
|
:auto-retry-count="autoRetryCount"
|
|
|
|
|
:refresh-key="refreshKey"
|
|
|
|
|
@iframe-load="onLoad"
|
|
|
|
|
@iframe-error="onError"
|
|
|
|
|
@refresh="refresh"
|
|
|
|
|
@open-new-tab-and-back="openNewTabAndBack"
|
|
|
|
|
/>
|
2026-03-30 21:03:00 +01:00
|
|
|
|
|
|
|
|
<!-- Mobile: floating glass close button -->
|
|
|
|
|
<button
|
|
|
|
|
class="md:hidden app-session-mobile-close"
|
|
|
|
|
aria-label="Close"
|
|
|
|
|
@click="closeSession"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-03-15 00:40:55 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<NostrIdentityPicker
|
|
|
|
|
:show="showIdentityPicker"
|
|
|
|
|
:app-name="appTitle"
|
2026-03-22 03:30:21 +00:00
|
|
|
@select="identity.onIdentitySelected"
|
2026-03-15 00:40:55 +00:00
|
|
|
@cancel="showIdentityPicker = false"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</Teleport>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-03-16 12:58:35 +00:00
|
|
|
import { useAppLauncherStore } from '@/stores/appLauncher'
|
2026-03-15 00:40:55 +00:00
|
|
|
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
|
2026-03-22 03:30:21 +00:00
|
|
|
import AppSessionHeader from './appSession/AppSessionHeader.vue'
|
|
|
|
|
import AppSessionFrame from './appSession/AppSessionFrame.vue'
|
|
|
|
|
import {
|
|
|
|
|
type DisplayMode, DISPLAY_MODE_KEY, NEW_TAB_APPS, IFRAME_BLOCKED_APPS,
|
|
|
|
|
resolveAppUrl, resolveAppTitle,
|
|
|
|
|
} from './appSession/appSessionConfig'
|
|
|
|
|
import { useAppIdentity } from './appSession/useAppIdentity'
|
|
|
|
|
import { useNostrBridge } from './appSession/useNostrBridge'
|
2026-03-15 00:40:55 +00:00
|
|
|
|
2026-03-16 12:58:35 +00:00
|
|
|
const props = defineProps<{
|
|
|
|
|
appIdProp?: string
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
close: []
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
/** True when rendered inline via store (panel mode), false when route-based */
|
|
|
|
|
const isInlinePanel = computed(() => !!props.appIdProp)
|
|
|
|
|
|
2026-03-15 00:40:55 +00:00
|
|
|
const route = useRoute()
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
|
|
|
|
const sessionRef = ref<HTMLElement | null>(null)
|
2026-03-22 03:30:21 +00:00
|
|
|
const frameRef = ref<InstanceType<typeof AppSessionFrame> | null>(null)
|
2026-03-15 00:40:55 +00:00
|
|
|
const loading = ref(true)
|
|
|
|
|
const isRefreshing = ref(false)
|
|
|
|
|
const iframeBlocked = ref(false)
|
|
|
|
|
const refreshKey = ref(0)
|
|
|
|
|
const showIdentityPicker = ref(false)
|
2026-03-19 14:52:16 +00:00
|
|
|
const autoRetryCount = ref(0)
|
2026-03-15 00:40:55 +00:00
|
|
|
let loadTimeoutId: ReturnType<typeof setTimeout> | null = null
|
2026-03-19 14:52:16 +00:00
|
|
|
let autoRetryId: ReturnType<typeof setTimeout> | null = null
|
2026-03-21 02:06:08 +00:00
|
|
|
let iframeCheckId: ReturnType<typeof setTimeout> | null = null
|
2026-03-15 00:40:55 +00:00
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
// Display mode -- persisted in localStorage
|
2026-03-15 00:40:55 +00:00
|
|
|
const displayMode = ref<DisplayMode>(
|
|
|
|
|
(localStorage.getItem(DISPLAY_MODE_KEY) as DisplayMode) || 'panel'
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
const appId = computed(() => {
|
|
|
|
|
const id = props.appIdProp || (route.params.appId as string)
|
|
|
|
|
if (typeof id !== 'string' || !/^[a-z0-9][a-z0-9._-]*$/.test(id) || id.length > 64) {
|
|
|
|
|
router.replace('/apps')
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
return id
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const appTitle = computed(() => resolveAppTitle(appId.value))
|
|
|
|
|
const mustOpenNewTab = computed(() => NEW_TAB_APPS.has(appId.value))
|
|
|
|
|
|
|
|
|
|
const appUrl = computed(() => {
|
|
|
|
|
return resolveAppUrl(appId.value, route.query.path as string | undefined)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// --- Identity & Nostr bridge ---
|
|
|
|
|
|
|
|
|
|
const iframeRef = computed(() => frameRef.value?.iframeRef ?? null)
|
|
|
|
|
|
|
|
|
|
const identity = useAppIdentity(appId, iframeRef, showIdentityPicker)
|
|
|
|
|
const nostrBridge = useNostrBridge(identity.getStoredIdentity, () => appUrl.value)
|
|
|
|
|
|
|
|
|
|
// --- Display mode ---
|
|
|
|
|
|
2026-03-15 00:40:55 +00:00
|
|
|
function setMode(mode: DisplayMode) {
|
|
|
|
|
if (displayMode.value === 'fullscreen' && document.fullscreenElement) {
|
|
|
|
|
document.exitFullscreen().catch(() => {})
|
|
|
|
|
}
|
|
|
|
|
displayMode.value = mode
|
|
|
|
|
localStorage.setItem(DISPLAY_MODE_KEY, mode)
|
2026-03-16 12:58:35 +00:00
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
// Switch from inline panel to route-based overlay/fullscreen
|
2026-03-16 12:58:35 +00:00
|
|
|
if (isInlinePanel.value && mode !== 'panel') {
|
|
|
|
|
const id = appId.value
|
|
|
|
|
emit('close')
|
|
|
|
|
router.push({ name: 'app-session', params: { appId: id } })
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
// Switch from route-based to inline panel
|
2026-03-16 12:58:35 +00:00
|
|
|
if (!isInlinePanel.value && mode === 'panel') {
|
|
|
|
|
const id = appId.value
|
|
|
|
|
const launcher = useAppLauncherStore()
|
|
|
|
|
router.push({ name: 'apps' }).then(() => {
|
|
|
|
|
launcher.panelAppId = id
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 00:40:55 +00:00
|
|
|
if (mode === 'fullscreen' && sessionRef.value && !document.fullscreenElement) {
|
|
|
|
|
sessionRef.value.requestFullscreen().catch(() => {})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reactive classes based on display mode
|
|
|
|
|
const backdropClasses = computed(() => {
|
2026-03-16 12:58:35 +00:00
|
|
|
if (isInlinePanel.value) return 'app-session-backdrop-inline'
|
|
|
|
|
return 'app-session-backdrop-overlay'
|
2026-03-15 00:40:55 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const panelClasses = computed(() => {
|
|
|
|
|
const base = 'app-session-panel glass-card'
|
2026-03-16 12:58:35 +00:00
|
|
|
if (isInlinePanel.value) return `${base} app-session-inline`
|
2026-03-15 00:40:55 +00:00
|
|
|
if (displayMode.value === 'fullscreen') return `${base} app-session-fullscreen`
|
2026-03-16 12:58:35 +00:00
|
|
|
return `${base} app-session-overlay`
|
2026-03-15 00:40:55 +00:00
|
|
|
})
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
// --- Lifecycle handlers ---
|
2026-03-15 00:40:55 +00:00
|
|
|
|
|
|
|
|
function onLoad() {
|
|
|
|
|
if (loadTimeoutId) { clearTimeout(loadTimeoutId); loadTimeoutId = null }
|
2026-03-19 14:52:16 +00:00
|
|
|
if (autoRetryId) { clearTimeout(autoRetryId); autoRetryId = null }
|
2026-03-15 00:40:55 +00:00
|
|
|
loading.value = false
|
|
|
|
|
isRefreshing.value = false
|
2026-03-19 14:52:16 +00:00
|
|
|
autoRetryCount.value = 0
|
2026-03-15 00:40:55 +00:00
|
|
|
// Check if iframe actually loaded content (same-origin only)
|
2026-03-21 02:06:08 +00:00
|
|
|
iframeCheckId = setTimeout(() => {
|
2026-03-15 00:40:55 +00:00
|
|
|
try {
|
2026-03-22 03:30:21 +00:00
|
|
|
const iframe = frameRef.value?.iframeRef
|
|
|
|
|
const doc = iframe?.contentDocument
|
2026-03-15 00:40:55 +00:00
|
|
|
if (doc) {
|
|
|
|
|
const body = doc.body
|
|
|
|
|
if (!body || (body.children.length === 0 && body.innerText.trim() === '')) {
|
|
|
|
|
iframeBlocked.value = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
2026-03-22 03:30:21 +00:00
|
|
|
// Cross-origin -- can't check, assume OK
|
2026-03-15 00:40:55 +00:00
|
|
|
}
|
|
|
|
|
}, 1000)
|
2026-03-22 03:30:21 +00:00
|
|
|
identity.onIframeLoadIdentity()
|
2026-03-15 00:40:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onError() {
|
|
|
|
|
if (loadTimeoutId) { clearTimeout(loadTimeoutId); loadTimeoutId = null }
|
|
|
|
|
loading.value = false
|
|
|
|
|
isRefreshing.value = false
|
|
|
|
|
iframeBlocked.value = true
|
2026-03-19 14:52:16 +00:00
|
|
|
// Auto-retry up to 6 times (60s total) for apps that are still starting
|
|
|
|
|
if (!mustOpenNewTab.value && autoRetryCount.value < 6) {
|
|
|
|
|
autoRetryId = setTimeout(() => {
|
|
|
|
|
autoRetryCount.value++
|
|
|
|
|
refresh()
|
|
|
|
|
}, 10000)
|
|
|
|
|
}
|
2026-03-15 00:40:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function refresh() {
|
2026-03-19 14:52:16 +00:00
|
|
|
if (autoRetryId) { clearTimeout(autoRetryId); autoRetryId = null }
|
2026-03-15 00:40:55 +00:00
|
|
|
isRefreshing.value = true
|
|
|
|
|
loading.value = true
|
|
|
|
|
iframeBlocked.value = false
|
|
|
|
|
refreshKey.value++
|
|
|
|
|
startLoadTimeout()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startLoadTimeout() {
|
|
|
|
|
if (loadTimeoutId) clearTimeout(loadTimeoutId)
|
|
|
|
|
loadTimeoutId = setTimeout(() => {
|
|
|
|
|
if (loading.value) {
|
|
|
|
|
loading.value = false
|
|
|
|
|
iframeBlocked.value = true
|
|
|
|
|
}
|
|
|
|
|
}, 12000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openNewTabAndBack() {
|
|
|
|
|
if (appUrl.value) window.open(appUrl.value, '_blank', 'noopener,noreferrer')
|
2026-03-16 12:58:35 +00:00
|
|
|
closeSession()
|
2026-03-15 00:40:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openNewTab() {
|
|
|
|
|
if (appUrl.value) window.open(appUrl.value, '_blank', 'noopener,noreferrer')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function iframeGoBack() {
|
2026-03-22 03:30:21 +00:00
|
|
|
try { frameRef.value?.iframeRef?.contentWindow?.history.back() } catch {}
|
2026-03-15 00:40:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function iframeGoForward() {
|
2026-03-22 03:30:21 +00:00
|
|
|
try { frameRef.value?.iframeRef?.contentWindow?.history.forward() } catch {}
|
2026-03-15 00:40:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-16 12:58:35 +00:00
|
|
|
function handleBackdropClick() {
|
|
|
|
|
closeSession()
|
2026-03-15 00:40:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeSession() {
|
|
|
|
|
if (document.fullscreenElement) document.exitFullscreen().catch(() => {})
|
2026-03-16 12:58:35 +00:00
|
|
|
if (isInlinePanel.value) emit('close')
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
else router.back()
|
2026-03-15 00:40:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
if (document.fullscreenElement) document.exitFullscreen().catch(() => {})
|
|
|
|
|
else closeSession()
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onFullscreenChange() {
|
|
|
|
|
if (!document.fullscreenElement && displayMode.value === 'fullscreen') {
|
|
|
|
|
displayMode.value = 'overlay'
|
|
|
|
|
localStorage.setItem(DISPLAY_MODE_KEY, 'overlay')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
function onMessage(e: MessageEvent) {
|
|
|
|
|
if (e.data?.type === 'nostr-request') nostrBridge.handleNostrRequest(e)
|
|
|
|
|
if (e.data?.type === 'archipelago:identity:request') identity.handleIdentityRequest()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 00:40:55 +00:00
|
|
|
// Enter fullscreen on mount if mode is fullscreen
|
|
|
|
|
watch(displayMode, (mode) => {
|
|
|
|
|
if (mode === 'fullscreen' && sessionRef.value && !document.fullscreenElement) {
|
|
|
|
|
sessionRef.value.requestFullscreen().catch(() => {})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-03-22 03:30:21 +00:00
|
|
|
// Apps that block iframes (X-Frame-Options) -- open in new tab, close session
|
2026-03-16 12:58:35 +00:00
|
|
|
if (mustOpenNewTab.value && appUrl.value) {
|
|
|
|
|
window.open(appUrl.value, '_blank', 'noopener,noreferrer')
|
|
|
|
|
if (isInlinePanel.value) emit('close')
|
|
|
|
|
else router.back()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 00:40:55 +00:00
|
|
|
window.addEventListener('keydown', onKeyDown, true)
|
|
|
|
|
window.addEventListener('message', onMessage)
|
|
|
|
|
document.addEventListener('fullscreenchange', onFullscreenChange)
|
|
|
|
|
if (IFRAME_BLOCKED_APPS.has(appId.value)) {
|
|
|
|
|
loading.value = false
|
|
|
|
|
iframeBlocked.value = true
|
|
|
|
|
} else {
|
|
|
|
|
startLoadTimeout()
|
|
|
|
|
}
|
|
|
|
|
if (displayMode.value === 'fullscreen') {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
sessionRef.value?.requestFullscreen().catch(() => {})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
if (loadTimeoutId) clearTimeout(loadTimeoutId)
|
2026-03-19 14:52:16 +00:00
|
|
|
if (autoRetryId) clearTimeout(autoRetryId)
|
2026-03-21 02:06:08 +00:00
|
|
|
if (iframeCheckId) clearTimeout(iframeCheckId)
|
2026-03-15 00:40:55 +00:00
|
|
|
window.removeEventListener('keydown', onKeyDown, true)
|
|
|
|
|
window.removeEventListener('message', onMessage)
|
|
|
|
|
document.removeEventListener('fullscreenchange', onFullscreenChange)
|
|
|
|
|
if (document.fullscreenElement) document.exitFullscreen().catch(() => {})
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
<style>
|
|
|
|
|
/* Unscoped so children can use these classes */
|
2026-03-15 00:40:55 +00:00
|
|
|
.app-session-root {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
2026-03-22 03:30:21 +00:00
|
|
|
/* Inline panel mode -- fills content area, no blur, original layout */
|
2026-03-16 12:58:35 +00:00
|
|
|
.app-session-backdrop-inline {
|
2026-03-15 00:40:55 +00:00
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background: rgba(0, 0, 0, 0.4);
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 12:58:35 +00:00
|
|
|
.app-session-inline {
|
2026-03-15 00:40:55 +00:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
2026-03-16 12:58:35 +00:00
|
|
|
.app-session-backdrop-inline {
|
2026-03-15 00:40:55 +00:00
|
|
|
padding: 1.5rem;
|
|
|
|
|
}
|
2026-03-16 12:58:35 +00:00
|
|
|
.app-session-inline {
|
2026-03-15 00:40:55 +00:00
|
|
|
border-radius: 1rem;
|
|
|
|
|
max-width: calc(100% - 1rem);
|
|
|
|
|
max-height: calc(100vh - 6rem);
|
|
|
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 03:30:21 +00:00
|
|
|
/* Overlay mode -- covers entire viewport including sidebar */
|
2026-03-15 00:40:55 +00:00
|
|
|
.app-session-backdrop-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
inset: 0;
|
|
|
|
|
z-index: 2400;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 0;
|
|
|
|
|
background: rgba(0, 0, 0, 0.6);
|
|
|
|
|
backdrop-filter: blur(12px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
.app-session-backdrop-overlay {
|
|
|
|
|
padding: 2.5rem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.app-session-overlay {
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
.app-session-overlay {
|
|
|
|
|
max-width: calc(100vw - 5rem);
|
|
|
|
|
max-height: calc(100vh - 5rem);
|
|
|
|
|
border-radius: 1rem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Fullscreen mode */
|
|
|
|
|
.app-session-fullscreen {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 0 !important;
|
|
|
|
|
max-width: none !important;
|
|
|
|
|
max-height: none !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Shared */
|
|
|
|
|
.app-session-btn {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.app-session-btn:hover {
|
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
.app-session-btn:disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mode dropdown */
|
|
|
|
|
.mode-option {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
text-align: left;
|
|
|
|
|
}
|
|
|
|
|
.mode-option:hover {
|
|
|
|
|
background: rgba(255, 255, 255, 0.08);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
.mode-option-active {
|
|
|
|
|
color: #fb923c;
|
|
|
|
|
background: rgba(251, 146, 60, 0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-fade-enter-active,
|
|
|
|
|
.menu-fade-leave-active {
|
|
|
|
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
.menu-fade-enter-from,
|
|
|
|
|
.menu-fade-leave-to {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateY(-4px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.content-fade-enter-active,
|
|
|
|
|
.content-fade-leave-active {
|
|
|
|
|
transition: opacity 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
.content-fade-enter-from,
|
|
|
|
|
.content-fade-leave-to {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
2026-03-30 21:03:00 +01:00
|
|
|
|
|
|
|
|
/* Mobile floating glass close button */
|
|
|
|
|
.app-session-mobile-close {
|
|
|
|
|
position: fixed;
|
|
|
|
|
bottom: calc(24px + env(safe-area-inset-bottom, 0px));
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
z-index: 2500;
|
|
|
|
|
width: 48px;
|
|
|
|
|
height: 48px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: rgba(0, 0, 0, 0.45);
|
|
|
|
|
backdrop-filter: blur(16px);
|
|
|
|
|
-webkit-backdrop-filter: blur(16px);
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
|
|
|
color: rgba(255, 255, 255, 0.85);
|
|
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
|
|
|
transition: background 0.15s ease, transform 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
.app-session-mobile-close:active {
|
|
|
|
|
background: rgba(0, 0, 0, 0.65);
|
|
|
|
|
transform: translateX(-50%) scale(0.9);
|
|
|
|
|
}
|
2026-03-15 00:40:55 +00:00
|
|
|
</style>
|