frontend: add About dialog and unify global dialog backdrop

This commit is contained in:
2026-05-03 23:40:54 +00:00
parent c3146744b7
commit 07089aa9a7
7 changed files with 205 additions and 31 deletions
+13 -7
View File
@@ -8,6 +8,7 @@
<SettingsModal />
<UserManagementModal />
<UserTokensModal />
<AboutModal />
<AccessDeniedModal />
<header>
<HeaderMain ref="headerMain" :path="path.pathList" :query="path.query" />
@@ -28,11 +29,12 @@ import type HeaderMain from '@/components/HeaderMain.vue'
import { loadSession, watchConnect, watchDisconnect } from '@/repositories/WS'
import { useMainStore } from '@/stores/main'
import type { ComputedRef } from 'vue'
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { RouterView } from 'vue-router'
import Router from '@/router/index'
import { computed } from 'vue'
import AboutModal from './components/AboutModal.vue'
import AccessDeniedModal from './components/AccessDeniedModal.vue'
import SelectionToolbar from './components/SelectionToolbar.vue'
import type SettingsModalVue from './components/SettingsModal.vue'
@@ -56,12 +58,16 @@ const path: ComputedRef<Path> = computed(() => {
query
}
})
watchEffect(() => {
document.title =
path.value.path.replace(/\/$/, '').split('/').pop() ||
store.server.name ||
'Cista Storage'
})
watch(
() => path.value.path,
() => {
document.title =
path.value.path.replace(/\/$/, '').split('/').pop() ||
store.server.name ||
'Cista Storage'
},
{ immediate: true }
)
onMounted(loadSession)
onMounted(watchConnect)
onUnmounted(watchDisconnect)
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><rect width="512" height="512" fill="#f80"/><path fill="#fff" d="M381 298h-84V167h-66L339 35l108 132h-66zm-168-84h-84v131H63l108 132 108-132h-66z"/></svg>

After

Width:  |  Height:  |  Size: 242 B

