mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix: mobile UI bugs - overlay touch, sticky header, keyboard sheet, viewport height, reader timeout
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Docker image to run. Pin a sha-* tag or @sha256 digest for stricter deploys.
|
||||
APP_IMAGE=ghcr.io/neonsolstice/bookorbit:latest
|
||||
APP_IMAGE=ghcr.io/bookorbit/bookorbit:latest
|
||||
|
||||
# App publish port on host
|
||||
APP_PORT=3000
|
||||
|
||||
@@ -24,9 +24,9 @@ const viewKey = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarProvider class="app-shell glow-wrapper min-h-screen" :class="backgroundClass">
|
||||
<SidebarProvider class="app-shell glow-wrapper min-h-svh" :class="backgroundClass">
|
||||
<AppSidebar />
|
||||
<SidebarInset class="app-shell-inset flex flex-col h-screen overflow-hidden relative bg-transparent pb-3">
|
||||
<SidebarInset class="app-shell-inset flex flex-col h-svh overflow-hidden relative bg-transparent pb-3">
|
||||
<!-- 1. Global App Header: Fixed at the top, independent of views -->
|
||||
<AppHeader />
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
/**
|
||||
* Tracks the height of the virtual keyboard using the visualViewport API.
|
||||
* On iOS/Android, when the on-screen keyboard opens, the visual viewport shrinks.
|
||||
* Returns the keyboard height in pixels (0 when hidden).
|
||||
*/
|
||||
export function useVirtualKeyboard() {
|
||||
const keyboardHeight = ref(0)
|
||||
|
||||
function update() {
|
||||
if (!window.visualViewport) return
|
||||
const height = Math.max(0, window.innerHeight - window.visualViewport.height - window.visualViewport.offsetTop)
|
||||
keyboardHeight.value = Math.round(height)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!window.visualViewport) return
|
||||
window.visualViewport.addEventListener('resize', update)
|
||||
window.visualViewport.addEventListener('scroll', update)
|
||||
update()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (!window.visualViewport) return
|
||||
window.visualViewport.removeEventListener('resize', update)
|
||||
window.visualViewport.removeEventListener('scroll', update)
|
||||
})
|
||||
|
||||
return { keyboardHeight }
|
||||
}
|
||||
@@ -163,7 +163,7 @@ function openMagicLinkCreate() {
|
||||
<!-- Tab bar (superusers only) -->
|
||||
<div
|
||||
v-if="isSuperuser"
|
||||
class="flex gap-1 mt-1 mb-5 md:mb-6 border-b border-border overflow-x-auto md:overflow-visible md:static sticky top-[5.25rem] z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 snap-x"
|
||||
class="flex gap-1 mt-1 mb-5 md:mb-6 border-b border-border overflow-x-auto md:overflow-visible md:static sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 snap-x"
|
||||
>
|
||||
<button
|
||||
class="px-3 py-3 md:py-2 text-sm font-medium shrink-0 border-b-2 -mb-px transition-colors snap-start"
|
||||
@@ -190,10 +190,7 @@ function openMagicLinkCreate() {
|
||||
</div>
|
||||
|
||||
<!-- Mobile CTA (when no tab bar is shown, keep the sticky button) -->
|
||||
<div
|
||||
v-if="!isSuperuser"
|
||||
class="md:hidden sticky top-[5.25rem] z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2 mt-4 mb-3"
|
||||
>
|
||||
<div v-if="!isSuperuser" class="md:hidden sticky top-0 z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2 mt-4 mb-3">
|
||||
<button class="settings-btn-primary w-full min-h-10 justify-center" @click="openCreate">
|
||||
<UserPlus :size="14" />
|
||||
Create user
|
||||
|
||||
@@ -169,7 +169,7 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden sticky top-[5.25rem] z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2">
|
||||
<div class="md:hidden sticky top-0 z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="settings-btn-primary h-9 flex-1 justify-center" @click="handleSearch">
|
||||
<Search :size="13" />
|
||||
|
||||
@@ -236,7 +236,7 @@ async function handleSetStatus(status: ReadStatus) {
|
||||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
class="group flex flex-col @container"
|
||||
class="group flex flex-col @container touch-manipulation"
|
||||
:class="[selectionMode || (primaryFile && !isMissing) ? 'cursor-pointer' : 'cursor-default', selectionMode ? 'select-none' : '']"
|
||||
@click="handleCardClick"
|
||||
@contextmenu.prevent
|
||||
@@ -364,7 +364,7 @@ async function handleSetStatus(status: ReadStatus) {
|
||||
<div
|
||||
v-if="!selectionMode"
|
||||
class="absolute inset-0 flex flex-col p-2 bg-black/70 transition-opacity duration-150"
|
||||
:class="[showMobileOverlay || 'group-hover:opacity-100 group-active:opacity-100', showMobileOverlay ? 'opacity-100' : 'opacity-0']"
|
||||
:class="[showMobileOverlay || 'group-hover:opacity-100', showMobileOverlay ? 'opacity-100' : 'opacity-0 pointer-events-none']"
|
||||
>
|
||||
<!-- Top row: Quick View -->
|
||||
<div class="shrink-0 flex justify-end">
|
||||
@@ -378,7 +378,7 @@ async function handleSetStatus(status: ReadStatus) {
|
||||
<button
|
||||
v-if="primaryFile && !isMissing"
|
||||
class="size-[30cqi] flex items-center justify-center rounded-full bg-primary text-white shadow-2xl transition-all duration-300 scale-75 hover:scale-110 active:scale-90"
|
||||
:class="[showMobileOverlay || 'group-hover:scale-100 group-active:scale-100', showMobileOverlay ? 'scale-100' : '']"
|
||||
:class="[showMobileOverlay || 'group-hover:scale-100', showMobileOverlay ? 'scale-100' : '']"
|
||||
@click.stop="openFile(primaryFile)"
|
||||
>
|
||||
<component :is="isAudiobook ? Play : BookOpen" class="size-[16cqi]" :class="{ 'ml-[2cqi]': isAudiobook }" />
|
||||
|
||||
@@ -474,7 +474,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
<div class="hidden lg:flex flex-none w-px h-4 bg-border mx-0.5" />
|
||||
|
||||
<button
|
||||
class="hidden lg:flex items-center gap-1.5 h-8 px-2.5 sm:px-3 rounded-lg border border-input bg-background text-sm hover:bg-muted transition-colors disabled:opacity-40"
|
||||
class="flex items-center gap-1.5 h-8 px-2.5 sm:px-3 rounded-lg border border-input bg-background text-sm hover:bg-muted transition-colors disabled:opacity-40"
|
||||
:disabled="!isDirty || saving"
|
||||
@click="reset"
|
||||
>
|
||||
@@ -482,7 +482,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="hidden lg:flex items-center gap-1.5 h-8 px-2.5 sm:px-3 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors disabled:opacity-40"
|
||||
class="flex items-center gap-1.5 h-8 px-2.5 sm:px-3 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors disabled:opacity-40"
|
||||
:disabled="!isDirty || saving"
|
||||
@click="submit"
|
||||
>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { useCollections } from '../composables/useCollections'
|
||||
import IconPicker from '@/components/IconPicker.vue'
|
||||
import type { Collection } from '@bookorbit/types'
|
||||
import { useVirtualKeyboard } from '@/composables/useVirtualKeyboard'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -20,6 +21,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { fetchCollectionsWithMembership, createCollection, addBooksToCollection } = useCollections()
|
||||
const { keyboardHeight } = useVirtualKeyboard()
|
||||
|
||||
const localCollections = ref<Collection[]>([])
|
||||
const addedIds = ref<Set<number>>(new Set())
|
||||
@@ -114,6 +116,7 @@ function handlePointerDownOutside(e: Event) {
|
||||
<SheetContent
|
||||
side="bottom"
|
||||
class="max-h-[80vh] overflow-y-auto sm:inset-x-auto sm:right-auto sm:left-1/2 sm:-translate-x-1/2 sm:w-full sm:max-w-lg sm:rounded-t-lg"
|
||||
:style="keyboardHeight > 0 ? { bottom: `${keyboardHeight}px` } : undefined"
|
||||
@pointer-down-outside="handlePointerDownOutside"
|
||||
>
|
||||
<SheetHeader>
|
||||
|
||||
@@ -100,7 +100,7 @@ onMounted(async () => {
|
||||
<template v-else>
|
||||
<!-- Tab bar -->
|
||||
<div
|
||||
class="flex gap-1 mb-5 md:mb-6 border-b border-border overflow-x-auto md:overflow-visible md:static sticky top-[5.25rem] z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 snap-x"
|
||||
class="flex gap-1 mb-5 md:mb-6 border-b border-border overflow-x-auto md:overflow-visible md:static sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 snap-x"
|
||||
>
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
|
||||
@@ -112,7 +112,7 @@ function availableRecipients(group: EmailGroup) {
|
||||
<div class="md:hidden flex items-center justify-between">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Recipient Groups</p>
|
||||
</div>
|
||||
<div v-if="!showCreate" class="md:hidden sticky top-[8.9rem] z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2">
|
||||
<div v-if="!showCreate" class="md:hidden sticky top-11 z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2">
|
||||
<button
|
||||
class="w-full min-h-10 flex items-center justify-center gap-1.5 px-3 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||
@click="showCreate = true"
|
||||
|
||||
@@ -199,7 +199,7 @@ watch(
|
||||
</div>
|
||||
<div
|
||||
v-if="canManageEmail && !showForm"
|
||||
class="md:hidden sticky top-[8.9rem] z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2"
|
||||
class="md:hidden sticky top-11 z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2"
|
||||
>
|
||||
<button
|
||||
class="w-full min-h-10 flex items-center justify-center gap-1.5 px-3 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||
|
||||
@@ -134,7 +134,7 @@ function deviceLabel(type: string | null): string {
|
||||
<div class="md:hidden flex items-center justify-between">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Recipients</p>
|
||||
</div>
|
||||
<div v-if="!showForm" class="md:hidden sticky top-[8.9rem] z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2 mb-6">
|
||||
<div v-if="!showForm" class="md:hidden sticky top-11 z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2 mb-6">
|
||||
<button
|
||||
class="w-full min-h-10 flex items-center justify-center gap-1.5 px-3 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||
@click="openCreate()"
|
||||
|
||||
@@ -133,7 +133,7 @@ watch(
|
||||
<div class="md:hidden flex items-center justify-between">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Email Templates</p>
|
||||
</div>
|
||||
<div v-if="!showForm" class="md:hidden sticky top-[8.9rem] z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2">
|
||||
<div v-if="!showForm" class="md:hidden sticky top-11 z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2">
|
||||
<button
|
||||
class="w-full min-h-10 flex items-center justify-center gap-1.5 px-3 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||
@click="openCreate()"
|
||||
|
||||
@@ -57,6 +57,8 @@ export function useFoliate(
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
let loadTimeoutId: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
try {
|
||||
await loadScript()
|
||||
|
||||
@@ -81,8 +83,13 @@ export function useFoliate(
|
||||
el.appendChild(view)
|
||||
viewRef.value = view
|
||||
|
||||
// Safety timeout: if the 'load' event never fires (e.g. service worker or
|
||||
// iframe restrictions on iOS), clear the loading state with a helpful message
|
||||
// so the UI doesn't get stuck forever.
|
||||
|
||||
view.addEventListener('load', (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail
|
||||
clearTimeout(loadTimeoutId)
|
||||
loading.value = false
|
||||
// The paginator's internal #view reference is updated in a microtask after the
|
||||
// 'load' event fires. Deferring to a macrotask ensures setStyles targets the
|
||||
@@ -107,10 +114,18 @@ export function useFoliate(
|
||||
view.addEventListener('error', (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail
|
||||
console.error('[foliate] error event', detail)
|
||||
clearTimeout(loadTimeoutId)
|
||||
error.value = detail?.message ?? 'Reader error'
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
loadTimeoutId = setTimeout(() => {
|
||||
if (loading.value) {
|
||||
error.value = 'Could not open the book. Your browser may not fully support the reader. Try refreshing or using a different browser.'
|
||||
loading.value = false
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
if (format === 'epub') {
|
||||
const infoRes = await api(`/api/v1/epub/${bookId}/info?fileId=${fileId}`)
|
||||
if (!infoRes.ok) throw new Error(`Failed to fetch EPUB info: ${infoRes.status}`)
|
||||
@@ -141,6 +156,7 @@ export function useFoliate(
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[useFoliate]', e)
|
||||
clearTimeout(loadTimeoutId)
|
||||
error.value = e instanceof Error ? e.message : 'Failed to open book'
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ onMounted(load)
|
||||
</SettingsPageHeader>
|
||||
<template v-else>
|
||||
<div
|
||||
class="md:hidden sticky top-[5.25rem] z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
class="md:hidden sticky top-11 z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
>
|
||||
<button class="text-xs text-muted-foreground hover:text-foreground transition-colors underline underline-offset-2" @click="reset()">
|
||||
Reset to defaults
|
||||
|
||||
@@ -102,7 +102,7 @@ async function runAuthorBackfillAll() {
|
||||
<template>
|
||||
<p class="settings-group-label">Configuration</p>
|
||||
<div
|
||||
class="md:hidden sticky top-[5.25rem] z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
class="md:hidden sticky top-11 z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<button class="settings-btn-primary flex-1 justify-center" :disabled="saving" @click="saveConfig">
|
||||
|
||||
@@ -29,7 +29,7 @@ onMounted(load)
|
||||
</SettingsPageHeader>
|
||||
<template v-else>
|
||||
<div
|
||||
class="md:hidden sticky top-[5.25rem] z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
class="md:hidden sticky top-11 z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
>
|
||||
<button class="text-xs text-muted-foreground hover:text-foreground transition-colors underline underline-offset-2" @click="reset()">
|
||||
Reset to defaults
|
||||
|
||||
@@ -83,7 +83,7 @@ onUnmounted(() => {
|
||||
</SettingsPageHeader>
|
||||
<template v-else>
|
||||
<div
|
||||
class="md:hidden sticky top-[5.25rem] z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
class="md:hidden sticky top-11 z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
>
|
||||
<button class="text-xs text-muted-foreground hover:text-foreground transition-colors underline underline-offset-2" @click="reset()">
|
||||
Reset to defaults
|
||||
|
||||
@@ -221,7 +221,7 @@ onUnmounted(() => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden sticky top-[5.25rem] z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg p-3 space-y-3">
|
||||
<div class="md:hidden sticky top-0 z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg p-3 space-y-3">
|
||||
<div class="space-y-1.5 pt-1 pb-4 border-border/60">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Upload preview</p>
|
||||
|
||||
@@ -139,7 +139,7 @@ function isExpired(expiresAt: string | null | undefined): boolean {
|
||||
Create shareable login links for shared accounts.
|
||||
</p>
|
||||
</div>
|
||||
<div class="md:hidden sticky top-[5.25rem] z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2 mt-4 mb-3">
|
||||
<div class="md:hidden sticky top-11 z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2 mt-4 mb-3">
|
||||
<button class="settings-btn-primary w-full min-h-10 justify-center" :disabled="sharedUsers.length === 0" @click="openCreateForm">
|
||||
<Plus :size="14" />
|
||||
Create link
|
||||
|
||||
@@ -63,7 +63,7 @@ function selectTab(tab: Tab) {
|
||||
<div
|
||||
:class="[
|
||||
tabWidths[activeTab],
|
||||
'flex gap-1 mb-5 md:mb-6 border-b border-border overflow-x-auto md:overflow-visible md:static sticky top-[5.25rem] z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 snap-x',
|
||||
'flex gap-1 mb-5 md:mb-6 border-b border-border overflow-x-auto md:overflow-visible md:static sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 snap-x',
|
||||
]"
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -132,7 +132,7 @@ function handleResetClick() {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="md:hidden sticky top-[5.25rem] z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
class="md:hidden sticky top-11 z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
|
||||
@@ -255,10 +255,7 @@ watch(
|
||||
<div class="md:hidden flex items-center justify-between mb-2">
|
||||
<p class="settings-group-label mb-0">OPDS Accounts</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="!showCreateForm"
|
||||
class="md:hidden sticky top-[5.25rem] z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2 mb-3"
|
||||
>
|
||||
<div v-if="!showCreateForm" class="md:hidden sticky top-0 z-20 border border-border/60 bg-card/95 backdrop-blur rounded-lg px-3 py-2 mb-3">
|
||||
<button
|
||||
class="w-full min-h-10 flex items-center justify-center gap-1.5 px-3 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||
@click="showCreateForm = true"
|
||||
|
||||
@@ -31,7 +31,7 @@ const showZoom = computed(() => effective.value.zoomMode === 'custom')
|
||||
</SettingsPageHeader>
|
||||
<template v-else>
|
||||
<div
|
||||
class="md:hidden sticky top-[5.25rem] z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
class="md:hidden sticky top-11 z-10 -mx-4 mb-4 px-4 py-2 border-y border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
>
|
||||
<button class="text-xs text-muted-foreground hover:text-foreground transition-colors underline underline-offset-2" @click="reset()">
|
||||
Reset to defaults
|
||||
|
||||
@@ -40,7 +40,7 @@ function selectTab(tab: Tab) {
|
||||
<SettingsPageHeader title="Reader" subtitle="Configure defaults for all reading modes in one place." />
|
||||
|
||||
<div
|
||||
class="flex gap-1 mb-5 md:mb-6 border-b border-border overflow-x-auto md:overflow-visible md:static sticky top-[5.25rem] z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 snap-x"
|
||||
class="flex gap-1 mb-5 md:mb-6 border-b border-border overflow-x-auto md:overflow-visible md:static sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 snap-x"
|
||||
>
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
|
||||
+1
-3
@@ -101,9 +101,7 @@ function handleResetToDefault() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="md:hidden sticky top-[5.25rem] z-10 mb-0 px-3 py-2 border-b border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75"
|
||||
>
|
||||
<div class="md:hidden sticky top-11 z-10 mb-0 px-3 py-2 border-b border-border/70 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
class="settings-btn h-8 px-3 flex items-center gap-1.5 text-xs font-medium text-muted-foreground border border-border rounded-md hover:text-destructive hover:border-destructive/50 hover:bg-destructive/5 transition-colors disabled:opacity-40"
|
||||
|
||||
@@ -20,6 +20,7 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
allowedHosts: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
|
||||
Reference in New Issue
Block a user