Property Amenities Grid
blockreal-estateBuilding and luxury residential amenities matrix featuring categorized lifestyle zones, interactive category tabs, instant keyword search, and detailed feature cards with pool photography thumbnail.
Installation
$pnpm dlx uipkge-ng@latest add property-amenities-grid$npx uipkge-ng@latest add property-amenities-grid$yarn dlx uipkge-ng@latest add property-amenities-grid$bunx uipkge-ng@latest add property-amenities-gridManifest:
https://uipkge.dev/r/angular/property-amenities-grid.jsonInstalls to:src/app/components/blocks/property-amenities-grid/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
AmenityCategoryinterface AmenityCategory {
id: CategoryId
label: string
count: number
icon: typeof Building2
description: string
}npm dependencies
Files installed (2)
src/app/components/blocks/property-amenities-grid/property-amenities-grid.component.ts46.9 kB
// Building and luxury residential amenities matrix: categorized lifestyle zones, // interactive category tabs, instant keyword search, and detailed feature cards with a // pool photography thumbnail. Port of the Vue block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowRight, Building2, Calendar, Car, Check, CircleDot, Clock, Compass, ConciergeBell, Dumbbell, FileText, Flame, HeartPulse, Laptop, LucideAngularModule, MapPin, Package, PawPrint, Search, ShieldCheck, Shirt, Video, Wifi, X, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' type CategoryId = 'all' | 'wellness' | 'work' | 'services' | 'parking' interface AmenityCategory { id: CategoryId label: string count: number icon: typeof Building2 description: string } const categories: AmenityCategory[] = [ { id: 'all', label: 'All Amenities', count: 24, icon: Building2, description: 'Explore the full suite of resident services, wellness facilities, and private lounges.', }, { id: 'wellness', label: 'Wellness & Recreation', count: 4, icon: HeartPulse, description: 'Resort-inspired spaces to rejuvenate body and mind, featuring sky-high fitness and hydrotherapy.', }, { id: 'work', label: 'Work & Productivity', count: 3, icon: Laptop, description: 'High-speed professional environments for focused execution, remote meetings, and collaboration.', }, { id: 'services', label: 'Convenience & Services', count: 3, icon: ConciergeBell, description: 'White-glove 24/7 concierge, contactless cold storage lockers, and on-demand valet care.', }, { id: 'parking', label: 'Parking & Pet Care', count: 2, icon: Car, description: 'Subterranean EV charging and a dedicated pet-care suite.', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-property-amenities-grid, [ui-property-amenities-grid]', standalone: true, // Vue renders the root <section> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiSeparatorComponent, ], template: ` <section data-slot="property-amenities-grid" [class]="rootClass"> <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <!-- Top Section Header --> <div class="flex flex-col items-start justify-between gap-6 lg:flex-row lg:items-end"> <div class="max-w-2xl space-y-3"> <div class="flex flex-wrap items-center gap-2"> <span ui-badge variant="outline" class="border-primary/30 bg-primary/5 text-primary gap-1.5 px-3 py-1 text-xs font-medium" > <lucide-icon [img]="Compass" class="size-3.5" /> {{ badge }} </span> <span ui-badge variant="secondary" class="gap-1 px-2.5 py-1 text-xs font-medium"> <lucide-icon [img]="Building2" class="size-3" /> 24 Total Amenities </span> </div> <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl"> {{ title }} </h2> <p class="text-muted-foreground text-base sm:text-lg"> {{ subtitle }} </p> </div> <!-- Quick Summary Stats Bar --> <div class="grid grid-cols-2 gap-3 sm:grid-cols-4 sm:gap-4"> <div class="bg-card border-border rounded-xl border p-3 shadow-xs"> <p class="text-foreground font-mono text-xl font-bold">24</p> <p class="text-muted-foreground text-xs font-medium">Curated Amenities</p> </div> <div class="bg-card border-border rounded-xl border p-3 shadow-xs"> <p class="text-foreground font-mono text-xl font-bold">4</p> <p class="text-muted-foreground text-xs font-medium">Lifestyle Zones</p> </div> <div class="bg-card border-border rounded-xl border p-3 shadow-xs"> <p class="text-foreground font-mono text-xl font-bold">24/7</p> <p class="text-muted-foreground text-xs font-medium">Concierge Access</p> </div> <div class="bg-card border-border rounded-xl border p-3 shadow-xs"> <p class="text-foreground font-mono text-xl font-bold">100%</p> <p class="text-muted-foreground text-xs font-medium">Pet Friendly</p> </div> </div> </div> <div ui-separator class="my-8"></div> <!-- Interactive Controls: Category Filter Tabs & Quick Search --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <!-- Category Filter Pills --> <div class="flex flex-wrap items-center gap-2"> @for (cat of categoryList; track cat.id) { <button ui-button size="sm" [variant]="activeCategory() === cat.id ? 'default' : 'outline'" class="h-8 gap-1.5 rounded-full px-3.5 text-xs font-medium" (click)="activeCategory.set(cat.id)" > <lucide-icon [img]="cat.icon" class="size-3.5" /> <span>{{ cat.label }}</span> <span [class]="pillCountClass(cat.id)"> {{ cat.count }} </span> </button> } </div> <!-- Keyword Search Input --> <div class="relative w-full sm:w-72"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" /> <input [value]="searchQuery()" (input)="searchQuery.set($any($event.target).value)" type="text" placeholder="Search amenities..." class="bg-card border-border placeholder:text-muted-foreground focus-visible:ring-primary h-9 w-full min-w-0 rounded-lg border pr-8 pl-9 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none" /> @if (searchQuery()) { <button type="button" class="text-muted-foreground hover:text-foreground absolute top-1/2 right-2.5 -translate-y-1/2" aria-label="Clear search" (click)="searchQuery.set('')" > <lucide-icon [img]="X" class="size-3.5" /> </button> } </div> </div> <!-- Search Results Indicator --> @if (isSearching()) { <div class="bg-muted/40 border-border mt-4 flex items-center justify-between rounded-lg border px-4 py-2 text-xs" > <span class="text-muted-foreground"> Showing matching results for <strong class="text-foreground">"{{ searchQuery() }}"</strong> </span> <button type="button" class="text-primary font-medium hover:underline" (click)="searchQuery.set('')" > Reset Filter </button> </div> } <!-- Categories Container --> <div class="mt-10 space-y-14"> <!-- CATEGORY 1: WELLNESS & RECREATION --> @if (showWellness()) { <div class="space-y-6"> <div class="flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center"> <div class="flex items-center gap-3"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="HeartPulse" class="size-5" /> </div> <div> <h3 class="text-foreground text-xl font-bold tracking-tight">Wellness & Recreation</h3> <p class="text-muted-foreground text-xs sm:text-sm"> Resort-inspired spaces to rejuvenate body and mind with skyline views and recovery hydrotherapy. </p> </div> </div> <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs font-medium"> 4 Featured Amenities </span> </div> <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> @if (matchesQuery('Heated Rooftop Infinity Pool pool skyline swim cabana')) { <div ui-card class="group border-border bg-card relative flex flex-col justify-between overflow-hidden shadow-xs transition-colors duration-300 hover:shadow-md md:col-span-2 lg:col-span-2" > <div class="relative h-64 w-full overflow-hidden sm:h-72"> <img src="https://images.unsplash.com/photo-1576013551627-0cc20b96c2a7?w=1200&auto=format&fit=crop&q=80" alt="Heated Rooftop Infinity Pool" class="size-full object-cover transition-transform duration-500 ease-out group-hover:scale-105" /> <div class="absolute inset-0 bg-gradient-to-t from-black/85 via-black/35 to-transparent"></div> <div class="absolute top-4 left-4 flex flex-wrap gap-2"> <span ui-badge class="bg-primary text-primary-foreground border-transparent text-xs font-medium shadow-xs" > Featured Highlight </span> <span ui-badge variant="secondary" class="bg-background/80 text-foreground border-white/20 text-xs font-medium backdrop-blur-md" > Year-Round Heated (84°F) </span> </div> <div class="absolute right-4 bottom-4 left-4 text-white"> <div class="flex items-center gap-1.5 text-xs text-zinc-300"> <lucide-icon [img]="MapPin" class="size-3.5 text-zinc-300" /> <span>Level 42 Sky Deck</span> <span class="mx-1.5">·</span> <lucide-icon [img]="Clock" class="size-3.5 text-zinc-300" /> <span>6:00 AM – 10:00 PM Daily</span> </div> </div> </div> <div ui-card-content class="flex flex-1 flex-col justify-between p-6"> <div class="space-y-2"> <h4 ui-card-title class="text-foreground text-xl font-bold">Heated Rooftop Infinity Pool</h4> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> 50-meter temperature-controlled infinity pool on the 42nd floor featuring private cabanas, teak daybeds, outdoor sun deck, and poolside refreshment bar with 360-degree panoramic skyline views. </p> </div> <div class="border-border mt-6 flex flex-wrap items-center gap-2 border-t pt-4"> <span ui-badge variant="outline" class="bg-muted/30 text-xs font-normal"> <lucide-icon [img]="Check" class="text-success mr-1 size-3" /> Panoramic Views </span> <span ui-badge variant="outline" class="bg-muted/30 text-xs font-normal"> <lucide-icon [img]="Check" class="text-success mr-1 size-3" /> Private Cabanas </span> <span ui-badge variant="outline" class="bg-muted/30 text-xs font-normal"> <lucide-icon [img]="Check" class="text-success mr-1 size-3" /> Towel Valet </span> <span ui-badge variant="outline" class="bg-muted/30 text-xs font-normal"> <lucide-icon [img]="Check" class="text-success mr-1 size-3" /> Poolside Bar </span> </div> </div> </div> } @if (matchesQuery('Equinox-style Fitness Center & Yoga Studio gym pilates workout')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="Dumbbell" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">24/7 Access</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Levels 3 & 4 </div> <h4 ui-card-title class="text-foreground text-lg font-bold"> Equinox-style Fitness Center & Yoga Studio </h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> Multi-level 4,500 sq ft strength and conditioning training facility outfitted with Woodway treadmills, Peloton bikes, Olympic lifting racks, and a dedicated sunlit yoga and reformer Pilates studio. </p> <div class="flex flex-wrap gap-1.5 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Woodway & Peloton</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Reformer Pilates</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Personal Trainers</span> </div> </div> </div> } @if (matchesQuery('Nordic Sauna & Cold Plunge Spa steam therapy recovery')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="Flame" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">Hydrotherapy</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Level 3 Spa Suite </div> <h4 ui-card-title class="text-foreground text-lg font-bold">Nordic Sauna & Cold Plunge Spa</h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> Therapeutic cedarwood dry sauna, aromatic eucalyptus steam room, and dual 48°F contrast therapy cold plunge pools engineered for post-workout muscle recovery and immune resilience. </p> <div class="flex flex-wrap gap-1.5 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">48°F Cold Plunge</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Cedar Dry Sauna</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Eucalyptus Steam</span> </div> </div> </div> } @if (matchesQuery('Private Tennis & Pickleball Court sports rooftop court')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md md:col-span-2 lg:col-span-2" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="CircleDot" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">Rooftop Court</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Level 43 Sports Deck </div> <h4 ui-card-title class="text-foreground text-lg font-bold"> Private Tennis & Pickleball Court </h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> Championship-grade rooftop regulation court with multi-sport cushioned acrylic surface, tournament-spec LED night illumination, automated Spinshot ball machines, and resident league reservations. </p> <div class="flex flex-wrap gap-2 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Tournament LEDs</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Ball Machine Loan</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">App Reservation</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Skyline Backdrop</span> </div> </div> </div> } </div> </div> } <!-- CATEGORY 2: WORK & PRODUCTIVITY --> @if (showWork()) { <div class="space-y-6"> <div class="flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center"> <div class="flex items-center gap-3"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="Laptop" class="size-5" /> </div> <div> <h3 class="text-foreground text-xl font-bold tracking-tight">Work & Productivity</h3> <p class="text-muted-foreground text-xs sm:text-sm"> Executive business facilities, private acoustic pods, and ultra-high-speed fiber network. </p> </div> </div> <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs font-medium"> 3 Featured Amenities </span> </div> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> @if (matchesQuery('Resident Co-Working Lounge with Private Pods workspace zoom office')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="Laptop" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">Quiet Focus</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Level 2 Mezzanine </div> <h4 ui-card-title class="text-foreground text-lg font-bold"> Resident Co-Working Lounge with Private Pods </h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> Spacious communal workspace with Herman Miller ergonomic seating, acoustic focus booths, private soundproof Zoom phone pods, and complimentary artisan espresso and nitro cold brew bar. </p> <div class="flex flex-wrap gap-1.5 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Soundproof Pods</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Herman Miller Chairs</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Espresso Bar</span> </div> </div> </div> } @if (matchesQuery('Executive Conference Room with AV boardroom meeting video presentation')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="Video" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">App Booking</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Level 2 North Wing </div> <h4 ui-card-title class="text-foreground text-lg font-bold"> Executive Conference Room with AV </h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> 16-person board room equipped with 85-inch 4K HDR video conferencing displays, spatial beamforming microphone arrays, digital whiteboard, and integrated presentation hub. </p> <div class="flex flex-wrap gap-1.5 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">16-Seat Capacity</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">4K Video Suite</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Digital Whiteboard</span> </div> </div> </div> } @if (matchesQuery('High-Speed 10 Gbps Fiber WiFi throughout wifi internet mesh')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md sm:col-span-2 lg:col-span-1" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="Wifi" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">Ultra-Fast</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Building-wide Coverage </div> <h4 ui-card-title class="text-foreground text-lg font-bold"> High-Speed 10 Gbps Fiber WiFi throughout </h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> Enterprise-grade symmetrical 10 Gbps redundant fiber backbone with Wi-Fi 7 access points delivering uninterrupted zero-drop roaming across all indoor lounges and outdoor terraces. </p> <div class="flex flex-wrap gap-1.5 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Wi-Fi 7 Mesh</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">10 Gbps Symmetrical</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Zero Roam Handoff</span> </div> </div> </div> } </div> </div> } <!-- CATEGORY 3: CONVENIENCE & SERVICES --> @if (showServices()) { <div class="space-y-6"> <div class="flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center"> <div class="flex items-center gap-3"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="ConciergeBell" class="size-5" /> </div> <div> <h3 class="text-foreground text-xl font-bold tracking-tight">Convenience & Services</h3> <p class="text-muted-foreground text-xs sm:text-sm"> White-glove hospitality, smart package handling, and premium garment care at your fingertips. </p> </div> </div> <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs font-medium"> 3 Featured Amenities </span> </div> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> @if (matchesQuery('24/7 Doorman & Concierge Desk doorman front desk hospitality security')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="ConciergeBell" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">24/7 Attended</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Main Lobby Entrance </div> <h4 ui-card-title class="text-foreground text-lg font-bold">24/7 Doorman & Concierge Desk</h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> Round-the-clock uniform concierge team providing white-glove resident hospitality, verified visitor check-in, key holding, luggage assistance, taxi dispatch, and dining reservations. </p> <div class="flex flex-wrap gap-1.5 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">White-Glove Staff</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Key Holding</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Guest Escort</span> </div> </div> </div> } @if (matchesQuery('Automated Package Lockers & Cold Storage Delivery parcel grocery refrigerated')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="Package" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">Smart Storage</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Lobby Concourse </div> <h4 ui-card-title class="text-foreground text-lg font-bold"> Automated Package Lockers & Cold Storage Delivery </h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> Contactless Luxer One smart package room with digital PIN and barcode pickup alerts, secure oversized parcel rooms, and commercial-grade refrigerated storage for fresh grocery deliveries. </p> <div class="flex flex-wrap gap-1.5 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Cold Grocery Lockers</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">24/7 App QR Pickup</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Oversized Item Vault</span> </div> </div> </div> } @if (matchesQuery('Dry Cleaning Valet & Laundry Services garment tailoring wash')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md sm:col-span-2 lg:col-span-1" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="Shirt" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">Valet Care</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Service Concourse B1 </div> <h4 ui-card-title class="text-foreground text-lg font-bold"> Dry Cleaning Valet & Laundry Services </h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> On-demand eco-friendly dry cleaning drop-off lockers with 24-hour turnaround, garment pressing, shoe shining, tailored alterations, and in-locker contactless delivery return. </p> <div class="flex flex-wrap gap-1.5 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">24h Turnaround</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Eco-Friendly Cleaning</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Tailored Alterations</span> </div> </div> </div> } </div> </div> } <!-- CATEGORY 4: PARKING & PET CARE --> @if (showParking()) { <div class="space-y-6"> <div class="flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center"> <div class="flex items-center gap-3"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="Car" class="size-5" /> </div> <div> <h3 class="text-foreground text-xl font-bold tracking-tight">Parking & Pet Care</h3> <p class="text-muted-foreground text-xs sm:text-sm"> Subterranean EV charging bays and private rooftop pet facilities for four-legged residents. </p> </div> </div> <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs font-medium"> 2 Featured Amenities </span> </div> <div class="grid grid-cols-1 gap-6 md:grid-cols-2"> @if (matchesQuery('Secure Underground Parking with EV Chargers electric vehicle garage stall')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="Car" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">EV Ready</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Levels P1 – P3 Subterranean </div> <h4 ui-card-title class="text-foreground text-lg font-bold"> Secure Underground Parking with EV Chargers </h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> Gated subterranean multi-level garage featuring automated license plate recognition entry gates, assigned private stalls, 48 universal Level 2 EV charging stations, and dedicated bike storage. </p> <div class="flex flex-wrap gap-1.5 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">48 EV Charging Ports</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">LPR Auto-Gate</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Covered Bike Vault</span> </div> </div> </div> } @if (matchesQuery('Private Rooftop Dog Park & Pet Spa Station canine grooming wash animal')) { <div ui-card class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md" > <div ui-card-header class="space-y-3 p-6 pb-2"> <div class="flex items-center justify-between"> <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl"> <lucide-icon [img]="PawPrint" class="size-5" /> </div> <span ui-badge variant="secondary" class="text-xs font-medium">Pet Friendly</span> </div> <div class="space-y-1"> <div class="text-muted-foreground flex items-center gap-1 text-xs"> <lucide-icon [img]="MapPin" class="text-primary size-3" /> Level 44 Sky Deck </div> <h4 ui-card-title class="text-foreground text-lg font-bold"> Private Rooftop Dog Park & Pet Spa Station </h4> </div> </div> <div ui-card-content class="space-y-4 p-6 pt-2"> <p ui-card-description class="text-muted-foreground text-sm leading-relaxed"> Enclosed rooftop canine park with anti-microbial synthetic turf and agility obstacles, paired with an indoor heated pet wash spa featuring stainless steel wash tubs and pro blow dry station. </p> <div class="flex flex-wrap gap-1.5 pt-2"> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Anti-Microbial Turf</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Heated Wash Tubs</span> <span ui-badge variant="outline" class="bg-muted/20 text-xs">Pro Blow Dryers</span> </div> </div> </div> } </div> </div> } </div> <!-- Bottom Callout Card: Resident Keycard App & Tour Booking --> <div ui-card class="bg-muted/30 border-border mt-16 overflow-hidden border shadow-xs"> <div ui-card-content class="flex flex-col items-start justify-between gap-6 p-6 sm:p-8 md:flex-row md:items-center" > <div class="flex items-start gap-4"> <div class="bg-primary/10 text-primary flex size-12 shrink-0 items-center justify-center rounded-xl"> <lucide-icon [img]="ShieldCheck" class="size-6" /> </div> <div class="space-y-1"> <h4 class="text-foreground text-base font-semibold sm:text-lg"> Keyless Resident Access & Mobile App Integration </h4> <p class="text-muted-foreground max-w-2xl text-xs sm:text-sm"> All 24 building amenities are digitally accessible via NFC mobile key, offering instant cabana and conference room reservations, real-time fitness occupancy, and automated guest passes. </p> </div> </div> <div class="flex flex-wrap items-center gap-3"> <button ui-button variant="outline" size="sm" class="gap-1.5 text-xs"> <lucide-icon [img]="FileText" class="size-3.5" /> Amenity Guide PDF </button> <button ui-button size="sm" class="gap-1.5 text-xs"> <lucide-icon [img]="Calendar" class="size-3.5" /> Schedule a Tour <lucide-icon [img]="ArrowRight" class="size-3.5" /> </button> </div> </div> </div> </div> </section> `, }) export class UiPropertyAmenitiesGridComponent { protected readonly ArrowRight = ArrowRight protected readonly Building2 = Building2 protected readonly Calendar = Calendar protected readonly Car = Car protected readonly Check = Check protected readonly CircleDot = CircleDot protected readonly Clock = Clock protected readonly Compass = Compass protected readonly ConciergeBell = ConciergeBell protected readonly Dumbbell = Dumbbell protected readonly FileText = FileText protected readonly Flame = Flame protected readonly HeartPulse = HeartPulse protected readonly Laptop = Laptop protected readonly MapPin = MapPin protected readonly Package = Package protected readonly PawPrint = PawPrint protected readonly Search = Search protected readonly ShieldCheck = ShieldCheck protected readonly Shirt = Shirt protected readonly Video = Video protected readonly Wifi = Wifi protected readonly X = X protected readonly cn = cn protected readonly categoryList = categories @Input() title = 'World-Class Amenities & Services' @Input() subtitle = 'Designed for wellness, productivity, and quiet luxury living.' @Input() badge = 'Building Features & Lifestyle' @Input('class') className?: string readonly activeCategory = signal<CategoryId>('all') readonly searchQuery = signal('') readonly isSearching = computed(() => this.searchQuery().trim().length > 0) get rootClass(): string { return cn('bg-background text-foreground w-full py-12 sm:py-16 lg:py-20', this.className) } pillCountClass(id: CategoryId): string { return cn( 'py-0.2 ml-1 rounded-full px-1.5 font-mono text-xs', this.activeCategory() === id ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-muted text-muted-foreground', ) } matchesQuery(text: string): boolean { if (!this.isSearching()) return true return text.toLowerCase().includes(this.searchQuery().toLowerCase().trim()) } readonly showWellness = computed(() => { if (this.activeCategory() !== 'all' && this.activeCategory() !== 'wellness') return false if (!this.isSearching()) return true return ( this.matchesQuery('Heated Rooftop Infinity Pool') || this.matchesQuery('Equinox-style Fitness Center & Yoga Studio') || this.matchesQuery('Nordic Sauna & Cold Plunge Spa') || this.matchesQuery('Private Tennis & Pickleball Court') || this.matchesQuery('wellness recreation pool gym sauna') ) }) readonly showWork = computed(() => { if (this.activeCategory() !== 'all' && this.activeCategory() !== 'work') return false if (!this.isSearching()) return true return ( this.matchesQuery('Resident Co-Working Lounge with Private Pods') || this.matchesQuery('Executive Conference Room with AV') || this.matchesQuery('High-Speed 10 Gbps Fiber WiFi throughout') || this.matchesQuery('work productivity office wifi conference') ) }) readonly showServices = computed(() => { if (this.activeCategory() !== 'all' && this.activeCategory() !== 'services') return false if (!this.isSearching()) return true return ( this.matchesQuery('24/7 Doorman & Concierge Desk') || this.matchesQuery('Automated Package Lockers & Cold Storage Delivery') || this.matchesQuery('Dry Cleaning Valet & Laundry Services') || this.matchesQuery('convenience services concierge doorman package laundry') ) }) readonly showParking = computed(() => { if (this.activeCategory() !== 'all' && this.activeCategory() !== 'parking') return false if (!this.isSearching()) return true return ( this.matchesQuery('Secure Underground Parking with EV Chargers') || this.matchesQuery('Private Rooftop Dog Park & Pet Spa Station') || this.matchesQuery('parking ev charger pet care dog park') ) }) }src/app/components/blocks/property-amenities-grid/index.ts0.1 kB
export { UiPropertyAmenitiesGridComponent } from './property-amenities-grid.component'
Raw manifest:https://uipkge.dev/r/angular/property-amenities-grid.json