From 25f470f2ab657cb462ff834ca748a8a88dc50fe9 Mon Sep 17 00:00:00 2001 From: ronload <91997734+ronload@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:47:57 +0800 Subject: [PATCH 1/3] feat(ui): redesign home hero with split layout Put the text block and the tool on the left and let the map fill the right, so the page reads as a landing hero while the form stays one glance away. The header now holds the brand, GitHub link and theme toggle at every breakpoint, replacing the card that only showed on desktop. The map panel bleeds to the viewport's right edge and sticks under the header, so the camera only frames what is on screen. Its canvas fades out through a CSS mask rather than background-colored overlays, which cut off the page glow behind it and left a visible seam. Corner marks in an accent color, mono section labels and a staggered entrance animation carry the new style; under reduced motion the entrance becomes a short fade. --- apps/web/src/app/globals.css | 48 ++++++++++ apps/web/src/app/layout.tsx | 28 +++--- apps/web/src/components/address-form.tsx | 7 +- apps/web/src/components/address-page.tsx | 109 +++++++++++----------- apps/web/src/components/bracket-badge.tsx | 46 +++++++++ apps/web/src/components/mode-toggle.tsx | 3 +- apps/web/src/components/section-label.tsx | 24 +++++ 7 files changed, 190 insertions(+), 75 deletions(-) create mode 100644 apps/web/src/components/bracket-badge.tsx create mode 100644 apps/web/src/components/section-label.tsx 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 }) => ( -
+

地圖載入失敗