UIPackage
Menu

Framework

Change language

Boilerplate repo

Floor Plan Explorer

blockreal-estate

Architectural unit floor plan inspector and residential layout explorer with 2D CAD-styled blueprint canvas, interactive room highlight pins, dimensional callouts, room breakdown specs, and building unit availability table.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/floor-plan-explorer.json
Named registry:npx shadcn-vue@latest add @uipkge/floor-plan-explorerInstalls to:app/components/blocks/floor-plan-explorer/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
classHTMLAttributes['class']optional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

RoomSpec
interface RoomSpec {
  id: string
  name: string
  shortLabel: string
  dimensionsImperial: string
  dimensionsMetric: string
  areaSqFt: number
  areaSqM: number
  flooring: string
  features: string[]
  pinX: number
  pinY: number
  rect: {
    x: number
    y: number
    w: number
    h: number
  }
}
AvailableUnit
interface AvailableUnit {
  unitNumber: string
  floor: number
  view: string
  monthlyRent: number
  sqFt: number
  status: 'available-now' | 'available-soon' | 'leased' | 'under-contract'
  statusLabel: string
  moveInDate: string
  exposure: string
}
FloorPlanData
interface FloorPlanData {
  id: string
  tabKey: string
  tabLabel: string
  marketingName: string
  tier: string
  beds: number
  baths: number
  totalAreaSqFt: number
  totalAreaSqM: number
  interiorSqFt: number
  exteriorSqFt: number
  ceilingHeight: string
  exposure: string
  startingRent: number
  description: string
  highlights: string[]
  rooms: RoomSpec[]
  availableUnits: AvailableUnit[]
}

