
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Structured create/edit entity form ("New project"): stacked SectionCards for project details (name with live slug preview, description with char counter), configuration (visibility Select, start date, budget), and access (link vs restricted RadioGroup, notification/comment Switch rows) with a sticky-feel footer bar. Create stays disabled until the name is filled; validation hints appear under empty required fields after the first submit attempt.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/form-layouts.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/form-layouts.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/form-layouts.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/form-layouts.jsonnpx shadcn-vue@latest add @uipkge/form-layoutsInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { HTMLAttributes } from 'vue'
import { Globe, Link2, Lock } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { SectionCard } from '@/components/ui/section-card'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
}>(),
{},
)
const DESCRIPTION_LIMIT = 280
const name = ref('')
const description = ref('')
const visibility = ref('internal')
const startDate = ref('')
const budget = ref('')
const access = ref<'link' | 'restricted'>('link')
const notifications = ref(true)
const comments = ref(true)
const submitted = ref(false)
const slug = computed(() => {
const cleaned = name.value
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
return cleaned || 'your-project'
})
const showNameError = computed(() => submitted.value && name.value.trim() === '')
function handleSubmit() {
submitted.value = true
if (name.value.trim() === '') return
}
</script>
<template>
<div data-slot="form-layouts" :class="['max-w-2xl', props.class]">
<form class="space-y-6" novalidate @submit.prevent="handleSubmit">
<SectionCard title="Project details" description="Name your project and describe what it covers.">
<div class="space-y-4">
<div class="space-y-2">
<Label for="fl-name">Name</Label>
<Input
id="fl-name"
v-model="name"
placeholder="Q3 launch site"
autocomplete="off"
:aria-invalid="showNameError || undefined"
/>
<p v-if="showNameError" role="alert" class="text-destructive text-xs">Project name is required.</p>
</div>
<div class="border-border bg-muted/40 flex items-center gap-2 rounded-md border px-3 py-2">
<Globe class="text-muted-foreground size-4 shrink-0" aria-hidden="true" />
<p class="min-w-0 truncate text-xs">
<span class="text-muted-foreground">acme.uipkge.app/</span>
<span class="text-foreground font-medium">{{ slug }}</span>
</p>
</div>
<div class="space-y-2">
<Label for="fl-description">Description</Label>
<Textarea id="fl-description" v-model="description" :rows="3" placeholder="What is this project about?" />
<p
class="text-right text-xs"
:class="description.length > DESCRIPTION_LIMIT ? 'text-destructive' : 'text-muted-foreground'"
>
{{ description.length }}/{{ DESCRIPTION_LIMIT }}
</p>
</div>
</div>
</SectionCard>
<SectionCard title="Configuration" description="Visibility, schedule and budget for the project.">
<div class="grid gap-4 sm:grid-cols-3">
<div class="space-y-2">
<Label for="fl-visibility">Visibility</Label>
<Select v-model="visibility">
<SelectTrigger id="fl-visibility" class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="public">Public</SelectItem>
<SelectItem value="internal">Internal</SelectItem>
<SelectItem value="private">Private</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="fl-start-date">Start date</Label>
<Input id="fl-start-date" v-model="startDate" type="date" />
</div>
<div class="space-y-2">
<Label for="fl-budget">Budget</Label>
<Input id="fl-budget" v-model="budget" type="number" :min="0" :step="500" class="text-left" />
</div>
</div>
</SectionCard>
<SectionCard title="Access" description="Decide who can open the project and what they can do.">
<div class="space-y-5">
<RadioGroup v-model="access" class="gap-3">
<div
class="hover:bg-accent/50 has-data-[state=checked]:border-primary flex items-start gap-3 rounded-lg border p-3 transition-colors"
>
<RadioGroupItem id="fl-access-link" value="link" class="mt-0.5" />
<label for="fl-access-link" class="cursor-pointer select-none">
<span class="flex items-center gap-1.5 text-sm font-medium">
<Link2 class="size-4" aria-hidden="true" />
Anyone with the link
</span>
<span class="text-muted-foreground mt-0.5 block text-xs"> Guests can view without signing in. </span>
</label>
</div>
<div
class="hover:bg-accent/50 has-data-[state=checked]:border-primary flex items-start gap-3 rounded-lg border p-3 transition-colors"
>
<RadioGroupItem id="fl-access-restricted" value="restricted" class="mt-0.5" />
<label for="fl-access-restricted" class="cursor-pointer select-none">
<span class="flex items-center gap-1.5 text-sm font-medium">
<Lock class="size-4" aria-hidden="true" />
Restricted
</span>
<span class="text-muted-foreground mt-0.5 block text-xs">
Only invited members can open the project.
</span>
</label>
</div>
</RadioGroup>
<div class="divide-y rounded-lg border">
<div class="flex items-center justify-between gap-4 p-4">
<div class="min-w-0">
<p class="text-sm font-medium">Notifications</p>
<p class="text-muted-foreground mt-0.5 text-xs">Email the team on status changes and mentions.</p>
</div>
<Switch v-model="notifications" aria-label="Toggle notifications" />
</div>
<div class="flex items-center justify-between gap-4 p-4">
<div class="min-w-0">
<p class="text-sm font-medium">Comments</p>
<p class="text-muted-foreground mt-0.5 text-xs">Let viewers comment on tasks and updates.</p>
</div>
<Switch v-model="comments" aria-label="Toggle comments" />
</div>
</div>
</div>
</SectionCard>
<div
class="bg-background/80 border-border sticky bottom-0 -mx-1 flex items-center justify-end gap-2 rounded-b-xl border-t px-1 py-4 backdrop-blur"
>
<Button type="button" variant="ghost" size="sm">Cancel</Button>
<Button type="submit" size="sm" :disabled="name.trim() === ''">Create project</Button>
</div>
</form>
</div>
</template>
Raw manifest:https://uipkge.dev/r/vue/form-layouts.json