+110
View File
@@ -0,0 +1,110 @@
<template>
<ModalDialog name="about" title="">
<div class="about-content">
<div class="about-logo-pane">
<img :src="logoUrl" alt="Cista Storage logo" class="about-logo" />
</div>
<div class="about-details">
<h3 class="about-name">Cista {{ softwareVersion }}</h3>
<p class="about-link">
<a :href="projectUrl" target="_blank" rel="noopener noreferrer">{{ displayProjectUrl }}</a>
</p>
<div class="dialog-buttons about-actions">
<div class="spacer"></div>
<input id="close" type="reset" value="Close" class="button" @click="close" />
</div>
</div>
</div>
</ModalDialog>
</template>
<script setup lang="ts">
import logoUrl from '@/assets/logo-square.svg?url'
import ModalDialog from '@/components/ModalDialog.vue'
import { useMainStore } from '@/stores/main'
import { computed } from 'vue'
const store = useMainStore()
const softwareVersion = computed(() => store.server.version || 'unknown')
const projectUrl = 'https://git.zi.fi/Vasanko/cista-storage'
const displayProjectUrl = projectUrl.replace(/^https?:\/\//, '')
const close = () => {
store.dialog = ''
}
</script>
<style scoped>
:deep(#about.modal-dialog) {
overflow: hidden;
}
.about-content {
display: grid;
grid-template-columns: 11rem minmax(0, 1fr);
align-items: stretch;
width: min(35rem, 92vw);
min-width: 0;
min-height: 0;
margin: -1rem;
overflow: hidden;
}
.about-logo-pane {
display: block;
padding: 0;
overflow: hidden;
}
.about-logo {
width: 100%;
height: auto;
aspect-ratio: 1 / 1;
margin: 0;
display: block;
}
.about-details {
display: flex;
flex-direction: column;
justify-content: center;
padding: 1.25rem;
}
.about-name {
margin: 0;
}
.about-link {
margin: 0.65rem 0 1rem;
word-break: break-word;
}
.about-actions {
margin-top: auto;
}
@media (max-width: 40rem) {
.about-content {
grid-template-columns: 1fr;
width: min(24rem, 90vw);
}
.about-logo-pane {
width: 100%;
aspect-ratio: 1 / 1;
}
.about-logo {
width: 100%;
height: 100%;
aspect-ratio: 1 / 1;
object-fit: contain;
}
.about-details {
padding: 0.85rem;
}
}
</style>
+7 -20
View File
@@ -1,32 +1,19 @@
<template>
<div v-if="store.dialog === 'accessdenied'" class="modal-overlay">
<div class="modal-dialog" id="accessdenied">
<div class="modal-content access-denied">
<p class="icon"></p>
<p class="message">Access Denied</p>
<button @click="reload" class="button">Reload</button>
</div>
<ModalDialog name="accessdenied" title="">
<div class="access-denied">
<p class="icon"></p>
<p class="message">Access Denied</p>
<button @click="reload" class="button">Reload</button>
</div>
</div>
</ModalDialog>
</template>
<script setup lang="ts">
import { useMainStore } from '@/stores/main'
import { holdGlobalBackdrop } from 'paskia'
import { watchEffect } from 'vue'
const store = useMainStore()
import ModalDialog from '@/components/ModalDialog.vue'
const reload = () => {
location.reload()
}
// Keep backdrop active when this dialog shows
watchEffect(() => {
if (store.dialog === 'accessdenied') {
holdGlobalBackdrop()
}
})
</script>
<style scoped>
+8
View File
@@ -164,6 +164,14 @@ const settingsMenu = (e: Event) => {
}
})
}
items.push({
label: '️ About Cista...',
onClick: () => {
store.dialog = 'about'
}
})
ContextMenu.showContextMenu({
// @ts-ignore
x: e.target.getBoundingClientRect().right,
+65 -3
View File
@@ -15,15 +15,55 @@
<script setup lang="ts">
import { useMainStore } from '@/stores/main'
import { holdGlobalBackdrop, releaseGlobalBackdrop } from 'paskia'
import { nextTick, ref, watchEffect } from 'vue'
import { nextTick, onBeforeUnmount, ref, watch, watchEffect } from 'vue'
const overlay = ref<HTMLDivElement | null>(null)
const dialog = ref<HTMLDivElement | null>(null)
const store = useMainStore()
let backdropHeld = false
const ensureGlobalBackdropStyles = () => {
if (typeof document === 'undefined') return
if (document.getElementById('paskia-dialog')) return
const style = document.createElement('style')
style.id = 'paskia-dialog'
style.textContent = `body::before {
content: '';
position: fixed;
inset: 0;
z-index: 1099;
background: transparent;
backdrop-filter: blur(0) brightness(1);
-webkit-backdrop-filter: blur(0) brightness(1);
pointer-events: none;
visibility: hidden;
transition: all 0.2s ease-out;
}
body.paskia-backdrop::before {
-webkit-backdrop-filter: blur(.2rem) brightness(0.5);
backdrop-filter: blur(.2rem) brightness(0.5);
visibility: visible;
}
body.paskia-backdrop {
overflow: auto;
}
#paskia-iframe {
border: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
color-scheme: auto;
background: transparent;
}
`
document.head.insertBefore(style, document.head.firstChild)
}
const close = () => {
store.dialog = ''
releaseGlobalBackdrop()
}
const props = defineProps<{
@@ -33,7 +73,6 @@ const props = defineProps<{
const show = () => {
store.dialog = props.name
holdGlobalBackdrop()
nextTick(() => {
overlay.value?.focus()
const input = dialog.value?.querySelector('input')
@@ -41,6 +80,29 @@ const show = () => {
})
}
defineExpose({ show, close })
watch(
() => store.dialog === props.name,
isOpen => {
if (isOpen && !backdropHeld) {
ensureGlobalBackdropStyles()
holdGlobalBackdrop()
backdropHeld = true
} else if (!isOpen && backdropHeld) {
releaseGlobalBackdrop()
backdropHeld = false
}
},
{ immediate: true }
)
onBeforeUnmount(() => {
if (backdropHeld) {
releaseGlobalBackdrop()
backdropHeld = false
}
})
watchEffect(() => {
if (overlay.value) {
overlay.value.focus()
+1 -1
View File
@@ -84,7 +84,7 @@ export const useMainStore = defineStore('main', {
paskia?: boolean
office_previews?: boolean
},
dialog: '' as '' | 'settings' | 'usermgmt' | 'accessdenied' | 'tokens',
dialog: '' as '' | 'settings' | 'usermgmt' | 'accessdenied' | 'tokens' | 'about',
uprogress: {} as any,
dprogress: {} as any,
prefs: {