Copy for LLM
MapView
A real moving map for the lens, built on Leaflet (~42KB). Dark raster tiles, locked to follow your position; route + you-are-here marker + focusable photo place markers draw on top. Keyless CARTO dark tiles by default; pass tileUrl for MapTiler/Stadia in production.
Installation
npx @glasskit-ui/cli add map-viewInstall the SDK (it provides GlassViewport, useDpad and the stylesheet), then copy these files into your project:
npm install @glasskit-ui/react// components/lib/utils.tsimport { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge";export type { ClassValue };/** * Merge class names the shadcn way: clsx joins conditionals, tailwind-merge * de-dupes conflicting Tailwind utilities so a consumer's `className` override * wins (e.g. passing `px-2` beats the component's `px-6`). Lens components are * Tailwind utilities + `--gk-*` tokens, so this de-dupe matters. */export function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs));}/** * Accessible name from a free-form `label` prop: the label itself when it's a * plain string, otherwise undefined (a ReactNode can't become an aria-label). */export function stringLabel(label: unknown): string | undefined { return typeof label === "string" ? label : undefined;}// components/glasskit/map-view.tsx"use client";import { useEffect, useRef, useState } from "react";import type { Map as LeafletMap, CircleMarker, Polyline } from "leaflet";import type * as LeafletNS from "leaflet";type Leaflet = typeof LeafletNS;import "leaflet/dist/leaflet.css";import { cn } from "../lib/utils";type LatLng = [number, number]; // [lat, lon]type Place = { at: LatLng; name?: string; image?: string; rating?: string };/** * <MapView> — a real, moving map for the lens, built on Leaflet (~42KB). Dark * raster tiles, locked to follow your position: it recenters smoothly as you * move (no manual pan/zoom — the glasses have no touch). Your route and a * "you are here" marker draw on top in the accent color. * * Tiles default to CARTO's dark basemap (keyless — fine for previews). In * production pass `tileUrl` for your own provider (MapTiler / Stadia free * tier); never ship someone else's key. Leaflet loads via dynamic import, so * the module is SSR-safe. */export function MapView({ center, zoom = 16, route, destination, places, onSelectPlace, tileUrl = "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", attribution = "© OpenStreetMap © CARTO", className,}: { /** Your position [lat, lon] — the map keeps this centered (follow mode). */ center: LatLng; zoom?: number; /** The route polyline as [lat, lon] points. Redraws when it changes. */ route?: LatLng[]; /** Destination [lat, lon] — a white pin. Redraws when it changes. */ destination?: LatLng; /** Photo markers (restaurants, stops): a circular image + name + rating. */ places?: Place[]; /** Fired when a place marker is activated (Enter / click). */ onSelectPlace?: (index: number, place: Place) => void; /** Raster tile template; defaults to CARTO dark (keyless, preview-grade). */ tileUrl?: string; attribution?: string; className?: string;}) { const el = useRef<HTMLDivElement>(null); const mapRef = useRef<LeafletMap | null>(null); const LRef = useRef<Leaflet | null>(null); const accentRef = useRef("#4c8dff"); const youRef = useRef<CircleMarker | null>(null); const routeRef = useRef<Polyline | null>(null); const destRef = useRef<CircleMarker | null>(null); const onSelectRef = useRef(onSelectPlace); onSelectRef.current = onSelectPlace; const [ready, setReady] = useState(false); // Build the map + tiles + focusable place markers once (dynamic import keeps // Leaflet off the server). Route/destination are drawn in the effect below // so they can change. useEffect(() => { let cancelled = false; let cleanup = () => {}; void (async () => { const L = (await import("leaflet")).default; if (cancelled || !el.current || mapRef.current) return; LRef.current = L; const accent = getComputedStyle(el.current).getPropertyValue("--accent").trim() || "#4c8dff"; accentRef.current = accent; const map = L.map(el.current, { center, zoom, zoomControl: false, dragging: false, scrollWheelZoom: false, doubleClickZoom: false, boxZoom: false, keyboard: false, touchZoom: false, }); L.tileLayer(tileUrl, { attribution, subdomains: "abcd", maxZoom: 20, }).addTo(map); // Photo place markers are D-pad-FOCUSABLE buttons: the focus engine walks // them by screen position (arrows), Enter activates → `onSelectPlace` // (the consumer typically routes there). Styling lives in styles.css. const pinEls: HTMLElement[] = []; (places ?? []).forEach((p, i) => { const nameChip = p.name ? `<span class="gk-mapview__pin-name">${esc(p.name)}${ p.rating ? `<span class="gk-mapview__pin-rating">★ ${esc(p.rating)}</span>` : "" }</span>` : ""; const icon = L.divIcon({ className: "gk-mapview__pinwrap", html: `<button type="button" class="focusable gk-mapview__pin" aria-label="${esc( p.name ?? "Place", )}">${nameChip}<span class="gk-mapview__pin-photo" style="background-image:url('${encodeURI( p.image ?? "", )}')"></span></button>`, iconSize: [46, 54], iconAnchor: [23, 54], }); const marker = L.marker(p.at, { icon, interactive: false }).addTo(map); const btn = marker.getElement()?.querySelector("button"); if (btn) { pinEls.push(btn); btn.addEventListener("click", () => { for (const b of pinEls) b.classList.remove("is-selected"); btn.classList.add("is-selected"); onSelectRef.current?.(i, p); }); } }); youRef.current = L.circleMarker(center, { radius: 8, color: "#fff", weight: 2, fillColor: accent, fillOpacity: 1, }).addTo(map); map.invalidateSize(); mapRef.current = map; setReady(true); cleanup = () => { map.remove(); mapRef.current = null; }; })(); return () => { cancelled = true; cleanup(); }; // build once // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Reactive route + destination: redraw whenever they change (once the map // exists). This is what lets "select a place → route appears." useEffect(() => { const map = mapRef.current; const L = LRef.current; if (!map || !L) return; routeRef.current?.remove(); routeRef.current = null; destRef.current?.remove(); destRef.current = null; if (route && route.length > 1) { routeRef.current = L.polyline(route, { color: accentRef.current, weight: 6, opacity: 0.95, lineCap: "round", lineJoin: "round", }).addTo(map); } if (destination) { destRef.current = L.circleMarker(destination, { radius: 7, color: "#fff", weight: 3, fillColor: "#fff", fillOpacity: 1, }).addTo(map); } }, [ready, route, destination]); // Follow: glide to the new position and move the marker when center changes. useEffect(() => { const map = mapRef.current; if (!map) return; map.panTo(center, { animate: true, duration: 0.6 }); youRef.current?.setLatLng(center); }, [center]); return <div ref={el} className={cn("gk-mapview", className)} />;}/** Escape text before it goes into a Leaflet divIcon's HTML string. */function esc(s: string): string { return s.replace( /[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", })[c]!, );}Usage
// keyless CARTO dark tiles (preview-grade); live position follows youconst here = useGeolocation(); // [lat, lon]<MapView center={[here.lat, here.lon]} route={routeLatLngs} destination={[37.7814, -122.4217]} // tileUrl="https://api.maptiler.com/maps/streets-dark/{z}/{x}/{y}.png?key=…"/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
center | [number, number] | — | Your position [lat, lon]. The map follows it. |
zoom | number | 16 | Zoom level. |
route | [number, number][] | — | Route polyline as [lat, lon] points. |
destination | [number, number] | — | Destination pin [lat, lon]. |
places | { at: [number, number]; name?: string; image?: string; rating?: string }[] | — | Photo markers (restaurants, stops): a circular image, name, and rating. Each is a D-pad-focusable button. |
onSelectPlace | (index: number, place: Place) => void | — | Fired when a place marker is activated (Enter / click). |
tileUrl | string | CARTO dark | Raster tile template. Bring your own provider key for production. |
attribution | string | © OpenStreetMap © CARTO | Tile attribution string shown on the map. |
When to use
MapView gives spatial context: a route, cross-streets, where you are among options. When the wearer only needs a heading to a single target, the lighter DirectionArrow is the better call.
Pin
A world-anchored waypoint marker (ring + dot, name + distance above) placed at a projected screen point. You project: derive x from the target's relative bearing (lib/geo), since the platform gives heading + GPS, not 3D pose. Never mirrored under RTL.
Overview
Composed, multi-region surfaces built from the primitives, like a now-playing card, a call screen, a notification, a chat thread, the AI orb, and a media tile. Drop one in and wire your data.