curio.
no. 10 · maps

Blueprint map

Streets redrawn as a blue technical plan on a grid.

Install

npx shadcn@latest add https://curio-components.vercel.app/r/map-blueprint.json

First curio component? Also add @import "./curio.css"; to your global CSS. See installation.

Usage

import { MapBlueprint } from '@/components/curio/maps/map-blueprint'

export function Where() {
  return <MapBlueprint lat={38.7223} lon={-9.1393} label="Lisbon, PT" />
}

Props

PropTypeDefaultDescription
lat / lonnumber-The centre of the map.
labelstring-Shown on the map, e.g. "Lisbon, PT".
hrefstring-Where clicking the map goes.
sizenumber260Square size in px.
zoomnumber12 to 13Map zoom level.

Source

This is exactly what the command copies into your project.

components/curio/maps/map-blueprint.tsx
import { MAP_CREDIT, MapTiles } from '../lib/map-tiles'
import { coords, Credit, type MapProps } from './map-pin'

// the street map redrawn as a blue technical plan on a grid
export function MapBlueprint({ lat, lon, label, href, size = 260, zoom = 13 }: MapProps) {
  return (
    <a
      href={href}
      target="_blank"
      rel="noopener noreferrer"
      aria-label={label ? `map of ${label}` : 'map'}
      className="relative block shrink-0 overflow-hidden"
      style={{ width: size, height: size, background: '#0f2a6b' }}
    >
      <div className="absolute inset-0 opacity-90 mix-blend-screen [filter:invert(1)_grayscale(1)_brightness(0.9)]">
        <MapTiles lat={lat} lon={lon} zoom={zoom} size={size} style="street" />
      </div>
      <div className="absolute inset-0" style={{ background: 'rgba(29,43,214,0.55)', mixBlendMode: 'multiply' }} />
      <div
        className="absolute inset-0"
        style={{
          backgroundImage:
            'linear-gradient(rgba(255,255,255,0.12) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.12) 1px, transparent 1px)',
          backgroundSize: '26px 26px',
        }}
      />
      <span className="absolute left-1/2 top-1/2 z-10 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rotate-45 border-2 border-white bg-[#ff5a2e]" />
      <span className="absolute left-2.5 top-2 z-10 font-mono text-[10px] uppercase tracking-[0.08em] text-white/80">
        Sheet 01{label ? ` · ${label}` : ''}
      </span>
      <span className="absolute bottom-2 right-2.5 z-10 font-mono text-[10px] text-white/80">{coords(lat, lon)}</span>
      <Credit text={MAP_CREDIT} light />
    </a>
  )
}
components/curio/lib/map-tiles.tsx
// square map centred on lat/lon, built from esri's free tiles (no api key).
// attribution is required: render MAP_CREDIT somewhere on the map.

export type TileStyle = 'street' | 'light' | 'dark' | 'light_nolabels' | 'dark_nolabels' | 'satellite'

const TILE = 256

const LAYERS: Record<TileStyle, string[]> = {
  street: ['World_Street_Map'],
  light: ['Canvas/World_Light_Gray_Base', 'Canvas/World_Light_Gray_Reference'],
  dark: ['Canvas/World_Dark_Gray_Base', 'Canvas/World_Dark_Gray_Reference'],
  light_nolabels: ['Canvas/World_Light_Gray_Base'],
  dark_nolabels: ['Canvas/World_Dark_Gray_Base'],
  satellite: ['World_Imagery'],
}

export const MAP_CREDIT = '© Esri, HERE, Garmin, OSM'
export const SATELLITE_CREDIT = '© Esri, Maxar, Earthstar'

// web mercator pixel position at a zoom level
export function project(lat: number, lon: number, zoom: number) {
  const n = 2 ** zoom
  const r = (lat * Math.PI) / 180
  return {
    x: ((lon + 180) / 360) * n * TILE,
    y: ((1 - Math.log(Math.tan(r) + 1 / Math.cos(r)) / Math.PI) / 2) * n * TILE,
  }
}

export function MapTiles({
  lat,
  lon,
  zoom,
  size,
  style,
  className = '',
}: {
  lat: number
  lon: number
  zoom: number
  size: number
  style: TileStyle
  className?: string
}) {
  const n = 2 ** zoom
  const { x, y } = project(lat, lon, zoom)
  const left = x - size / 2
  const top = y - size / 2
  const x0 = Math.floor(left / TILE)
  const y0 = Math.floor(top / TILE)
  const x1 = Math.floor((left + size) / TILE)
  const y1 = Math.floor((top + size) / TILE)

  const tiles = []
  for (const layer of LAYERS[style]) {
    for (let tx = x0; tx <= x1; tx++) {
      for (let ty = y0; ty <= y1; ty++) {
        const wrapped = ((tx % n) + n) % n
        tiles.push(
          // eslint-disable-next-line @next/next/no-img-element
          <img
            key={`${layer}-${tx}-${ty}`}
            src={`https://server.arcgisonline.com/ArcGIS/rest/services/${layer}/MapServer/tile/${zoom}/${ty}/${wrapped}`}
            alt=""
            width={TILE}
            height={TILE}
            draggable={false}
            className="absolute max-w-none select-none"
            style={{ left: Math.round(tx * TILE - left), top: Math.round(ty * TILE - top), width: TILE, height: TILE }}
          />,
        )
      }
    }
  }

  return (
    <div className={`absolute inset-0 overflow-hidden ${className}`} aria-hidden="true">
      {tiles}
    </div>
  )
}

/** light tiles normally, dark tiles inside .dark / [data-theme="dark"] */
export function ThemedTiles(props: { lat: number; lon: number; zoom: number; size: number; light: TileStyle; dark: TileStyle }) {
  const { light, dark, ...rest } = props
  return (
    <>
      <MapTiles {...rest} style={light} className="curio-tiles-light" />
      <MapTiles {...rest} style={dark} className="curio-tiles-dark" />
    </>
  )
}
components/curio/maps/map-pin.tsx
// pulsing location pin shared by the map cards
export function MapPin({ color = 'var(--curio-accent)' }: { color?: string }) {
  return (
    <span className="absolute left-1/2 top-1/2 z-10 -translate-x-1/2 -translate-y-1/2">
      <span className="curio-pulse absolute -inset-[10px] rounded-full opacity-40" style={{ background: color }} />
      <span className="relative block h-[18px] w-[18px] rounded-full border-[3px] border-white shadow-[0_2px_8px_rgba(0,0,0,0.35)]" style={{ background: color }} />
    </span>
  )
}

export function Credit({ text, light = false }: { text: string; light?: boolean }) {
  return (
    <span className="absolute bottom-1 left-1.5 z-10 text-[9px] opacity-60" style={{ color: light ? 'rgba(255,255,255,0.85)' : 'var(--curio-muted)' }}>
      {text}
    </span>
  )
}

export type MapProps = {
  lat: number
  lon: number
  /** e.g. "pune, in" */
  label?: string
  /** where clicking the map goes */
  href?: string
  /** square size in px */
  size?: number
  zoom?: number
}

export function coords(lat: number, lon: number) {
  return `${Math.abs(lat).toFixed(2)}°${lat >= 0 ? 'N' : 'S'} ${Math.abs(lon).toFixed(2)}°${lon >= 0 ? 'E' : 'W'}`
}