Files installed (5)

  • app/components/blocks/floor-plan-explorer/FloorPlanExplorer.vue13.2 kB
    <script setup lang="ts">
    import { computed, ref } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import { Bath, Bed, Building2, Check, Download } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
    import AvailableUnitsTable from './AvailableUnitsTable.vue'
    import FloorPlanCanvas from './FloorPlanCanvas.vue'
    import { floorPlans } from './floor-plan-explorer-data'
    import type { AvailableUnit, FloorPlanData, RoomSpec } from './floor-plan-explorer-types'
    
    export type { AvailableUnit, FloorPlanData, RoomSpec } from './floor-plan-explorer-types'
    import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
    
    const props = defineProps<{
      class?: HTMLAttributes['class']
    }>()
    
    const activeTab = ref('plan-b')
    const selectedRoomId = ref<string | null>('living')
    const hoveredRoomId = ref<string | null>(null)
    const selectedUnitNumber = ref<string>('402')
    const showDimensions = ref(true)
    const showFurniture = ref(true)
    const showPins = ref(true)
    const unitSystem = ref<'imperial' | 'metric'>('imperial')
    const isDownloadingPdf = ref(false)
    const downloadSuccess = ref(false)
    const showTourSuccess = ref(false)
    
    const currentPlan = computed(() => floorPlans[activeTab.value] || floorPlans['plan-b'])
    
    const activeRoom = computed(() => {
      if (hoveredRoomId.value) {
        return currentPlan.value.rooms.find((r) => r.id === hoveredRoomId.value) || null
      }
      if (selectedRoomId.value) {
        return currentPlan.value.rooms.find((r) => r.id === selectedRoomId.value) || null
      }
      return currentPlan.value.rooms[0] || null
    })
    
    function handleSelectTab(key: string) {
      activeTab.value = key
      const plan = floorPlans[key]
      if (plan) {
        selectedRoomId.value = plan.rooms[0]?.id || null
        hoveredRoomId.value = null
        selectedUnitNumber.value = plan.availableUnits[0]?.unitNumber || ''
      }
    }
    
    function handleSelectRoom(roomId: string) {
      selectedRoomId.value = roomId === selectedRoomId.value ? null : roomId
    }
    
    function handleHoverRoom(roomId: string | null) {
      hoveredRoomId.value = roomId
    }
    
    function handleSelectUnit(unitNo: string) {
      selectedUnitNumber.value = unitNo
    }
    
    function handleDownloadPdf() {
      isDownloadingPdf.value = true
      downloadSuccess.value = false
      setTimeout(() => {
        isDownloadingPdf.value = false
        downloadSuccess.value = true
        setTimeout(() => {
          downloadSuccess.value = false
        }, 4000)
      }, 900)
    }
    
    function handleScheduleTour() {
      showTourSuccess.value = true
      setTimeout(() => {
        showTourSuccess.value = false
      }, 4500)
    }
    </script>
    
    <template>
      <div data-slot="floor-plan-explorer" :class="cn('bg-background text-foreground w-full space-y-6', props.class)">
        <!-- Header: Building Title, Plan Switcher Tabs, Action Button -->
        <header class="border-border flex flex-col gap-4 border-b pb-6 lg:flex-row lg:items-center lg:justify-between">
          <div class="space-y-1.5">
            <div class="flex items-center gap-2">
              <Badge variant="outline" class="text-muted-foreground gap-1 px-2 py-0.5 font-mono text-xs">
                <Building2 class="text-primary size-3" />
                The Grandview Collection
              </Badge>
              <span class="text-muted-foreground text-xs">Tower West · Residences</span>
            </div>
            <h1 class="text-xl font-semibold tracking-tight sm:text-2xl">
              The Grandview Penthouse Collection · Floor Plan Explorer
            </h1>
            <p class="text-muted-foreground text-xs sm:text-sm">
              Interactive architectural layout inspector, dimensional specifications, and real-time residential unit
              availability.
            </p>
          </div>
    
          <div class="flex flex-wrap items-center gap-3">
            <Button
              aria-label="Download attachment"
              variant="outline"
              size="sm"
              class="h-9 gap-2 text-xs"
              :disabled="isDownloadingPdf"
              @click="handleDownloadPdf"
            >
              <Download v-if="!downloadSuccess" class="text-muted-foreground size-3.5" />
              <Check v-else class="text-success size-3.5" />
              <span>{{
                isDownloadingPdf ? 'Generating PDF...' : downloadSuccess ? 'PDF Downloaded' : 'Download PDF Floor Plan'
              }}</span>
            </Button>
          </div>
        </header>
    
        <!-- Plan Selection Tabs Bar -->
        <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
          <Tabs :model-value="activeTab" class="w-full sm:w-auto" @update:model-value="handleSelectTab(String($event))">
            <TabsList class="bg-muted/60 grid h-10 w-full grid-cols-3 gap-1 p-1 sm:w-auto">
              <TabsTrigger
                value="plan-a"
                class="data-[state=active]:bg-background px-3 text-xs font-medium data-[state=active]:shadow-xs sm:px-4 sm:text-sm"
              >
                Plan A - 1 Bed / 1 Bath
              </TabsTrigger>
              <TabsTrigger
                value="plan-b"
                class="data-[state=active]:bg-background px-3 text-xs font-medium data-[state=active]:shadow-xs sm:px-4 sm:text-sm"
              >
                Plan B - 2 Bed / 2 Bath
              </TabsTrigger>
              <TabsTrigger
                value="plan-c"
                class="data-[state=active]:bg-background px-3 text-xs font-medium data-[state=active]:shadow-xs sm:px-4 sm:text-sm"
              >
                Plan C - 3 Bed Penthouse
              </TabsTrigger>
            </TabsList>
          </Tabs>
    
          <div class="flex items-center gap-2">
            <span class="text-muted-foreground text-xs">Units:</span>
            <div class="border-border bg-muted/30 inline-flex rounded-md border p-0.5">
              <button
                type="button"
                :class="
                  cn(
                    'focus-visible:ring-ring rounded px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
                    unitSystem === 'imperial'
                      ? 'bg-background text-foreground shadow-xs'
                      : 'text-muted-foreground hover:text-foreground',
                  )
                "
                @click="unitSystem = 'imperial'"
              >
                Sq Ft / ft
              </button>
              <button
                type="button"
                :class="
                  cn(
                    'focus-visible:ring-ring rounded px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
                    unitSystem === 'metric'
                      ? 'bg-background text-foreground shadow-xs'
                      : 'text-muted-foreground hover:text-foreground',
                  )
                "
                @click="unitSystem = 'metric'"
              >
                m² / m
              </button>
            </div>
          </div>
        </div>
    
        <!-- 2-Column Inspector: Left 2D CAD Blueprint Canvas, Right Specs & Availability -->
        <div class="grid grid-cols-1 gap-6 lg:grid-cols-12">
          <!-- Left Column: 2D Architectural Layout Canvas (7 cols) -->
          <FloorPlanCanvas
            :plan="currentPlan"
            :active-room="activeRoom"
            :unit-system="unitSystem"
            :show-dimensions="showDimensions"
            :show-furniture="showFurniture"
            :show-pins="showPins"
            :selected-room-id="selectedRoomId"
            :hovered-room-id="hoveredRoomId"
            @toggle-dimensions="showDimensions = !showDimensions"
            @toggle-furniture="showFurniture = !showFurniture"
            @toggle-pins="showPins = !showPins"
            @hover-room="handleHoverRoom"
            @select-room="handleSelectRoom"
          />
    
          <!-- Right Column: Unit Specs, Room Breakdown & Available Units Table (5 cols) -->
          <div class="space-y-6 lg:col-span-5">
            <!-- Selected Plan Header Card with Total Area -->
            <Card class="border-border bg-card shadow-xs">
              <CardHeader class="pb-3">
                <div class="flex flex-wrap items-center justify-between">
                  <Badge variant="secondary" class="font-mono text-xs">
                    {{ currentPlan.tier }}
                  </Badge>
                  <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                    <Bed class="size-3.5" />
                    <span>{{ currentPlan.beds }} Bed</span>
                    <span>·</span>
                    <Bath class="size-3.5" />
                    <span>{{ currentPlan.baths }} Bath</span>
                  </div>
                </div>
                <CardTitle class="mt-1 text-lg font-semibold tracking-tight sm:text-xl">
                  {{ currentPlan.marketingName }}
                </CardTitle>
                <CardDescription class="text-xs leading-relaxed sm:text-sm">
                  {{ currentPlan.description }}
                </CardDescription>
              </CardHeader>
    
              <CardContent class="space-y-4">
                <!-- Total Area Hero Metric Box -->
                <div class="border-border bg-muted/40 rounded-lg border p-4">
                  <p class="text-muted-foreground text-xs font-medium">Total Architectural Living Area</p>
                  <div class="mt-1 flex items-baseline gap-2">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">
                      {{ currentPlan.totalAreaSqFt.toLocaleString() }}
                    </span>
                    <span class="text-muted-foreground text-sm font-medium">Sq Ft</span>
                    <span class="text-muted-foreground/60">/</span>
                    <span class="text-muted-foreground font-mono text-lg font-semibold tabular-nums">
                      {{ currentPlan.totalAreaSqM.toFixed(1) }}
                    </span>
                    <span class="text-muted-foreground text-xs font-medium"></span>
                  </div>
                  <div
                    class="border-border/60 text-muted-foreground mt-2 flex flex-wrap items-center justify-between border-t pt-2 text-xs"
                  >
                    <span
                      >Interior:
                      <strong class="text-foreground font-mono tabular-nums"
                        >{{ currentPlan.interiorSqFt }} sq ft</strong
                      ></span
                    >
                    <span
                      >Terrace:
                      <strong class="text-foreground font-mono tabular-nums"
                        >{{ currentPlan.exteriorSqFt }} sq ft</strong
                      ></span
                    >
                  </div>
                </div>
    
                <!-- Room Breakdown List with Interactive Highlights -->
                <div>
                  <div class="mb-2 flex flex-wrap items-center justify-between">
                    <h4 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                      Room Breakdown & Dimensions
                    </h4>
                    <span class="text-muted-foreground text-xs">{{ currentPlan.rooms.length }} Spaces</span>
                  </div>
    
                  <div class="space-y-1.5">
                    <div
                      v-for="room in currentPlan.rooms"
                      :key="room.id"
                      :class="
                        cn(
                          'flex cursor-pointer items-center justify-between rounded-md border p-2.5 transition-colors duration-150',
                          selectedRoomId === room.id
                            ? 'border-primary bg-primary/5 shadow-xs'
                            : hoveredRoomId === room.id
                              ? 'border-border bg-muted/60'
                              : 'border-border/60 bg-card hover:bg-muted/30',
                        )
                      "
                      @mouseenter="handleHoverRoom(room.id)"
                      @mouseleave="handleHoverRoom(null)"
                      @click="handleSelectRoom(room.id)"
                    >
                      <div class="flex min-w-0 items-center gap-2">
                        <span
                          :class="
                            cn(
                              'size-2 rounded-full transition-colors',
                              selectedRoomId === room.id
                                ? 'bg-primary'
                                : hoveredRoomId === room.id
                                  ? 'bg-primary/70'
                                  : 'bg-muted-foreground/40',
                            )
                          "
                        ></span>
                        <div>
                          <p class="text-foreground text-xs font-medium">{{ room.name }}</p>
                          <p class="text-muted-foreground text-xs">{{ room.flooring }}</p>
                        </div>
                      </div>
    
                      <div class="shrink-0 text-right">
                        <p class="text-foreground font-mono text-xs font-semibold tabular-nums">
                          {{ unitSystem === 'imperial' ? room.dimensionsImperial : room.dimensionsMetric }}
                        </p>
                        <p class="text-muted-foreground font-mono text-xs tabular-nums">
                          {{ unitSystem === 'imperial' ? `${room.areaSqFt} sq ft` : `${room.areaSqM} m²` }}
                        </p>
                      </div>
                    </div>
                  </div>
                </div>
              </CardContent>
            </Card>
    
            <!-- Available Units in Building Table -->
            <AvailableUnitsTable
              :plan="currentPlan"
              :selected-unit-number="selectedUnitNumber"
              :tour-success="showTourSuccess"
              @select-unit="handleSelectUnit"
              @schedule-tour="handleScheduleTour"
            />
          </div>
        </div>
      </div>
    </template>
    
  • app/components/blocks/floor-plan-explorer/FloorPlanCanvas.vue18.7 kB
  • app/components/blocks/floor-plan-explorer/AvailableUnitsTable.vue5.1 kB
  • app/components/blocks/floor-plan-explorer/floor-plan-explorer-data.ts14.6 kB
  • app/components/blocks/floor-plan-explorer/floor-plan-explorer-types.ts1 kB

Raw manifest:https://uipkge.dev/r/vue/floor-plan-explorer.json