UI-компоненти Легка
Location Pin Card
Location card with map placeholder, animated pin drop, address details, distance badge, and directions button. Pure CSS.
Відкрити в Lab
MCP
css react tailwind svelte vue
Цілі: TS JS HTML React Svelte Vue
Код
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f9fafb;
min-height: 100vh;
padding: 32px 16px;
display: flex;
justify-content: center;
}
.demo {
width: 100%;
max-width: 480px;
}
.section-label {
font-size: 11px;
font-weight: 700;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 10px;
}
.pin-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.pin-map {
height: 180px;
position: relative;
overflow: hidden;
}
.map-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(145deg, #e0e7ff 0%, #c7d2fe 100%);
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.map-grid {
position: absolute;
inset: 0;
background-image: linear-gradient(rgba(255, 255, 255, 0.3) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.3) 1px, transparent 1px);
background-size: 32px 32px;
}
.map-pin {
position: relative;
z-index: 2;
animation: pin-drop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.3s both;
}
@keyframes pin-drop {
from {
transform: translateY(-40px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.pin-ring {
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 16px;
height: 6px;
background: rgba(0, 0, 0, 0.15);
border-radius: 50%;
animation: pin-shadow 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.3s both;
}
@keyframes pin-shadow {
from {
transform: translateX(-50%) scaleX(0);
opacity: 0;
}
to {
transform: translateX(-50%) scaleX(1);
opacity: 1;
}
}
.pin-body {
padding: 16px 20px 20px;
}
.pin-name-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
margin-bottom: 4px;
}
.pin-name {
font-size: 16px;
font-weight: 800;
color: #111827;
}
.pin-distance {
font-size: 12px;
font-weight: 700;
color: #6366f1;
background: #eef2ff;
padding: 3px 8px;
border-radius: 20px;
white-space: nowrap;
}
.pin-addr {
font-size: 13px;
color: #6b7280;
line-height: 1.5;
margin-bottom: 10px;
}
.pin-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 14px;
}
.pin-tag {
font-size: 11px;
font-weight: 600;
background: #f3f4f6;
color: #6b7280;
padding: 3px 8px;
border-radius: 6px;
}
.pin-tag--open {
background: #dcfce7;
color: #166534;
}
.pin-actions {
display: flex;
gap: 8px;
}
.pin-btn {
font-size: 13px;
font-weight: 600;
padding: 8px 16px;
border-radius: 8px;
text-decoration: none;
background: #f3f4f6;
color: #374151;
transition: background 0.15s;
}
.pin-btn:hover {
background: #e5e7eb;
}
.pin-btn--primary {
background: #6366f1;
color: #fff;
}
.pin-btn--primary:hover {
background: #5558e3;
}
/* Branch list */
.branch-list {
display: flex;
flex-direction: column;
gap: 1px;
background: #e5e7eb;
border-radius: 14px;
overflow: hidden;
}
.branch-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: #fff;
}
.branch-icon {
font-size: 18px;
flex-shrink: 0;
}
.branch-info {
flex: 1;
}
.branch-name {
font-size: 14px;
font-weight: 700;
color: #111827;
margin-bottom: 2px;
}
.branch-addr {
font-size: 12px;
color: #9ca3af;
}
.branch-dist {
font-size: 12px;
font-weight: 700;
color: #6b7280;
flex-shrink: 0;
}// Pure CSS animations — no JavaScript required<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Location Pin Card</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h2 class="section-label">Single location</h2>
<div class="pin-card">
<div class="pin-map">
<div class="map-placeholder">
<div class="map-grid"></div>
<div class="map-pin" id="mapPin">
<svg width="24" height="32" viewBox="0 0 24 32" fill="none">
<path d="M12 0C5.373 0 0 5.373 0 12c0 8 12 20 12 20s12-12 12-20c0-6.627-5.373-12-12-12z" fill="#6366f1"/>
<circle cx="12" cy="12" r="5" fill="#fff"/>
</svg>
<div class="pin-ring"></div>
</div>
</div>
</div>
<div class="pin-body">
<div class="pin-info">
<div class="pin-name-row">
<h3 class="pin-name">Acme Headquarters</h3>
<span class="pin-distance">0.4 km</span>
</div>
<p class="pin-addr">350 5th Avenue, Floor 68<br/>New York, NY 10118</p>
<div class="pin-tags">
<span class="pin-tag pin-tag--open">Open now</span>
<span class="pin-tag">Mon–Fri 9–6</span>
<span class="pin-tag">+1 212 555 0100</span>
</div>
</div>
<div class="pin-actions">
<a href="#" class="pin-btn pin-btn--primary">Directions</a>
<a href="#" class="pin-btn">Save</a>
<a href="#" class="pin-btn">Share</a>
</div>
</div>
</div>
<h2 class="section-label" style="margin-top:28px;">Branch list</h2>
<div class="branch-list">
<div class="branch-item">
<div class="branch-icon">📍</div>
<div class="branch-info">
<p class="branch-name">Acme — Manhattan</p>
<p class="branch-addr">350 5th Ave, New York, NY</p>
</div>
<span class="branch-dist">0.4 km</span>
</div>
<div class="branch-item">
<div class="branch-icon">📍</div>
<div class="branch-info">
<p class="branch-name">Acme — Brooklyn</p>
<p class="branch-addr">1 Court Square, LIC, NY</p>
</div>
<span class="branch-dist">3.2 km</span>
</div>
<div class="branch-item">
<div class="branch-icon">📍</div>
<div class="branch-info">
<p class="branch-name">Acme — Hoboken</p>
<p class="branch-addr">88 River St, Hoboken, NJ</p>
</div>
<span class="branch-dist">5.1 km</span>
</div>
</div>
</div>
</body>
</html>import { useState } from "react";
interface Location {
id: number;
name: string;
address: string;
city: string;
country: string;
distance: string;
rating: number;
reviews: number;
open: boolean;
hours: string;
phone: string;
lat: number;
lng: number;
}
const LOCATIONS: Location[] = [
{
id: 1,
name: "HQ — Main Campus",
address: "1 Hacker Way",
city: "Menlo Park, CA 94025",
country: "United States",
distance: "0.2 mi",
rating: 4.8,
reviews: 2340,
open: true,
hours: "Mon–Fri 8am–8pm",
phone: "+1 (650) 555-0101",
lat: 37.4848,
lng: -122.1487,
},
{
id: 2,
name: "Downtown Office",
address: "181 Fremont St, Floor 32",
city: "San Francisco, CA 94105",
country: "United States",
distance: "1.4 mi",
rating: 4.6,
reviews: 871,
open: true,
hours: "Mon–Fri 9am–6pm",
phone: "+1 (415) 555-0199",
lat: 37.7907,
lng: -122.3958,
},
{
id: 3,
name: "Research Lab East",
address: "77 Massachusetts Ave",
city: "Cambridge, MA 02139",
country: "United States",
distance: "2.8 mi",
rating: 4.9,
reviews: 540,
open: false,
hours: "Mon–Thu 9am–5pm",
phone: "+1 (617) 555-0042",
lat: 42.3601,
lng: -71.0942,
},
];
function MapPlaceholder({ lat, lng, pin }: { lat: number; lng: number; pin: string }) {
return (
<div className="relative h-48 bg-[#0d1117] rounded-xl overflow-hidden border border-[#30363d]">
{/* Grid lines */}
<svg className="absolute inset-0 w-full h-full opacity-20" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="mapgrid" width="24" height="24" patternUnits="userSpaceOnUse">
<path d="M 24 0 L 0 0 0 24" fill="none" stroke="#30363d" strokeWidth="0.5" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#mapgrid)" />
{/* Roads */}
<line x1="0" y1="60%" x2="100%" y2="60%" stroke="#21262d" strokeWidth="3" />
<line x1="0" y1="30%" x2="100%" y2="30%" stroke="#21262d" strokeWidth="2" />
<line x1="30%" y1="0" x2="30%" y2="100%" stroke="#21262d" strokeWidth="2" />
<line x1="65%" y1="0" x2="65%" y2="100%" stroke="#21262d" strokeWidth="3" />
<line x1="80%" y1="0" x2="80%" y2="100%" stroke="#21262d" strokeWidth="1.5" />
<rect x="32%" y="40%" width="15%" height="15%" fill="#1c2128" rx="3" />
<rect x="50%" y="20%" width="12%" height="8%" fill="#1c2128" rx="2" />
</svg>
{/* Animated pin */}
<div
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-full"
style={{ animation: "pin-drop 0.6s cubic-bezier(0.34,1.56,0.64,1) both" }}
>
<div className="relative">
<div
className="w-8 h-8 rounded-full rounded-bl-none rotate-45 flex items-center justify-center -rotate-45 shadow-lg"
style={{ background: "#58a6ff" }}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="white"
strokeWidth="2.5"
className="rotate-0"
>
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" />
<circle cx="12" cy="9" r="2.5" fill="white" stroke="none" />
</svg>
</div>
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-2 h-2 bg-[#58a6ff] rounded-full opacity-40 blur-sm scale-150" />
</div>
</div>
{/* Coords badge */}
<div className="absolute bottom-2 right-2 bg-[#161b22]/80 border border-[#30363d] rounded-lg px-2 py-1 font-mono text-[9px] text-[#484f58]">
{lat.toFixed(4)}, {lng.toFixed(4)}
</div>
<style>{`@keyframes pin-drop{from{transform:translate(-50%,-150%) scale(0.5);opacity:0}to{transform:translate(-50%,-100%) scale(1);opacity:1}}`}</style>
</div>
);
}
function Stars({ rating }: { rating: number }) {
return (
<div className="flex items-center gap-0.5">
{[1, 2, 3, 4, 5].map((s) => (
<svg
key={s}
width="10"
height="10"
viewBox="0 0 24 24"
fill={s <= Math.round(rating) ? "#e3b341" : "none"}
stroke="#e3b341"
strokeWidth="2"
>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
))}
<span className="text-[10px] text-[#8b949e] ml-1">
{rating} ({(rating * 100).toLocaleString()})
</span>
</div>
);
}
export default function LocationPinCardRC() {
const [selected, setSelected] = useState(0);
const loc = LOCATIONS[selected];
return (
<div className="min-h-screen bg-[#0d1117] p-6 flex justify-center">
<div className="w-full max-w-[420px] space-y-4">
{/* Location tabs */}
<div className="flex gap-1 bg-[#161b22] border border-[#30363d] rounded-xl p-1">
{LOCATIONS.map((l, i) => (
<button
key={l.id}
onClick={() => setSelected(i)}
className={`flex-1 px-2 py-1.5 rounded-lg text-[11px] font-semibold transition-colors truncate ${
selected === i
? "bg-[#21262d] text-[#e6edf3]"
: "text-[#8b949e] hover:text-[#e6edf3]"
}`}
>
{i === 0 ? "HQ" : i === 1 ? "Downtown" : "Lab"}
</button>
))}
</div>
{/* Card */}
<div className="bg-[#161b22] border border-[#30363d] rounded-2xl overflow-hidden">
<MapPlaceholder lat={loc.lat} lng={loc.lng} pin={loc.name} />
<div className="p-5 space-y-4">
{/* Name + open badge */}
<div className="flex items-start justify-between gap-2">
<h3 className="text-[15px] font-bold text-[#e6edf3] leading-tight">{loc.name}</h3>
<span
className={`flex-shrink-0 text-[10px] font-bold px-2 py-0.5 rounded-full ${
loc.open
? "bg-green-500/10 text-green-400 border border-green-500/20"
: "bg-red-500/10 text-red-400 border border-red-500/20"
}`}
>
{loc.open ? "Open" : "Closed"}
</span>
</div>
<Stars rating={loc.rating} />
{/* Address */}
<div className="space-y-1.5 text-[12px]">
<div className="flex items-start gap-2 text-[#8b949e]">
<svg
className="flex-shrink-0 mt-0.5"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" />
</svg>
<div>
<p>{loc.address}</p>
<p>{loc.city}</p>
<p className="text-[#484f58]">{loc.country}</p>
</div>
</div>
<div className="flex items-center gap-2 text-[#8b949e]">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
<span>{loc.hours}</span>
</div>
<div className="flex items-center gap-2 text-[#8b949e]">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 9.5a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.62 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z" />
</svg>
<span>{loc.phone}</span>
</div>
</div>
{/* Distance + CTA */}
<div className="flex items-center gap-2 pt-1">
<span className="flex items-center gap-1.5 text-[11px] text-[#8b949e] bg-[#21262d] border border-[#30363d] rounded-lg px-2.5 py-1.5">
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" />
</svg>
{loc.distance} away
</span>
<button className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-[#58a6ff] rounded-xl text-[12px] font-bold text-white hover:bg-[#79b8ff] transition-colors">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
>
<polygon points="3 11 22 2 13 21 11 13 3 11" />
</svg>
Get Directions
</button>
</div>
</div>
</div>
</div>
</div>
);
}<script>
const LOCATIONS = [
{
id: 1,
name: "HQ \u2014 Main Campus",
address: "1 Hacker Way",
city: "Menlo Park, CA 94025",
country: "United States",
distance: "0.2 mi",
rating: 4.8,
reviews: 2340,
open: true,
hours: "Mon\u2013Fri 8am\u20138pm",
phone: "+1 (650) 555-0101",
lat: 37.4848,
lng: -122.1487,
},
{
id: 2,
name: "Downtown Office",
address: "181 Fremont St, Floor 32",
city: "San Francisco, CA 94105",
country: "United States",
distance: "1.4 mi",
rating: 4.6,
reviews: 871,
open: true,
hours: "Mon\u2013Fri 9am\u20136pm",
phone: "+1 (415) 555-0199",
lat: 37.7907,
lng: -122.3958,
},
{
id: 3,
name: "Research Lab East",
address: "77 Massachusetts Ave",
city: "Cambridge, MA 02139",
country: "United States",
distance: "2.8 mi",
rating: 4.9,
reviews: 540,
open: false,
hours: "Mon\u2013Thu 9am\u20135pm",
phone: "+1 (617) 555-0042",
lat: 42.3601,
lng: -71.0942,
},
];
let selected = 0;
$: loc = LOCATIONS[selected];
const tabLabels = ["HQ", "Downtown", "Lab"];
</script>
<div class="min-h-screen bg-[#0d1117] p-6 flex justify-center">
<div class="w-full max-w-[420px] space-y-4">
<!-- Location tabs -->
<div class="flex gap-1 bg-[#161b22] border border-[#30363d] rounded-xl p-1">
{#each LOCATIONS as l, i}
<button
on:click={() => (selected = i)}
class="flex-1 px-2 py-1.5 rounded-lg text-[11px] font-semibold transition-colors truncate {selected === i ? 'bg-[#21262d] text-[#e6edf3]' : 'text-[#8b949e]'}"
>
{tabLabels[i]}
</button>
{/each}
</div>
<!-- Card -->
<div class="bg-[#161b22] border border-[#30363d] rounded-2xl overflow-hidden">
<!-- Map placeholder -->
<div class="relative h-48 bg-[#0d1117] rounded-xl overflow-hidden border border-[#30363d]">
<svg class="absolute inset-0 w-full h-full opacity-20" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="mapgrid" width="24" height="24" patternUnits="userSpaceOnUse">
<path d="M 24 0 L 0 0 0 24" fill="none" stroke="#30363d" stroke-width="0.5"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#mapgrid)"/>
<line x1="0" y1="60%" x2="100%" y2="60%" stroke="#21262d" stroke-width="3"/>
<line x1="0" y1="30%" x2="100%" y2="30%" stroke="#21262d" stroke-width="2"/>
<line x1="30%" y1="0" x2="30%" y2="100%" stroke="#21262d" stroke-width="2"/>
<line x1="65%" y1="0" x2="65%" y2="100%" stroke="#21262d" stroke-width="3"/>
<line x1="80%" y1="0" x2="80%" y2="100%" stroke="#21262d" stroke-width="1.5"/>
<rect x="32%" y="40%" width="15%" height="15%" fill="#1c2128" rx="3"/>
<rect x="50%" y="20%" width="12%" height="8%" fill="#1c2128" rx="2"/>
</svg>
<!-- Pin -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-full" style="animation: pin-drop 0.6s cubic-bezier(0.34,1.56,0.64,1) both;">
<div class="relative">
<div class="w-8 h-8 rounded-full rounded-bl-none rotate-45 flex items-center justify-center -rotate-45 shadow-lg" style="background: #58a6ff;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/>
<circle cx="12" cy="9" r="2.5" fill="white" stroke="none"/>
</svg>
</div>
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 w-2 h-2 bg-[#58a6ff] rounded-full opacity-40 blur-sm scale-150"></div>
</div>
</div>
<!-- Coords badge -->
<div class="absolute bottom-2 right-2 bg-[#161b22]/80 border border-[#30363d] rounded-lg px-2 py-1 font-mono text-[9px] text-[#484f58]">
{loc.lat.toFixed(4)}, {loc.lng.toFixed(4)}
</div>
</div>
<div class="p-5 space-y-4">
<!-- Name + open badge -->
<div class="flex items-start justify-between gap-2">
<h3 class="text-[15px] font-bold text-[#e6edf3] leading-tight">{loc.name}</h3>
<span
class="flex-shrink-0 text-[10px] font-bold px-2 py-0.5 rounded-full border {loc.open ? 'bg-green-500/10 text-green-400 border-green-500/20' : 'bg-red-500/10 text-red-400 border-red-500/20'}"
>
{loc.open ? "Open" : "Closed"}
</span>
</div>
<!-- Stars -->
<div class="flex items-center gap-0.5">
{#each [1, 2, 3, 4, 5] as star}
<svg width="10" height="10" viewBox="0 0 24 24"
fill={star <= Math.round(loc.rating) ? "#e3b341" : "none"}
stroke="#e3b341" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
{/each}
<span class="text-[10px] text-[#8b949e] ml-1">{loc.rating} ({(loc.rating * 100).toLocaleString()})</span>
</div>
<!-- Address -->
<div class="space-y-1.5 text-[12px]">
<div class="flex items-start gap-2 text-[#8b949e]">
<svg class="flex-shrink-0 mt-0.5" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/>
</svg>
<div>
<p>{loc.address}</p>
<p>{loc.city}</p>
<p class="text-[#484f58]">{loc.country}</p>
</div>
</div>
<div class="flex items-center gap-2 text-[#8b949e]">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
</svg>
<span>{loc.hours}</span>
</div>
<div class="flex items-center gap-2 text-[#8b949e]">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 9.5a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.62 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
<span>{loc.phone}</span>
</div>
</div>
<!-- Distance + CTA -->
<div class="flex items-center gap-2 pt-1">
<span class="flex items-center gap-1.5 text-[11px] text-[#8b949e] bg-[#21262d] border border-[#30363d] rounded-lg px-2.5 py-1.5">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>
</svg>
{loc.distance} away
</span>
<button class="flex-1 flex items-center justify-center gap-1.5 py-2 bg-[#58a6ff] rounded-xl text-[12px] font-bold text-white hover:bg-[#79b8ff] transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polygon points="3 11 22 2 13 21 11 13 3 11"/>
</svg>
Get Directions
</button>
</div>
</div>
</div>
</div>
</div>
<style>
@keyframes pin-drop {
from { transform: translate(-50%, -150%) scale(0.5); opacity: 0; }
to { transform: translate(-50%, -100%) scale(1); opacity: 1; }
}
</style><script setup>
import { ref, computed } from "vue";
const LOCATIONS = [
{
id: 1,
name: "HQ \u2014 Main Campus",
address: "1 Hacker Way",
city: "Menlo Park, CA 94025",
country: "United States",
distance: "0.2 mi",
rating: 4.8,
reviews: 2340,
open: true,
hours: "Mon\u2013Fri 8am\u20138pm",
phone: "+1 (650) 555-0101",
lat: 37.4848,
lng: -122.1487,
},
{
id: 2,
name: "Downtown Office",
address: "181 Fremont St, Floor 32",
city: "San Francisco, CA 94105",
country: "United States",
distance: "1.4 mi",
rating: 4.6,
reviews: 871,
open: true,
hours: "Mon\u2013Fri 9am\u20136pm",
phone: "+1 (415) 555-0199",
lat: 37.7907,
lng: -122.3958,
},
{
id: 3,
name: "Research Lab East",
address: "77 Massachusetts Ave",
city: "Cambridge, MA 02139",
country: "United States",
distance: "2.8 mi",
rating: 4.9,
reviews: 540,
open: false,
hours: "Mon\u2013Thu 9am\u20135pm",
phone: "+1 (617) 555-0042",
lat: 42.3601,
lng: -71.0942,
},
];
const selected = ref(0);
const loc = computed(() => LOCATIONS[selected.value]);
const tabLabels = ["HQ", "Downtown", "Lab"];
</script>
<template>
<div class="min-h-screen bg-[#0d1117] p-6 flex justify-center">
<div class="w-full max-w-[420px] space-y-4">
<!-- Location tabs -->
<div class="flex gap-1 bg-[#161b22] border border-[#30363d] rounded-xl p-1">
<button
v-for="(l, i) in LOCATIONS"
:key="l.id"
@click="selected = i"
class="flex-1 px-2 py-1.5 rounded-lg text-[11px] font-semibold transition-colors truncate"
:class="selected === i ? 'bg-[#21262d] text-[#e6edf3]' : 'text-[#8b949e] hover:text-[#e6edf3]'"
>
{{ tabLabels[i] }}
</button>
</div>
<!-- Card -->
<div class="bg-[#161b22] border border-[#30363d] rounded-2xl overflow-hidden">
<!-- Map placeholder -->
<div class="relative h-48 bg-[#0d1117] rounded-xl overflow-hidden border border-[#30363d]">
<svg class="absolute inset-0 w-full h-full opacity-20" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="mapgrid" width="24" height="24" patternUnits="userSpaceOnUse">
<path d="M 24 0 L 0 0 0 24" fill="none" stroke="#30363d" stroke-width="0.5"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#mapgrid)"/>
<line x1="0" y1="60%" x2="100%" y2="60%" stroke="#21262d" stroke-width="3"/>
<line x1="0" y1="30%" x2="100%" y2="30%" stroke="#21262d" stroke-width="2"/>
<line x1="30%" y1="0" x2="30%" y2="100%" stroke="#21262d" stroke-width="2"/>
<line x1="65%" y1="0" x2="65%" y2="100%" stroke="#21262d" stroke-width="3"/>
<line x1="80%" y1="0" x2="80%" y2="100%" stroke="#21262d" stroke-width="1.5"/>
<rect x="32%" y="40%" width="15%" height="15%" fill="#1c2128" rx="3"/>
<rect x="50%" y="20%" width="12%" height="8%" fill="#1c2128" rx="2"/>
</svg>
<!-- Pin -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-full pin-drop">
<div class="relative">
<div class="w-8 h-8 rounded-full rounded-bl-none rotate-45 flex items-center justify-center -rotate-45 shadow-lg" style="background: #58a6ff;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/>
<circle cx="12" cy="9" r="2.5" fill="white" stroke="none"/>
</svg>
</div>
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 w-2 h-2 bg-[#58a6ff] rounded-full opacity-40 blur-sm scale-150"></div>
</div>
</div>
<!-- Coords badge -->
<div class="absolute bottom-2 right-2 bg-[#161b22]/80 border border-[#30363d] rounded-lg px-2 py-1 font-mono text-[9px] text-[#484f58]">
{{ loc.lat.toFixed(4) }}, {{ loc.lng.toFixed(4) }}
</div>
</div>
<div class="p-5 space-y-4">
<!-- Name + open badge -->
<div class="flex items-start justify-between gap-2">
<h3 class="text-[15px] font-bold text-[#e6edf3] leading-tight">{{ loc.name }}</h3>
<span
class="flex-shrink-0 text-[10px] font-bold px-2 py-0.5 rounded-full border"
:class="loc.open
? 'bg-green-500/10 text-green-400 border-green-500/20'
: 'bg-red-500/10 text-red-400 border-red-500/20'"
>
{{ loc.open ? 'Open' : 'Closed' }}
</span>
</div>
<!-- Stars -->
<div class="flex items-center gap-0.5">
<svg v-for="star in 5" :key="star" width="10" height="10" viewBox="0 0 24 24"
:fill="star <= Math.round(loc.rating) ? '#e3b341' : 'none'"
stroke="#e3b341" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
<span class="text-[10px] text-[#8b949e] ml-1">{{ loc.rating }} ({{ (loc.rating * 100).toLocaleString() }})</span>
</div>
<!-- Address -->
<div class="space-y-1.5 text-[12px]">
<div class="flex items-start gap-2 text-[#8b949e]">
<svg class="flex-shrink-0 mt-0.5" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/>
</svg>
<div>
<p>{{ loc.address }}</p>
<p>{{ loc.city }}</p>
<p class="text-[#484f58]">{{ loc.country }}</p>
</div>
</div>
<div class="flex items-center gap-2 text-[#8b949e]">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
</svg>
<span>{{ loc.hours }}</span>
</div>
<div class="flex items-center gap-2 text-[#8b949e]">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 9.5a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.62 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
<span>{{ loc.phone }}</span>
</div>
</div>
<!-- Distance + CTA -->
<div class="flex items-center gap-2 pt-1">
<span class="flex items-center gap-1.5 text-[11px] text-[#8b949e] bg-[#21262d] border border-[#30363d] rounded-lg px-2.5 py-1.5">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>
</svg>
{{ loc.distance }} away
</span>
<button class="flex-1 flex items-center justify-center gap-1.5 py-2 bg-[#58a6ff] rounded-xl text-[12px] font-bold text-white hover:bg-[#79b8ff] transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polygon points="3 11 22 2 13 21 11 13 3 11"/>
</svg>
Get Directions
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
@keyframes pin-drop {
from { transform: translate(-50%, -150%) scale(0.5); opacity: 0; }
to { transform: translate(-50%, -100%) scale(1); opacity: 1; }
}
.pin-drop {
animation: pin-drop 0.6s cubic-bezier(0.34,1.56,0.64,1) both;
}
</style>Location card with a CSS map placeholder, animated drop-pin, address block with street/city/country, distance badge, and a directions CTA. Multiple variants — single location, multi-branch list.