refactor(ui): shared Logo, centralize theme hotkey, layout and CSS tweaks

This commit is contained in:
Anmoldeep Singh
2026-03-19 12:29:00 +05:30
parent ba39973102
commit 5b1d921cad
12 changed files with 114 additions and 94 deletions

View File

@@ -40,7 +40,7 @@ This project uses [CARTO Basemaps](https://docs.carto.com/faqs/carto-basemaps) w
- **Commercial use**: Requires a CARTO Enterprise license. [Request a demo](https://carto.com/request-live-demo) for pricing details.
- **Non-commercial use**: Free for CARTO grantees under their [basemap terms](https://carto.com/legal/bmap).
- **Alternative**: You can switch to [OpenStreetMap](https://www.openstreetmap.org/) tiles or any other MapLibre-compatible tile provider.
- **Alternative**: You can switch to [OpenStreetMap](https://www.openstreetmap.org/) tiles or any other MapLibre-compatible tile provider (MapTiler, Stadia Maps, etc).
## Contributing

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@ interface IframePreviewProps {
export function IframePreview({ src, title }: IframePreviewProps) {
return (
<div className="relative w-full overflow-hidden rounded-xl border h-(--block-preview-height)">
<div className="relative h-(--block-preview-height) w-full overflow-hidden rounded-xl border">
<iframe src={src} title={title} className="size-full border-0" />
</div>
);

View File

@@ -1,5 +1,5 @@
import Link from "next/link";
import { MapPin } from "lucide-react";
import { Logo } from "./logo";
const footerLinks = {
product: [
@@ -36,12 +36,7 @@ export function Footer() {
<div className="container py-12 md:py-16">
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
<div className="col-span-2 md:col-span-1">
<Link href="/" className="flex w-fit items-center gap-1.5">
<MapPin className="size-4 shrink-0" />
<span className="text-lg font-semibold tracking-tight">
mapcn
</span>
</Link>
<Logo className="w-fit" />
<p className="text-muted-foreground mt-3 max-w-xs text-sm leading-relaxed">
Free & open-source, ready-to-use, customizable map components for
React.

View File

@@ -20,14 +20,14 @@ export function Header({ className }: { className?: string }) {
<header
className={cn("bg-background sticky top-0 z-50 h-14 w-full", className)}
>
<nav className="container flex size-full items-center gap-2">
<nav className="container flex size-full items-center">
<MobileNav />
<Logo className="hidden shrink-0 lg:flex" />
<Logo className="mr-3 hidden shrink-0 lg:flex" />
<MainNav className="hidden lg:flex" />
<div className="ml-auto flex h-4.5 items-center gap-2">
<CommandSearch />
<Separator orientation="vertical" className="hidden md:block" />
<Separator orientation="vertical" className="ml-2 hidden md:block" />
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="sm" asChild>

View File

@@ -1,7 +1,6 @@
import Link from "next/link";
import { MapPin } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "./ui/button";
interface LogoProps {
className?: string;
@@ -10,16 +9,16 @@ interface LogoProps {
export function Logo({ className, onClick }: LogoProps) {
return (
<Button
asChild
size="sm"
variant="ghost"
className={cn("px-2.5 text-base font-semibold", className)}
<Link
href="/"
onClick={onClick}
className={cn(
"flex h-8 items-center gap-1.5 text-lg font-semibold",
className,
)}
>
<Link href="/" onClick={onClick}>
<MapPin />
mapcn
</Link>
</Button>
<MapPin className="size-4" />
mapcn
</Link>
);
}

View File

@@ -12,15 +12,9 @@ export function MainNav({ className, ...props }: React.ComponentProps<"nav">) {
if (!navItems?.length) return null;
return (
<nav className={cn("flex items-center gap-1", className)} {...props}>
<nav className={cn("flex items-center", className)} {...props}>
{navItems.map((item) => (
<Button
key={item.href}
variant="ghost"
asChild
size="sm"
className="px-2.5"
>
<Button key={item.href} variant="ghost" asChild size="sm">
<Link
href={item.href}
className="relative inline-flex items-center gap-1.5"

View File

@@ -46,7 +46,7 @@ function PageHeader({
<section
className={cn(
"container mx-auto flex w-full max-w-6xl flex-col gap-4 py-16 md:py-24 lg:pt-26 lg:pb-24",
"container mx-auto flex w-full max-w-6xl flex-col gap-4 py-16 md:py-20 lg:py-24",
align === "center"
? "items-center text-center"
: "items-start text-left",

View File

@@ -1,12 +1,12 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ComponentProps } from "react";
import * as React from "react";
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
export function ThemeProvider({
function ThemeProvider({
children,
...props
}: ComponentProps<typeof NextThemesProvider>) {
}: React.ComponentProps<typeof NextThemesProvider>) {
return (
<NextThemesProvider
attribute="class"
@@ -15,7 +15,57 @@ export function ThemeProvider({
disableTransitionOnChange
{...props}
>
<ThemeHotkey />
{children}
</NextThemesProvider>
);
}
function isTypingTarget(target: EventTarget | null) {
if (!(target instanceof HTMLElement)) {
return false;
}
return (
target.isContentEditable ||
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.tagName === "SELECT"
);
}
function ThemeHotkey() {
const { resolvedTheme, setTheme } = useTheme();
React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.defaultPrevented || event.repeat) {
return;
}
if (event.metaKey || event.ctrlKey || event.altKey) {
return;
}
if (event.key.toLowerCase() !== "t") {
return;
}
if (isTypingTarget(event.target)) {
return;
}
setTheme(resolvedTheme === "dark" ? "light" : "dark");
}
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [resolvedTheme, setTheme]);
return null;
}
export { ThemeProvider };

View File

@@ -25,27 +25,6 @@ export function ThemeToggle() {
setMounted(true);
}, []);
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
// Don't trigger if typing in an input/textarea
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
(e.target as HTMLElement)?.isContentEditable
) {
return;
}
if ((e.key === "t" || e.key === "T") && !e.metaKey && !e.ctrlKey) {
e.preventDefault();
toggleTheme();
}
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [toggleTheme]);
if (!mounted) {
return <Skeleton className="size-8" />;
}

View File

@@ -45,7 +45,7 @@ function getSystemTheme(): Theme {
function useResolvedTheme(themeProp?: "light" | "dark"): Theme {
const [detectedTheme, setDetectedTheme] = useState<Theme>(
() => getDocumentTheme() ?? getSystemTheme()
() => getDocumentTheme() ?? getSystemTheme(),
);
useEffect(() => {
@@ -146,11 +146,11 @@ type MapProps = {
function DefaultLoader() {
return (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background/50 backdrop-blur-xs">
<div className="bg-background/50 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-xs">
<div className="flex gap-1">
<span className="size-1.5 rounded-full bg-muted-foreground/60 animate-pulse" />
<span className="size-1.5 rounded-full bg-muted-foreground/60 animate-pulse [animation-delay:150ms]" />
<span className="size-1.5 rounded-full bg-muted-foreground/60 animate-pulse [animation-delay:300ms]" />
<span className="bg-muted-foreground/60 size-1.5 animate-pulse rounded-full" />
<span className="bg-muted-foreground/60 size-1.5 animate-pulse rounded-full [animation-delay:150ms]" />
<span className="bg-muted-foreground/60 size-1.5 animate-pulse rounded-full [animation-delay:300ms]" />
</div>
</div>
);
@@ -178,7 +178,7 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
loading = false,
...props
},
ref
ref,
) {
const containerRef = useRef<HTMLDivElement>(null);
const [mapInstance, setMapInstance] = useState<MapLibreGL.Map | null>(null);
@@ -199,7 +199,7 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
dark: styles?.dark ?? defaultStyles.dark,
light: styles?.light ?? defaultStyles.light,
}),
[styles]
[styles],
);
// Expose the map instance to the parent component
@@ -318,14 +318,14 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
map: mapInstance,
isLoaded: isLoaded && isStyleLoaded,
}),
[mapInstance, isLoaded, isStyleLoaded]
[mapInstance, isLoaded, isStyleLoaded],
);
return (
<MapContext.Provider value={contextValue}>
<div
ref={containerRef}
className={cn("relative w-full h-full", className)}
className={cn("relative h-full w-full", className)}
>
{(!isLoaded || loading) && <DefaultLoader />}
{/* SSR-safe: children render only when map is loaded on client */}
@@ -508,7 +508,7 @@ function MarkerContent({ children, className }: MarkerContentProps) {
<div className={cn("relative cursor-pointer", className)}>
{children || <DefaultMarkerIcon />}
</div>,
marker.getElement()
marker.getElement(),
);
}
@@ -580,15 +580,15 @@ function MarkerPopup({
return createPortal(
<div
className={cn(
"relative rounded-md border bg-popover p-3 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
className
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 relative rounded-md border p-3 shadow-md",
className,
)}
>
{closeButton && (
<button
type="button"
onClick={handleClose}
className="absolute top-1 right-1 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
className="ring-offset-background focus:ring-ring absolute top-1 right-1 z-10 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none"
aria-label="Close popup"
>
<X className="h-4 w-4" />
@@ -597,7 +597,7 @@ function MarkerPopup({
)}
{children}
</div>,
container
container,
);
}
@@ -666,13 +666,13 @@ function MarkerTooltip({
return createPortal(
<div
className={cn(
"rounded-md bg-foreground px-2 py-1 text-xs text-background shadow-md animate-in fade-in-0 zoom-in-95",
className
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 rounded-md px-2 py-1 text-xs shadow-md",
className,
)}
>
{children}
</div>,
container
container,
);
}
@@ -699,9 +699,9 @@ function MarkerLabel({
<div
className={cn(
"absolute left-1/2 -translate-x-1/2 whitespace-nowrap",
"text-[10px] font-medium text-foreground",
"text-foreground text-[10px] font-medium",
positionClasses[position],
className
className,
)}
>
{children}
@@ -735,7 +735,7 @@ const positionClasses = {
function ControlGroup({ children }: { children: React.ReactNode }) {
return (
<div className="flex flex-col rounded-md border border-border bg-background shadow-sm overflow-hidden [&>button:not(:last-child)]:border-b [&>button:not(:last-child)]:border-border">
<div className="border-border bg-background [&>button:not(:last-child)]:border-border flex flex-col overflow-hidden rounded-md border shadow-sm [&>button:not(:last-child)]:border-b">
{children}
</div>
);
@@ -758,8 +758,8 @@ function ControlButton({
aria-label={label}
type="button"
className={cn(
"flex items-center justify-center size-8 hover:bg-accent dark:hover:bg-accent/40 transition-colors",
disabled && "opacity-50 pointer-events-none cursor-not-allowed"
"hover:bg-accent dark:hover:bg-accent/40 flex size-8 items-center justify-center transition-colors",
disabled && "pointer-events-none cursor-not-allowed opacity-50",
)}
disabled={disabled}
>
@@ -812,7 +812,7 @@ function MapControls({
(error) => {
console.error("Error getting location:", error);
setWaitingForLocation(false);
}
},
);
}
}, [map, onLocate]);
@@ -832,7 +832,7 @@ function MapControls({
className={cn(
"absolute z-10 flex flex-col gap-1.5",
positionClasses[position],
className
className,
)}
>
{showZoom && (
@@ -1006,15 +1006,15 @@ function MapPopup({
return createPortal(
<div
className={cn(
"relative rounded-md border bg-popover p-3 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
className
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 relative rounded-md border p-3 shadow-md",
className,
)}
>
{closeButton && (
<button
type="button"
onClick={handleClose}
className="absolute top-1 right-1 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
className="ring-offset-background focus:ring-ring absolute top-1 right-1 z-10 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none"
aria-label="Close popup"
>
<X className="h-4 w-4" />
@@ -1023,7 +1023,7 @@ function MapPopup({
)}
{children}
</div>,
container
container,
);
}
@@ -1169,7 +1169,7 @@ function MapRoute({
}
type MapClusterLayerProps<
P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties
P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,
> = {
/** GeoJSON FeatureCollection data or URL to fetch GeoJSON from */
data: string | GeoJSON.FeatureCollection<GeoJSON.Point, P>;
@@ -1186,18 +1186,18 @@ type MapClusterLayerProps<
/** Callback when an unclustered point is clicked */
onPointClick?: (
feature: GeoJSON.Feature<GeoJSON.Point, P>,
coordinates: [number, number]
coordinates: [number, number],
) => void;
/** Callback when a cluster is clicked. If not provided, zooms into the cluster */
onClusterClick?: (
clusterId: number,
coordinates: [number, number],
pointCount: number
pointCount: number,
) => void;
};
function MapClusterLayer<
P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties
P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,
>({
data,
clusterMaxZoom = 14,
@@ -1375,7 +1375,7 @@ function MapClusterLayer<
const handleClusterClick = async (
e: MapLibreGL.MapMouseEvent & {
features?: MapLibreGL.MapGeoJSONFeature[];
}
},
) => {
const features = map.queryRenderedFeatures(e.point, {
layers: [clusterLayerId],
@@ -1387,7 +1387,7 @@ function MapClusterLayer<
const pointCount = feature.properties?.point_count as number;
const coordinates = (feature.geometry as GeoJSON.Point).coordinates as [
number,
number
number,
];
if (onClusterClick) {
@@ -1407,7 +1407,7 @@ function MapClusterLayer<
const handlePointClick = (
e: MapLibreGL.MapMouseEvent & {
features?: MapLibreGL.MapGeoJSONFeature[];
}
},
) => {
if (!onPointClick || !e.features?.length) return;
@@ -1423,7 +1423,7 @@ function MapClusterLayer<
onPointClick(
feature as unknown as GeoJSON.Feature<GeoJSON.Point, P>,
coordinates
coordinates,
);
};

View File

@@ -123,6 +123,9 @@
* {
@apply border-border outline-ring/50;
}
html {
@apply overscroll-y-none;
}
body {
@apply bg-background text-foreground font-sans;
}