feat(deck): mobile support — scrollable slides, swipe, desktop notice
Content-heavy slides were clipped on phones (fixed h-dvh + centered overflow-hidden collided with the top/bottom bars). SlideFrame now scrolls and top-aligns below md (clearing the header/controls), while desktop keeps the centered, full-bleed look unchanged. Add horizontal touch-swipe navigation (vertical scroll still works). Add a shadcn-vue Alert component and show a mobile-only "Best viewed on desktop" notice on the home slide; keyboard hint is desktop-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9b06e69f06
commit
5cdbb76ab7
7 changed files with 94 additions and 4 deletions
16
src/components/ui/alert/Alert.vue
Normal file
16
src/components/ui/alert/Alert.vue
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type AlertVariants, alertVariants } from '.'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
variant?: AlertVariants['variant']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-slot="alert" role="alert" :class="cn(alertVariants({ variant }), props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
12
src/components/ui/alert/AlertDescription.vue
Normal file
12
src/components/ui/alert/AlertDescription.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-slot="alert-description" :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
12
src/components/ui/alert/AlertTitle.vue
Normal file
12
src/components/ui/alert/AlertTitle.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h5 data-slot="alert-title" :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
|
||||
<slot />
|
||||
</h5>
|
||||
</template>
|
||||
23
src/components/ui/alert/index.ts
Normal file
23
src/components/ui/alert/index.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
export { default as Alert } from './Alert.vue'
|
||||
export { default as AlertTitle } from './AlertTitle.vue'
|
||||
export { default as AlertDescription } from './AlertDescription.vue'
|
||||
|
||||
export const alertVariants = cva(
|
||||
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type AlertVariants = VariantProps<typeof alertVariants>
|
||||
|
|
@ -49,6 +49,22 @@ const slides = [
|
|||
const { index, direction, goto, next, prev, isFirst, isLast } = useDeck(slides.length)
|
||||
const current = computed(() => slides[index.value])
|
||||
const transitionName = computed(() => (direction.value === 1 ? 'slide-next' : 'slide-prev'))
|
||||
|
||||
// swipe navigation (touch) — horizontal only, so vertical scrolling still works
|
||||
let touchX = 0
|
||||
let touchY = 0
|
||||
function onTouchStart(e: TouchEvent) {
|
||||
touchX = e.changedTouches[0].clientX
|
||||
touchY = e.changedTouches[0].clientY
|
||||
}
|
||||
function onTouchEnd(e: TouchEvent) {
|
||||
const dx = e.changedTouches[0].clientX - touchX
|
||||
const dy = e.changedTouches[0].clientY - touchY
|
||||
if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) * 1.5) {
|
||||
if (dx < 0) next()
|
||||
else prev()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -75,7 +91,7 @@ const transitionName = computed(() => (direction.value === 1 ? 'slide-next' : 's
|
|||
</header>
|
||||
|
||||
<!-- slide stage -->
|
||||
<main class="h-full w-full">
|
||||
<main class="h-full w-full" @touchstart.passive="onTouchStart" @touchend.passive="onTouchEnd">
|
||||
<Transition :name="transitionName" mode="out-in">
|
||||
<KeepAlive>
|
||||
<component :is="current.component" :key="current.id" class="h-full w-full" />
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ defineProps<{
|
|||
|
||||
<template>
|
||||
<section
|
||||
class="mx-auto flex h-full w-full max-w-6xl flex-col px-8 py-14 sm:px-14 lg:px-20"
|
||||
:class="center ? 'items-center justify-center text-center' : 'justify-center'"
|
||||
class="mx-auto flex h-full w-full max-w-6xl flex-col overflow-y-auto px-5 pt-20 pb-24 md:overflow-visible md:px-14 md:py-14 lg:px-20"
|
||||
:class="[center ? 'items-center text-center' : '', 'justify-start md:justify-center']"
|
||||
>
|
||||
<p
|
||||
v-if="kicker"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { Monitor } from '@lucide/vue'
|
||||
import SlideFrame from '../components/SlideFrame.vue'
|
||||
import PlaceholderBadge from '../components/PlaceholderBadge.vue'
|
||||
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||
import { brand, surfaceEntities, accentText } from '../data'
|
||||
</script>
|
||||
|
||||
|
|
@ -40,7 +42,16 @@ import { brand, surfaceEntities, accentText } from '../data'
|
|||
Raising now · {{ brand.firstNode }}
|
||||
</span>
|
||||
|
||||
<p class="mt-10 text-xs text-fg-muted">
|
||||
<!-- mobile-only: this deck is built for desktop -->
|
||||
<Alert class="mt-8 max-w-sm border-amber-400/40 bg-amber-400/10 text-left md:hidden">
|
||||
<Monitor class="size-4" />
|
||||
<AlertTitle>Best viewed on desktop</AlertTitle>
|
||||
<AlertDescription class="text-fg-muted">
|
||||
This investment deck is designed for a larger screen. Swipe to browse the highlights.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<p class="mt-10 hidden text-xs text-fg-muted md:block">
|
||||
Use <kbd class="rounded border border-line-2 px-1.5 py-0.5">→</kbd> /
|
||||
<kbd class="rounded border border-line-2 px-1.5 py-0.5">←</kbd> to navigate
|
||||
</p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue