diff --git a/apps/web/public/data/styles/dark-matter-neutral.json b/apps/web/public/data/styles/dark-matter-neutral.json index 6b580da..60b5830 100644 --- a/apps/web/public/data/styles/dark-matter-neutral.json +++ b/apps/web/public/data/styles/dark-matter-neutral.json @@ -5894,5 +5894,9 @@ } ], "id": "voyager", - "owner": "Carto" + "owner": "Carto", + "transition": { + "duration": 0, + "delay": 0 + } } \ No newline at end of file diff --git a/apps/web/public/data/styles/positron-neutral.json b/apps/web/public/data/styles/positron-neutral.json index 5067893..9007dc9 100644 --- a/apps/web/public/data/styles/positron-neutral.json +++ b/apps/web/public/data/styles/positron-neutral.json @@ -5889,5 +5889,9 @@ } ], "id": "voyager", - "owner": "Carto" + "owner": "Carto", + "transition": { + "duration": 0, + "delay": 0 + } } \ No newline at end of file diff --git a/apps/web/scripts/process-map-styles.ts b/apps/web/scripts/process-map-styles.ts index ca85b6a..6f78f42 100644 --- a/apps/web/scripts/process-map-styles.ts +++ b/apps/web/scripts/process-map-styles.ts @@ -24,6 +24,7 @@ interface StyleSpecification { sprite?: string; glyphs?: string; layers?: Record[]; + transition?: { duration?: number; delay?: number }; } function parseColor( @@ -90,6 +91,9 @@ function neutralizeStyle( theme: "dark" | "light", ): StyleSpecification { const neutralized = { ...style }; + // Theme switches swap styles behind a hidden map canvas. The default 300ms + // paint transition would still be running when the canvas fades back in. + neutralized.transition = { duration: 0, delay: 0 }; if (neutralized.layers) { neutralized.layers = neutralized.layers.map((layer) => { diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 1905b3b..8bd250c 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -40,6 +40,7 @@ --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); + --color-mark: var(--mark); --radius-sm: calc(var(--radius) * 0.6); --radius-md: calc(var(--radius) * 0.8); --radius-lg: var(--radius); @@ -82,6 +83,7 @@ --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); + --mark: #f9452d; } .dark { @@ -116,6 +118,7 @@ --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.556 0 0); + --mark: #e1f435; } @layer base { @@ -195,6 +198,51 @@ body::after { } } +/* Staggered entrance. Each element sets its own --enter-delay; the + `backwards` fill keeps it hidden during the delay without leaving a + transform or filter behind once the animation ends. */ +@keyframes enter-fx { + from { + opacity: 0; + filter: blur(6px); + transform: translateY(14px); + } +} + +@keyframes enter-fade { + from { + opacity: 0; + } +} + +.enter-fx { + animation: enter-fx 0.6s cubic-bezier(0.16, 1, 0.3, 1) var(--enter-delay, 0s) + backwards; +} + +@media (prefers-reduced-motion: reduce) { + .enter-fx { + animation-name: enter-fade; + animation-duration: 0.2s; + } +} + +/* Fades the hero map out toward its left, top and bottom edges. A mask + rather than overlays in the background color, so whatever lies behind + (the page glow included) shows through unchanged. */ +@utility map-fade { + mask-image: + linear-gradient(to right, transparent, rgb(0 0 0 / 0.3) 11%, #000 22%), + linear-gradient( + to bottom, + transparent, + #000 4rem, + #000 calc(100% - 6rem), + transparent + ); + mask-composite: intersect; +} + /* Stock shadcn slot overrides ───────────────────────────────────────── These rules live outside @layer so they beat Tailwind utility classes. They compensate for stock component defaults that don't match the diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 16f3e63..3eff7c2 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -2,6 +2,7 @@ import { Analytics } from "@vercel/analytics/next"; import { SpeedInsights } from "@vercel/speed-insights/next"; import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import Link from "next/link"; import { ThemeProvider } from "@/components/theme-provider"; import { LogoIcon, GitHubIcon } from "@/components/icons"; import { ModeToggle } from "@/components/mode-toggle"; @@ -78,29 +79,26 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > -
-
-
- - - zipkit - - - / +
+
+ + + - - {"地址英譯 & 郵遞區號"} + + zipkit -
-
+ + diff --git a/apps/web/src/components/address-form.tsx b/apps/web/src/components/address-form.tsx index 18fbd80..e0c020b 100644 --- a/apps/web/src/components/address-form.tsx +++ b/apps/web/src/components/address-form.tsx @@ -9,6 +9,7 @@ import { DistrictSelect } from "@/components/district-select"; import { RoadCombobox } from "@/components/road-combobox"; import { AddressDetailInputs } from "@/components/address-detail-inputs"; import { ResultCard } from "@/components/result-card"; +import { SectionLabel } from "@/components/section-label"; import { toSlug } from "@/lib/slug"; type AddressFormProps = { @@ -81,11 +82,7 @@ export function AddressForm({
-
- - {"查詢結果"} - -
+ {"查詢結果"} {state.city && state.district && (
diff --git a/apps/web/src/components/address-page.tsx b/apps/web/src/components/address-page.tsx index 9afe5a9..f484928 100644 --- a/apps/web/src/components/address-page.tsx +++ b/apps/web/src/components/address-page.tsx @@ -7,13 +7,12 @@ import { memo, type ComponentType, } from "react"; +import { ArrowUpRight } from "lucide-react"; import type { City, District } from "@zipkit/core"; import { useAddressState } from "@/hooks/use-address-state"; import { toSlug } from "@/lib/slug"; import { AddressForm } from "@/components/address-form"; -import { LogoIcon, GitHubIcon } from "@/components/icons"; -import { ModeToggle } from "@/components/mode-toggle"; -import { Card } from "@/components/ui/card"; +import { BracketBadge } from "@/components/bracket-badge"; /* ------------------------------------------------------------------ */ /* Module-level preload */ @@ -54,9 +53,13 @@ if ( const MAX_RETRIES = 2; +const POSTAL_DATA_URL = + "https://www.post.gov.tw/post/internet/Download/index.jsp?ID=2292"; + interface TaiwanMapProps { city: City | null; district: District | null; + cities: City[]; } type MapLoadState = @@ -117,11 +120,11 @@ function useLazyMap() { /* ------------------------------------------------------------------ */ const MapPlaceholder = () => ( -
+
); const MapError = ({ onRetry }: { onRetry: () => void }) => ( -
+

地圖載入失敗