
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
formuiZod-first form block built on TanStack Vue Form. Wires field labels, descriptions, error messages, and validation together; bind a field once and the rest is automatic.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/form.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/form.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/form.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/form.jsonnpx shadcn-vue@latest add @uipkge/formInstalls to:app/components/ui/form/| Name | Type / Values | Default | Required |
|---|---|---|---|
form | TanstackFormApi | — | required |
class | HTMLAttributes['class'] | — | optional |
Type aliases from this item's source — use them to shape the data you pass in.
FieldMetainterface FieldMeta {
errors: Array<unknown>
errorMap?: Record<string, Array<unknown> | undefined>
isDirty: boolean
isTouched: boolean
isValid: boolean
}FieldApiinterface FieldApi {
name: string
state: {
value: unknown
meta: FieldMeta
}
store: {
subscribe: (cb: () => void) => (() => void) | { unsubscribe: () => void }
}
handleChange: (v: unknown) => void
handleBlur: () => void
}FormFieldContextinterface FormFieldContext {
name: string
error: ComputedRef<string | undefined>
valid: ComputedRef<boolean>
isDirty: ComputedRef<boolean>
isTouched: ComputedRef<boolean>
}TanstackFormApiinterface TanstackFormApi {
Field: unknown
handleSubmit: () => void | Promise<void>
}<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { provide } from 'vue'
import { cn } from '@/lib/utils'
import { FORM_INSTANCE_INJECTION_KEY, type TanstackFormApi } from './injectionKeys'
const props = defineProps<{
form: TanstackFormApi
class?: HTMLAttributes['class']
}>()
provide(FORM_INSTANCE_INJECTION_KEY, props.form)
function onSubmit(e: Event) {
e.preventDefault()
e.stopPropagation()
void props.form.handleSubmit()
}
</script>
<template>
<form data-uipkge data-slot="form" :class="cn(props.class)" @submit="onSubmit">
<slot />
</form>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<{
align?: 'left' | 'center' | 'right'
gap?: 'sm' | 'md' | 'lg'
class?: HTMLAttributes['class']
}>(),
{
align: 'right',
gap: 'md',
},
)
const alignClasses = {
left: 'justify-start',
center: 'justify-center',
right: 'justify-end',
}
const gapClasses = {
sm: 'gap-2',
md: 'gap-3',
lg: 'gap-4',
}
</script>
<template>
<div
data-uipkge
data-slot="form-actions"
:class="cn('flex flex-wrap items-center', alignClasses[align], gapClasses[gap], props.class)"
>
<slot />
</div>
</template>
<script lang="ts" setup>
import { Slot } from 'reka-ui'
import { useFormField } from './useFormField'
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
</script>
<template>
<Slot
:id="formItemId"
data-uipkge
data-slot="form-control"
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
:aria-invalid="!!error"
>
<slot />
</Slot>
</template>
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useFormField } from './useFormField'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const { formDescriptionId } = useFormField()
</script>
<template>
<p
:id="formDescriptionId"
data-uipkge
data-slot="form-description"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</p>
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue'
import FormFieldInner from './FormFieldInner.vue'
import { FORM_INSTANCE_INJECTION_KEY, type TanstackFormApi } from './injectionKeys'
const props = defineProps<{
form?: TanstackFormApi
name: string
validators?: Record<string, unknown>
}>()
const injected = inject(FORM_INSTANCE_INJECTION_KEY, null)
const resolvedForm = computed<TanstackFormApi>(() => {
const f = props.form ?? injected
if (!f) throw new Error('<FormField> requires a `form` prop or to be nested inside <Form :form="...">')
return f
})
</script>
<template>
<component :is="resolvedForm.Field" :name="props.name" :validators="props.validators">
<template #default="{ field }">
<FormFieldInner :field="field">
<template #default="binding">
<slot v-bind="binding" />
</template>
</FormFieldInner>
</template>
</component>
</template>
<script lang="ts" setup>
import { computed, onUnmounted, provide, shallowRef, watch } from 'vue'
import { FORM_FIELD_INJECTION_KEY } from './injectionKeys'
interface FieldMeta {
errors: Array<unknown>
errorMap?: Record<string, Array<unknown> | undefined>
isDirty: boolean
isTouched: boolean
isValid: boolean
}
interface FieldApi {
name: string
state: {
value: unknown
meta: FieldMeta
}
store: {
subscribe: (cb: () => void) => (() => void) | { unsubscribe: () => void }
}
handleChange: (v: unknown) => void
handleBlur: () => void
}
const props = defineProps<{ field: FieldApi }>()
const snapshot = shallowRef({ value: props.field.state.value, meta: props.field.state.meta })
let unsubscribe: (() => void) | null = null
function refreshSnapshot() {
snapshot.value = { value: props.field.state.value, meta: props.field.state.meta }
}
function subscribe(field: FieldApi) {
unsubscribe?.()
const subscription = field.store.subscribe(refreshSnapshot)
// TanStack Store supports both legacy callbacks and subscription objects.
unsubscribe = typeof subscription === 'function' ? subscription : () => subscription.unsubscribe()
refreshSnapshot()
}
subscribe(props.field)
watch(
() => props.field,
(next) => subscribe(next),
)
onUnmounted(() => unsubscribe?.())
function formatError(raw: unknown): string {
if (typeof raw === 'string') return raw
if (raw && typeof raw === 'object' && 'message' in raw) {
return String((raw as { message: unknown }).message)
}
return String(raw)
}
const error = computed(() => {
const meta = snapshot.value.meta
const flat = meta.errors
if (Array.isArray(flat) && flat.length > 0 && flat[0] != null) {
return formatError(flat[0])
}
const map = meta.errorMap ?? {}
for (const key of Object.keys(map)) {
const arr = map[key]
if (Array.isArray(arr) && arr.length > 0 && arr[0] != null) {
return formatError(arr[0])
}
}
return undefined
})
const valid = computed(() => snapshot.value.meta.isValid)
const isDirty = computed(() => snapshot.value.meta.isDirty)
const isTouched = computed(() => snapshot.value.meta.isTouched)
provide(FORM_FIELD_INJECTION_KEY, {
name: props.field.name,
error,
valid,
isDirty,
isTouched,
})
const componentField = computed(() => ({
modelValue: snapshot.value.value,
'onUpdate:modelValue': (v: unknown) => props.field.handleChange(v),
onBlur: () => props.field.handleBlur(),
name: props.field.name,
}))
</script>
<template>
<slot
:field="props.field"
:component-field="componentField"
:error="error"
:valid="valid"
:is-dirty="isDirty"
:is-touched="isTouched"
/>
</template>
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { useId } from 'reka-ui'
import { computed, provide } from 'vue'
import { cn } from '@/lib/utils'
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys'
import FormLabel from './FormLabel.vue'
import type { FormStatus } from './types'
const props = defineProps<{
class?: HTMLAttributes['class']
label?: string
required?: boolean
description?: string
status?: FormStatus
help?: string
layout?: 'vertical' | 'horizontal'
labelWidth?: string
}>()
const id = useId()
provide(FORM_ITEM_INJECTION_KEY, id)
const statusBorderClass = computed(() => {
switch (props.status) {
case 'error':
return '[&_input]:border-destructive [&_textarea]:border-destructive [&_button]:border-destructive'
case 'warning':
return '[&_input]:border-warning [&_textarea]:border-warning [&_button]:border-warning'
case 'success':
return '[&_input]:border-success [&_textarea]:border-success [&_button]:border-success'
default:
return ''
}
})
const isHorizontal = computed(() => props.layout === 'horizontal')
</script>
<template>
<div
data-uipkge
data-slot="form-item"
:class="
cn(
'grid gap-1.5',
isHorizontal && 'grid-cols-[var(--label-width,140px)_1fr] items-start gap-x-4 gap-y-0',
props.class,
)
"
:style="labelWidth ? { '--label-width': labelWidth } : undefined"
>
<div v-if="label || $slots.label" class="flex items-center gap-1">
<FormLabel v-if="label" :for="id" :class="status === 'error' ? 'text-destructive' : undefined">{{
label
}}</FormLabel>
<slot name="label" />
<span v-if="required" class="text-destructive text-sm" aria-hidden="true">*</span>
</div>
<div :class="cn('space-y-1', statusBorderClass)">
<slot />
<p v-if="description" class="text-muted-foreground text-xs">{{ description }}</p>
<p
v-if="help"
class="text-xs"
:role="status === 'error' ? 'alert' : undefined"
:class="{
'text-destructive': status === 'error',
'text-warning': status === 'warning',
'text-success': status === 'success',
'text-muted-foreground': !status,
}"
>
{{ help }}
</p>
</div>
</div>
</template>
<script lang="ts" setup>
import type { LabelProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Label } from '@/components/ui/label'
import { useFormField } from './useFormField'
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
const { error, formItemId } = useFormField()
</script>
<template>
<Label
data-uipkge
data-slot="form-label"
:data-error="!!error"
:class="cn('data-[error=true]:text-destructive', props.class)"
:for="formItemId"
>
<slot />
</Label>
</template>
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useFormField } from './useFormField'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const { error, formMessageId } = useFormField()
</script>
<template>
<p
v-if="error"
:id="formMessageId"
data-uipkge
data-slot="form-message"
role="alert"
:class="cn('text-destructive text-sm', props.class)"
>
{{ error }}
</p>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
title?: string
subtitle?: string
description?: string
divider?: boolean
headingLevel?: 'h2' | 'h3' | 'h4' | 'h5'
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div data-uipkge data-slot="form-section" :class="cn('space-y-3', props.class)">
<div v-if="divider || title || subtitle" :class="divider && 'border-t pt-4'">
<div v-if="title || subtitle" class="space-y-1">
<component :is="props.headingLevel ?? 'h4'" v-if="title" class="text-sm font-semibold">{{ title }}</component>
<p v-if="subtitle" class="text-muted-foreground text-xs">{{ subtitle }}</p>
</div>
</div>
<p v-if="description" class="text-muted-foreground text-xs">{{ description }}</p>
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes, Component } from 'vue'
import { CircleAlert, TriangleAlert, CircleCheck } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import type { FormStatus } from './types'
const props = defineProps<{
class?: HTMLAttributes['class']
status: FormStatus
message: string
}>()
const statusConfig: Record<NonNullable<FormStatus>, { icon: Component; container: string }> = {
error: {
icon: CircleAlert,
container: 'bg-destructive/10 text-destructive border-destructive/20',
},
warning: {
icon: TriangleAlert,
container: 'bg-warning/10 text-warning border-warning/30',
},
success: {
icon: CircleCheck,
container: 'bg-success/10 text-success border-success/30',
},
}
</script>
<template>
<div
v-if="status"
data-uipkge
data-slot="form-status"
:role="status === 'error' ? 'alert' : 'status'"
:class="
cn('flex items-center gap-2 rounded-md border px-3 py-2 text-sm', statusConfig[status].container, props.class)
"
>
<component :is="statusConfig[status].icon" class="size-4 shrink-0" aria-hidden="true" />
<span>{{ message }}</span>
</div>
</template>
export { default as Form } from './Form.vue'
export { default as FormField } from './FormField.vue'
export { default as FormControl } from './FormControl.vue'
export { default as FormDescription } from './FormDescription.vue'
export { default as FormItem } from './FormItem.vue'
export { default as FormLabel } from './FormLabel.vue'
export { default as FormMessage } from './FormMessage.vue'
export { default as FormActions } from './FormActions.vue'
export { default as FormSection } from './FormSection.vue'
export { default as FormStatus } from './FormStatus.vue'
export { useFormField } from './useFormField'
export { FORM_ITEM_INJECTION_KEY, FORM_FIELD_INJECTION_KEY } from './injectionKeys'
export type { FormFieldContext } from './injectionKeys'
export type { FormStatus as FormStatusValue } from './types'
export { useForm } from '@tanstack/vue-form'
import type { ComputedRef, InjectionKey } from 'vue'
export const FORM_ITEM_INJECTION_KEY = Symbol() as InjectionKey<string>
export interface FormFieldContext {
name: string
error: ComputedRef<string | undefined>
valid: ComputedRef<boolean>
isDirty: ComputedRef<boolean>
isTouched: ComputedRef<boolean>
}
export const FORM_FIELD_INJECTION_KEY = Symbol() as InjectionKey<FormFieldContext>
export interface TanstackFormApi {
Field: unknown
handleSubmit: () => void | Promise<void>
}
export const FORM_INSTANCE_INJECTION_KEY = Symbol() as InjectionKey<TanstackFormApi>
export type FormStatus = 'error' | 'warning' | 'success' | null | undefined
import { computed, inject } from 'vue'
import { FORM_FIELD_INJECTION_KEY, FORM_ITEM_INJECTION_KEY } from './injectionKeys'
export function useFormField() {
const fieldContext = inject(FORM_FIELD_INJECTION_KEY)
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY)
if (!fieldContext) throw new Error('useFormField should be used within <FormField>')
const id = fieldItemContext
return {
id,
name: computed(() => fieldContext.name),
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
valid: fieldContext.valid,
isDirty: fieldContext.isDirty,
isTouched: fieldContext.isTouched,
error: fieldContext.error,
}
}
Raw manifest:https://uipkge.dev/r/vue/form.json