Animated Stack
Smoothly animate items in and out of a vertical stack
AnimatedItem
Use AnimatedItem to wrap content that should animate in/out
This content stays in place while the above animates.
<AnimatedItem visible={false}>
<div>Your content</div>
</AnimatedItem>
AnimatedStack
Use AnimatedStack to manage multiple items
<AnimatedStack
gap={12}
items={[
{ key: "1", visible: true, content: ... },
{ key: "2", visible: false, content: ... },
]}
/>
Usage
import { AnimatedStack, AnimatedItem } from "@/components/AnimatedStack";
// Single item toggle
<AnimatedItem visible={isVisible}>
<div>Content that animates in/out</div>
</AnimatedItem>
// Multiple items in a stack
<AnimatedStack
gap={16} // Gap between items (px)
duration={0.3} // Animation duration (seconds)
items={[
{ key: "unique-id", visible: true, content: <div>...</div> },
]}
/>Badges
Status and feature badges
Cards
Card components for services, features, and testimonials
Service Cards
Residential
Interior & exterior cleaning
Commercial
Office buildings & high-rises
Pricing Cards
One-Time
$185
Monthly
$167
10% off
Bi-Weekly
$157
15% off
Weekly
$148
20% off
Testimonial Card
“Lucent Wash transformed our office building. The windows have never looked better. Professional, punctual, and perfect results!”
Sarah M.
Dallas, TX
Feature Cards
Licensed & Insured
Full coverage for peace of mind
Eco-Friendly
Safe for your family & pets
Flexible Scheduling
Book at your convenience
Charts
Data visualization components
MiniBarChart
A tiny 12-bar sparkline chart. Accepts an array of ISO date strings, auto-buckets them by month for the past 12 months, and renders proportional bars. Hover each bar for the month label and count.
Size Variations
96x24 (default)
160x40
240x64
Color Variations
bg-blue-400 (default)
bg-emerald-400
bg-amber-400
Empty State
No data — flat placeholder bars
import MiniBarChart from "@/components/MiniBarChart"; <MiniBarChart dates={["2026-01-10", "2026-02-01", ...]} // ISO date strings barColor="bg-blue-400" // Tailwind bg class width={96} // Chart width in px height={24} // Chart height in px />
Color Palette
Electric blue and tropical cyan create a fresh, clean aesthetic
Primary (Electric Blue)
50
#EFF6FF
100
#DBEAFE
200
#BFDBFE
300
#93C5FD
400
#60A5FA
500
#3B82F6
600
#2563EB
700
#1D4ED8
800
#1E40AF
900
#1E3A8A
950
#172554
Accent (Cyan / Water)
50
#ECFEFF
100
#CFFAFE
200
#A5F3FC
300
#67E8F9
400
#22D3EE
500
#06B6D4
600
#0891B2
700
#0E7490
800
#155E75
900
#164E63
950
#083344
Success (Emerald)
50
#ECFDF5
100
#D1FAE5
200
#A7F3D0
300
#6EE7B7
400
#34D399
500
#10B981
600
#059669
700
#047857
800
#065F46
900
#064E3B
950
#022C22
Warning (Amber)
50
#FFFBEB
100
#FEF3C7
200
#FDE68A
300
#FCD34D
400
#FBBF24
500
#F59E0B
600
#D97706
700
#B45309
800
#92400E
900
#78350F
950
#451A03
Error (Red)
50
#FEF2F2
100
#FEE2E2
200
#FECACA
300
#FCA5A5
400
#F87171
500
#EF4444
600
#DC2626
700
#B91C1C
800
#991B1B
900
#7F1D1D
950
#450A0A
Gradients
Hero Gradient
Button Gradient
Success Button Gradient
Water Gradient
Date & Time
Centralized formatters from @/lib/format — all date/time display should use these.
Date Formatters
formatDate()Short date for tables & cards
formatDateLong()Full date for headings & confirmations
formatDateWithYear()Short date + year for reviews & payments
formatDateFormal()Long date without weekday for emails
formatMonthYear()Calendar headers
Time Formatters
formatTimeTime from ISO datetime
formatScheduledTime("14:30")HH:MM → 12-hour
formatScheduledTime("morning")Time window label
formatScheduledTime("afternoon")Time window label
formatScheduledTime("flexible")Time window label
formatDateTimeDate + time for timestamps
Relative & Duration
formatRelativeDate("2026-02-25")Admin table style (with color)
formatRelativeDate("2026-02-26")Admin table style
formatRelativeDateLabel("2026-02-25")Crew-facing simple label
formatRelativeDateLabel("2026-02-26")Crew-facing simple label
formatRelativeDateLabel("2026-03-04")Crew-facing (next week)
formatTimeAgo (5m)Compact time-ago
formatTimeAgo (2h)Compact time-ago
formatElapsed (45 min)Elapsed duration
formatElapsed (1h 23m)Elapsed duration
formatCountdown (2h 15m)Countdown timer
formatCountdown (0)Countdown at zero
Usage
import { formatDate, formatTime, formatElapsed } from "@/lib/format";
// In admin files, you can also import via the re-export:
import { formatDate } from "@/lib/admin/format";
// Dates — always pass date-only strings ("2026-02-19")
formatDate("2026-02-19") // "Thu, Feb 19"
formatDateLong("2026-02-19") // "Thursday, February 19, 2026"
formatDateWithYear("2026-02-19") // "Feb 19, 2026"
// Times — pass ISO datetime strings or HH:MM
formatTime("2026-02-19T14:30:00Z") // "2:30 PM"
formatScheduledTime("14:30") // "2:30 PM"
formatScheduledTime("morning") // "8 AM – 12 PM"
// Durations — pass milliseconds, no seconds shown
formatElapsed(2700000) // "45 min"
formatCountdown(8100000) // "2h 15m"Detail Sections
Consistent section cards for admin detail views with built-in edit/view state management
Card Variant (default)
Status
Properties
3Notes
Internal admin notes
Plain Variant
Bookings
5With Actions Badge
Contact
Editable (click the pencil)
Sections start in view mode. Click the pencil to edit. Save/Revert buttons appear in edit mode. Border highlights blue while editing.
Contact
Notes
Danger Zone
Danger Zone
Props
| Prop | Type | Description |
|---|---|---|
| title | string | Section heading |
| subtitle | string? | Secondary text below title |
| count | number? | Badge count next to title |
| variant | "card" | "plain" | Card with border (default) or plain heading |
| padding | "default" | "compact" | Content padding size |
| actions | ReactNode? | Extra header content (badges, buttons) |
| editable | boolean? | Enable built-in edit pencil / save+revert |
| isEditing | boolean? | Whether section is in edit mode |
| onEdit / onSave / onRevert | () => void | Edit lifecycle callbacks |
| isSaving | boolean? | Disables save button, shows loading text |
Effects & Animations
Sparkles, glass shimmer, and squeegee streak effects
Sparkles
Crystal Clear Views
Twinkling sparkle animation
<Sparkles count={15} minSize={16} maxSize={32} />Import from @/components/SparklesGlass Panel with Shimmer
3D Glass Shimmer
Watch the light sweep across this frosted glass panel
Squeegee Streak Effect
Horizontal streak animation like a squeegee wipe
3D Glass Cards
Residential
Hover to see 3D lift effect
Commercial
Hover to see 3D lift effect
Loading Shimmer
Soap Suds Shader
WebGL shader effect with animated pastel rainbow soap-bubble blobs using simplex noise and fractal brownian motion. Falls back to a CSS gradient if WebGL context is lost.
Soap Suds
Animated WebGL shader overlay
<SoapSudsShader className="..." />Import from @/components/SoapSudsShaderVideo Background
Full-bleed background video with dark overlay and smooth fade-in. Falls back to solid dark background while loading. Used across hero sections on Residential, Commercial, and Careers pages.
Video Background
Auto-playing, muted, looping video
<VideoBackground />Import from @/components/VideoBackgroundFetches video URL from landing page settings. Props: none — self-contained.
Favicon
Browser tab icons matching the AppIcon design — swirl logo on gradient background
icon.png (Primary Favicon)
32x32 PNG
/app/icon.png
apple-icon.png (Apple Touch Icon)
180x180 PNG
/app/apple-icon.png
Favicon Files
/app/icon.png- PNG favicon for modern browsers (32x32)/app/apple-icon.png- Apple Touch Icon (180x180)- Uses the
AppIcondesign — white swirl logo on cyan-teal gradient with rounded corners - Next.js automatically generates favicon meta tags from these files
Form Elements
Inputs, selects, and form controls
Select Component
Custom dropdown select with keyboard navigation, animations, and multiple variants.
Status Select
Compact pill-style select for status fields with color indicators.
pendingimport { Select, StatusSelect } from "@/components/Select"; // Basic usage <Select value={value} onChange={setValue} options={[ { value: "residential", label: "Residential" }, { value: "commercial", label: "Commercial" }, ]} placeholder="Select..." size="md" // "sm" | "md" | "lg" variant="default" // "default" | "minimal" | "pill" /> // Status select with colors <StatusSelect value={status} onChange={setStatus} options={[ { value: "pending", label: "Pending", color: "bg-amber-100 text-amber-700" }, { value: "confirmed", label: "Confirmed", color: "bg-blue-100 text-blue-700" }, ]} />
TimePicker
Two-step time selector: first pick Morning or Afternoon, then choose a 30-minute slot from a grid. Animated transitions between steps using AnimatedItem.
import TimePicker from "@/components/TimePicker"; <TimePicker value="09:00" // "HH:MM" format or "" onChange={setTime} // (time: string) => void className="..." // optional wrapper class />
Toggle Switch
Accessible toggle switch pattern with role="switch" and aria-checked. Used in crew member availability schedules.
Note: This is currently an inline pattern (not yet extracted into a standalone component). Consider creating a Toggle component for reuse.
OTP Input
6-digit verification code input with numeric filtering, paste button, and monospace styling. Used across login, signup, and invite flows.
Current value: (empty)
import OtpInput from "@/components/OtpInput"; <OtpInput value={otpCode} onChange={setOtpCode} autoFocus // optional disabled={false} // optional />
Other Form Elements
Fun Slider
An interactive range slider with spring physics, glow effects, and particle bursts.
Color Variants
Three color themes that map to the brand palette. Drag or tap anywhere on the track. Particles burst from the thumb as the value increases.
Sizes
Small for inline controls, medium (default) for forms, large for hero/landing sections.
States
import FunSlider from "@/components/FunSlider"; <FunSlider value={value} onChange={setValue} min={10} // minimum value max={60} // maximum value step={1} // increment (default 1) variant="blue" // "blue" | "cyan" | "emerald" size="lg" // "sm" | "md" | "lg" particles={true} // show particle bursts (default true) disabled={false} // disabled state />
Headers
Navigation header variants used across the site. These render the actual Header component.
Main Site Header
Used on: Landing page, service pages<Header variant="main" />
Full Width (resize browser to see breakpoints: xl=1280px+, lg=1024px, mobile=<1024px)
xl (1280px+): All nav items visible, full phone number, "My Account" with icon, "Get Free Quote"
lg (1024-1279px): Services, Pricing + "More" dropdown, phone & account as icons only, "Get Quote"
<lg: Hamburger menu
Subpage Header
Used on: Residential, Commercial, Design System<Header variant="subpage" backHref="/" pageLabel="Page Name" />
Resize browser to see responsive behavior: back label and wordmark hide on small screens
Dashboard Header
Used on: Customer dashboard, Admin dashboard, Quote flow<Header variant="dashboard" userEmail="user@example.com" />
Resize browser to see responsive behavior: email hides on small screens
Header Component API
Import:
import Header from "@/components/Header";Props:
variant:"main" | "subpage" | "dashboard"backHref: Back button destination (subpage only)backLabel: Back button text (subpage only)pageLabel: Page name shown after logo (subpage only)userEmail: User email to display (dashboard only)onClose: Close button callback (dashboard only)
Responsive Breakpoints (Main Header):
xl (1280px+): Full text for phone, account, "Get Free Quote"lg (1024px): Icons for phone/account, "Get Quote", "More" dropdown<lg: Hamburger menu with full mobile nav
Logo
Upload your logo.svg to the public folder and it will appear here
App Icon
Used in headers and as a favicon. White logo on cyan-teal gradient background.
64px
48px
40px
32px
24px
<AppIcon size={40} /> - Import from @/components/AppIcon
Logo Wordmark
Combined AppIcon + wordmark for headers and auth pages. Clickable to navigate home.
48px (Auth pages)
40px (Main header)
36px (Dashboard)
32px (Subpage)
<LogoWordmark iconSize={40} /> - Import from @/components/LogoWordmark
Props: iconSize, clickable, href, hideWordmarkOnMobile
On Light Background
On Dark Background
On Brand Gradient
Logo File Requirements
/public/logo.svg- Primary logo (for light backgrounds)/public/logo-white.svg- White version (for dark/gradient backgrounds)- Recommended dimensions: 200x60px (horizontal lockup)
- Minimum clear space: 20px on all sides
Maps
Leaflet-based map components with lazy loading to avoid SSR issues. All maps support dark mode via CSS filter inversion.
ServiceAreaMap
Isochrone drive-time visualization centered on White Settlement. Three concentric zones (15/30/45 min) rendered as GeoJSON overlays on grayscale Voyager tiles. Non-interactive.
Tile: voyager + grayscale · View: MAP_VIEWS.serviceArea· Used on: Landing page
import ServiceAreaMap from "@/components/ServiceAreaMap"; <ServiceAreaMap /> // No props — loads /data/isochrone.json
PropertyMap
Single-marker map centered on a property location. Includes marker with address popup and "Get Directions" buttons for Google Maps and Apple Maps. Supports satellite/map toggle.
Tile: light + satellite · View: MAP_VIEWS.property· Used on: Booking detail, admin property drawer
import PropertyMap from "@/components/PropertyMap"; <PropertyMap lat={32.7595} lng={-97.3933} address="123 White Settlement Rd, Fort Worth, TX" />
AllPropertiesMap
Multi-marker map showing all properties, color-coded by type (blue = residential, gold = commercial). Auto-fits bounds to markers. Includes click callbacks per marker and a color legend.
Tile: light + satellite · View: MAP_VIEWS.allProperties· Used on: Admin properties tab
import AllPropertiesMap from "@/components/AllPropertiesMap"; <AllPropertiesMap properties={[ { id: "1", address: "123 Main St", property_type: "residential", lat: 32.77, lng: -96.79 }, ... ]} onPropertyClick={(id) => openDrawer(id)} />
AddressMapPreview
Interactive map with draggable marker for address selection. Click or drag to set location, with reverse geocoding for address lookup. Supports satellite/map toggle.
Tile: light + satellite · View: MAP_VIEWS.addressPicker· Used on: Booking form, admin property edit
import AddressMapPreview from "@/components/AddressMapPreview"; <AddressMapPreview lat={32.7595} lng={-97.3933} onPositionChange={({ lat, lng }) => setCoords({ lat, lng })} />
Tile Layers & Map Views
Centralized in lib/constants.ts as MAP_TILES, MAP_ATTRIBUTION, and MAP_VIEWS.
Tile Layers
light— Low-contrast, subtle roads. Good for overlays.voyager— Higher-contrast colored roads. Navigation maps.voyagerGrayscale— Voyager +.leaflet-grayscaleCSS class. Grayscale roads with colored overlays.satellite— Esri satellite imagery.
Preset Views
dfw— Arlington center, zoom 9. Full metroplex.serviceArea— White Settlement, zoom 8. Isochrone display.fortWorth— Fort Worth, zoom 12. Crew/sales maps.property— Zoom 15. Single property close-up.addressPicker— Zoom 16. Address selection.allProperties— Zoom 11. Multi-property overview.
Modals
Animated modal dialogs with spring physics and smooth transitions
Centered Modal
Scales up with fade, backdrop blur. Good for forms and content.
Dialog Modal
Confirmation dialogs with title, description, and action buttons.
Fullscreen Modal
Slides up from bottom, covers the whole screen. Used in quote flow.
Usage
import { Modal, FullscreenModal, DialogModal, useModal } from "@/components/Modal";
// Hook for managing modal state
const modal = useModal();
// Centered Modal (forms, content)
<Modal isOpen={modal.isOpen} onClose={modal.close}>
<div className="p-6">Modal content</div>
</Modal>
// Dialog Modal (confirmations)
<DialogModal
isOpen={modal.isOpen}
onClose={modal.close}
title="Confirm"
description="Are you sure?"
actions={<>
<button onClick={modal.close}>Cancel</button>
<button onClick={handleConfirm}>Confirm</button>
</>}
/>
// Fullscreen Modal (multi-step flows)
<FullscreenModal
isOpen={modal.isOpen}
onClose={modal.close}
title="Step 1 of 3"
>
<div>Full page content</div>
</FullscreenModal>OpenGraph Images
Social media preview images for public-facing pages (1200x630px recommended)
Crystal Clear Views, Every Time
Professional Window Washing | DFW Metroplex
Join Our Team
Now Hiring | Part-Time Window Washers | $18-22/hr
Residential Window Cleaning
Streak-Free Windows for Your Home | DFW
Commercial Window Cleaning
Professional Service for Your Business | DFW
OpenGraph Image Guidelines
- Recommended size:
1200x630px(1.91:1 aspect ratio) - File format: PNG or JPG, under 8MB
- Place as
opengraph-image.pngin each route folder - Next.js automatically generates og:image meta tags from these files
Pricing Breakdown
Shared read-only pricing display with itemized discounts. Used in admin BookingDrawer and customer BookingDetailClient.
With Itemized Discounts
Fallback (No Itemization)
Prepay Discount Only
All Discount Types
No Discounts
Manual Admin Discount
Sales % Discount (15% off)
Sales Fixed Price Override ($150)
Sales + Frequency Discount Stacked
Grandfathered Pricing (Locked Rate)
Grandfathered (No Savings Yet)
High-Value Commercial
Small Job + Travel Fee
Kitchen Sink (All Discount Types)
Props Reference
| Prop | Type | Description |
|---|---|---|
| subtotal | number | Pre-discount subtotal |
| discountItems | DiscountItem[] | Itemized discounts (frequency, referral, prepay, manual) |
| totalDiscount | number | Fallback flat discount when discountItems is empty |
| travelFee | number | Travel surcharge (shown only if > 0) |
| total | number | Final total after discounts + travel fee |
| tip | number? | Tip amount (shown only when showTip is true and tip > 0) |
| showTip | boolean? | Whether to display the tip line |
| lineItems | {label, amount}[]? | Optional line items shown above subtotal (e.g. windows, screens) |
| grandfatherInfo | {isGrandfathered, savings}? | Shows locked-in rate badge when isGrandfathered is true. Savings shows delta vs current rates. |
Rolling Number
An odometer-style animated counter. Each digit rolls independently with spring physics.
Interactive Demo
Click the buttons to change the value and watch each digit column roll to its new position.
Prefix & Suffix
Attach static text before or after the rolling digits.
prefix="$"
suffix="%"
padStart=2
Spring Tuning
Adjust stiffness and damping for different feels.
Snappy (400/20)
Default (200/25)
Bouncy (80/15)
Any Size
Inherits font-size and line-height from className, so it works at any scale.
text-sm
text-2xl
text-5xl
import RollingNumber from "@/components/RollingNumber"; <RollingNumber value={42} prefix="$" // optional static prefix suffix="%" // optional static suffix padStart={2} // pad with leading zeros stiffness={200} // spring stiffness (default 200) damping={25} // spring damping (default 25) className="text-3xl font-bold text-blue-600 leading-[40px]" />
Shadows
Elevation system using Tailwind shadows
Standard Shadows
shadow-sm
Subtle shadow for cards and inputs
shadow
Default shadow for elevated elements
shadow-md
Medium shadow - used on headers
shadow-lg
Large shadow for modals and dropdowns
shadow-xl
Extra large shadow for prominent cards
shadow-2xl
Largest shadow for hero elements
Colored Shadows
shadow-lg shadow-blue-500/25
Primary CTA buttons
shadow-lg shadow-cyan-500/25
Accent elements
shadow-lg shadow-slate-200/50
Service cards
Inner Shadows
shadow-inner
Inset shadow for pressed states or inputs
shadow-none
Remove shadow (useful for hover states)
Shadow Usage Guidelines
shadow-md- Headers (fixed/sticky navigation)shadow-lg- Cards, dropdowns, modalsshadow-lg shadow-blue-500/25- Primary CTA buttonsshadow-xl- Hover states on cards
Sub-Headers
Standardized header for sub-pages (Schedule, Properties, Profile, etc.). Back button left, logo centered, nothing right. Page title goes in the body, not the header.
Live Demo
Used on: Schedule, Properties, Profile, and all customer sub-pages
Sub-Header Pattern
Structure:
- Left: Back arrow + label (label hidden on mobile via
hidden sm:inline) - Center:
LogoWordmark iconSize={36} - Right: Empty spacer
w-[88px] hidden sm:blockto balance the back button
Body title:
- Page title is an
h1placed inside<main>, not in the header - Optionally includes a contextual icon (Calendar, MapPin, etc.)
- Style:
text-2xl font-bold
Container:
sticky top-0 z-50bg-white dark:bg-blackwithborder-b- Padding:
px-4 sm:px-6 py-3 sm:py-4 - Max-width matches page content (
max-w-2xltomax-w-5xl)
Used on:
- Schedule page (
/schedule/[propertyId]) - Properties page (
/properties) - Profile page (
/profile)
Typography
Clean, modern type scale with Inter font family
Display / 60px
Crystal Clear Views
H1 / 48px
Professional Window Cleaning
H2 / 36px
Our Services
H3 / 24px
Residential Cleaning
Body Large / 18px
We provide streak-free, sparkling clean windows for your home or business.
Body / 16px
Serving the entire DFW metroplex with reliable, professional window cleaning services.
Small / 14px
© 2025 Lucent Wash. All rights reserved. Privacy Policy | Terms of Service