
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
popoveruiClick-triggered floating panel anchored to a trigger element. Supports optional localStorage persistence and configurable dismissal (click-outside, escape, manual). Built on reka-ui with collision detection.
Also available for React ->Dialogs capture immediate user focus for critical actions. Sheets slide in from screen edges for deep secondary forms, while Popovers provide lightweight contextual cards.
| Component | Role | Use when |
|---|---|---|
| Dialog Critical confirmations, destructive warnings, or focused single-purpose tasks requiring user resolution. | Modal Window | |
| Sheet Complex edit forms, filter panes, or shopping carts that retain underlying page context. | Off-Canvas Slide-Over | |
| Drawer Mobile bottom sheets, touch-friendly sliding panels, or secondary inspection trays. | Edge Panel / Bottom Sheet | |
| Popovercurrent Small contextual dropdowns, color pickers, date pickers, or inline configuration flyouts. | Contextual Floating Card |
$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/popover.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/popover.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/popover.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/popover.jsonnpx shadcn-vue@latest add @uipkge/popoverInstalls to:app/components/ui/popover/| Name | Type / Values | Default | Required |
|---|---|---|---|
persist | string | boolean | false | optional |
closeBehavior | PopoverCloseBehavior | 'auto' | optional |
Type aliases from this item's source — use them to shape the data you pass in.
PopoverContextinterface PopoverContext {
closeBehavior: Ref<PopoverCloseBehavior>
}<script setup lang="ts">
import type { PopoverRootEmits, PopoverRootProps } from 'reka-ui'
import { PopoverRoot, useForwardPropsEmits } from 'reka-ui'
import { computed, onMounted, provide, ref, toRef, useId, watch } from 'vue'
import { POPOVER_INJECTION_KEY, type PopoverCloseBehavior } from './context'
const props = withDefaults(
defineProps<
PopoverRootProps & {
persist?: string | boolean
closeBehavior?: PopoverCloseBehavior
}
>(),
{
persist: false,
closeBehavior: 'auto',
},
)
const emits = defineEmits<PopoverRootEmits>()
provide(POPOVER_INJECTION_KEY, {
closeBehavior: toRef(props, 'closeBehavior'),
})
const autoId = useId()
const storageKey = computed(() => {
if (props.persist === false) return null
if (props.persist === true) return `uipkge-popover-${autoId}`
return props.persist
})
const localOpen = ref<boolean>(props.defaultOpen ?? false)
onMounted(() => {
if (!storageKey.value || typeof localStorage === 'undefined') return
if (localStorage.getItem(storageKey.value) === '1') {
localOpen.value = true
if (props.open === undefined) {
emits('update:open', true)
}
}
})
watch(
() => (props.open === undefined ? localOpen.value : props.open),
(v) => {
if (!storageKey.value || typeof localStorage === 'undefined') return
if (v) localStorage.setItem(storageKey.value, '1')
else localStorage.removeItem(storageKey.value)
},
)
const forwarded = useForwardPropsEmits(
computed(() => ({
open: props.open,
defaultOpen: props.defaultOpen,
modal: props.modal,
})),
emits,
)
function onUpdateOpen(value: boolean) {
localOpen.value = value
emits('update:open', value)
}
</script>
<template>
<PopoverRoot v-slot="slotProps" data-uipkge data-slot="popover" v-bind="forwarded" @update:open="onUpdateOpen">
<slot v-bind="slotProps" />
</PopoverRoot>
</template>
<script setup lang="ts">
import type { PopoverAnchorProps } from 'reka-ui'
import { PopoverAnchor } from 'reka-ui'
const props = defineProps<PopoverAnchorProps>()
</script>
<template>
<PopoverAnchor data-uipkge data-slot="popover-anchor" v-bind="props">
<slot />
</PopoverAnchor>
</template>
<script setup lang="ts">
import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { PopoverContent, PopoverPortal, useForwardPropsEmits } from 'reka-ui'
import { inject } from 'vue'
import { cn } from '@/lib/utils'
import { POPOVER_INJECTION_KEY } from './context'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(), {
align: 'center',
sideOffset: 4,
})
const emits = defineEmits<PopoverContentEmits>()
const ctx = inject(POPOVER_INJECTION_KEY, null)
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
// Compose so closeBehavior still applies when consumers pass their own handlers.
// Spread after `forwarded` so we replace (not merge-double) the emit wrappers.
function onPointerDownOutside(e: Event) {
const mode = ctx?.closeBehavior.value ?? 'auto'
if (mode === 'esc' || mode === 'manual' || mode === 'none') {
e.preventDefault()
}
emits('pointerDownOutside', e as never)
}
function onEscapeKeyDown(e: KeyboardEvent) {
const mode = ctx?.closeBehavior.value ?? 'auto'
if (mode === 'click-outside' || mode === 'manual' || mode === 'none') {
e.preventDefault()
}
emits('escapeKeyDown', e)
}
</script>
<template>
<PopoverPortal>
<PopoverContent
data-uipkge
data-slot="popover-content"
v-bind="{
...$attrs,
...forwarded,
onPointerDownOutside,
onEscapeKeyDown,
}"
:class="
cn(
'bg-popover text-popover-foreground motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:ease-emphasized motion-safe:data-[state=open]:blur-in-2 motion-safe:data-[state=closed]:blur-out-2 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--reka-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden motion-safe:data-[state=closed]:duration-[var(--dur-exit)] motion-safe:data-[state=open]:duration-200',
props.class,
)
"
>
<slot />
</PopoverContent>
</PopoverPortal>
</template>
<script setup lang="ts">
import type { PopoverTriggerProps } from 'reka-ui'
import { PopoverTrigger } from 'reka-ui'
const props = defineProps<PopoverTriggerProps>()
</script>
<template>
<PopoverTrigger data-uipkge data-slot="popover-trigger" v-bind="props">
<slot />
</PopoverTrigger>
</template>
import type { InjectionKey, Ref } from 'vue'
export type PopoverCloseBehavior = 'auto' | 'click-outside' | 'esc' | 'manual' | 'none'
export interface PopoverContext {
closeBehavior: Ref<PopoverCloseBehavior>
}
export const POPOVER_INJECTION_KEY: InjectionKey<PopoverContext> = Symbol('uipkge-popover')
export { default as Popover } from './Popover.vue'
export { default as PopoverAnchor } from './PopoverAnchor.vue'
export { default as PopoverContent } from './PopoverContent.vue'
export { default as PopoverTrigger } from './PopoverTrigger.vue'
Raw manifest:https://uipkge.dev/r/vue/popover.json