frontend: polish app launch and release experience

This commit is contained in:
archipelago
2026-06-11 00:24:40 -04:00
parent c393b96da3
commit 1a3d726eac
140 changed files with 5930 additions and 920 deletions
@@ -94,4 +94,20 @@ describe('useMarketplaceApp', () => {
const app = getCurrentApp()
expect(app!.description).toEqual({ short: 'Short desc', long: 'Long description' })
})
it('preserves real screenshot metadata', () => {
const { setCurrentApp, getCurrentApp } = useMarketplaceApp()
setCurrentApp({
id: 'test',
screenshots: [
'/screenshots/test-dashboard.png',
{ src: '/screenshots/test-settings.png', alt: 'Settings view' },
],
})
expect(getCurrentApp()!.screenshots).toEqual([
'/screenshots/test-dashboard.png',
{ src: '/screenshots/test-settings.png', alt: 'Settings view' },
])
})
})
@@ -77,10 +77,10 @@ describe('useOnboarding', () => {
localStorage.setItem('neode_onboarding_complete', '1')
const promise = isOnboardingComplete()
await vi.advanceTimersByTimeAsync(2000)
await vi.advanceTimersByTimeAsync(8000)
const result = await promise
expect(result).toBe(true)
})
}, 10000)
})
describe('completeOnboarding', () => {
@@ -0,0 +1,37 @@
import { onBeforeUnmount, watch, type Ref } from 'vue'
let activeLocks = 0
let previousOverflow = ''
function lock() {
if (typeof document === 'undefined') return
if (activeLocks === 0) {
previousOverflow = document.body.style.overflow
document.body.style.overflow = 'hidden'
}
activeLocks += 1
}
function unlock() {
if (typeof document === 'undefined' || activeLocks === 0) return
activeLocks -= 1
if (activeLocks === 0) {
document.body.style.overflow = previousOverflow
previousOverflow = ''
}
}
export function useBodyScrollLock(active: Ref<boolean>) {
watch(
active,
(isActive, wasActive) => {
if (isActive && !wasActive) lock()
if (!isActive && wasActive) unlock()
},
{ immediate: true }
)
onBeforeUnmount(() => {
if (active.value) unlock()
})
}
@@ -0,0 +1,42 @@
import { nextTick, onBeforeUnmount, onMounted, ref, type Ref } from 'vue'
export function useCollapsingHeaderTabs(
headerRef: Ref<HTMLElement | null>,
primaryRef: Ref<HTMLElement | null>,
tabsProbeRef: Ref<HTMLElement | null>,
minSearchWidth = 176
) {
const collapsed = ref(false)
let resizeObserver: ResizeObserver | null = null
function measure() {
const header = headerRef.value
const probe = tabsProbeRef.value
if (!header || !probe) return
const primaryWidth = primaryRef.value?.getBoundingClientRect().width ?? 0
const tabsWidth = probe.getBoundingClientRect().width
const gapWidth = 48
collapsed.value = primaryWidth + tabsWidth + minSearchWidth + gapWidth > header.clientWidth
}
function scheduleMeasure() {
nextTick(() => requestAnimationFrame(measure))
}
onMounted(() => {
scheduleMeasure()
resizeObserver = new ResizeObserver(scheduleMeasure)
if (headerRef.value) resizeObserver.observe(headerRef.value)
if (primaryRef.value) resizeObserver.observe(primaryRef.value)
if (tabsProbeRef.value) resizeObserver.observe(tabsProbeRef.value)
window.addEventListener('resize', scheduleMeasure)
})
onBeforeUnmount(() => {
resizeObserver?.disconnect()
window.removeEventListener('resize', scheduleMeasure)
})
return { collapsed, measure: scheduleMeasure }
}
@@ -16,6 +16,7 @@ export interface MarketplaceAppInfo {
dockerImage: string
/** External web URL for iframe-based web apps (no container needed) */
webUrl?: string
screenshots?: AppScreenshot[]
containerConfig?: {
ports?: string[]
volumes?: string[]
@@ -25,6 +26,11 @@ export interface MarketplaceAppInfo {
}
}
export type AppScreenshot = string | {
src: string
alt?: string
}
// Simple in-memory store for the current marketplace app
const currentMarketplaceApp = ref<MarketplaceAppInfo | null>(null)
@@ -46,6 +52,7 @@ export function useMarketplaceApp() {
s9pkUrl: app.s9pkUrl ?? '',
dockerImage: app.dockerImage ?? '',
webUrl: (app as Record<string, unknown>).webUrl as string | undefined,
screenshots: Array.isArray(app.screenshots) ? app.screenshots : undefined,
containerConfig: (app as Record<string, unknown>).containerConfig as MarketplaceAppInfo['containerConfig'],
}
}