2 Commits

Author SHA1 Message Date
Gemini Agent
065250c1cf Redesign: Warm Sanctuary aesthetic for core pages
- Implement cohesive 'Warm Sanctuary' design system
- Add Playfair Display + Source Sans 3 typography
- Create paper texture background and warm color palette
- Redesign Today Dashboard with elegant cards and animations
- Redesign Medication Form with step-by-step visual flow
- Redesign Emergency Card with clear visual hierarchy
- Redesign Onboarding with floating blobs and welcoming feel
- Update Tailwind config with new colors, shadows, and animations
2026-03-01 07:06:58 +00:00
Gemini Agent
a5181cf6fe Add timezone support and auto-sync push subscriptions
- Install tzdata in Docker for proper Australia/Perth timezone handling
- Update VAPID email to standard placeholder
- Auto-sync browser push subscriptions to server on page load

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:29:42 +00:00
11 changed files with 1093 additions and 553 deletions

View File

@@ -33,6 +33,8 @@ RUN npm run build
FROM node:20-slim AS runner
WORKDIR /app
RUN apt-get update && apt-get install -y tzdata && rm -rf /var/lib/apt/lists/*
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV TZ=Australia/Perth

View File

@@ -18,7 +18,7 @@ services:
# Push notification VAPID keys
- NEXT_PUBLIC_VAPID_PUBLIC_KEY=BEFs_VtoxY7SpNnd-ubz1ioliESlRI4sY6ny7Qp3rm7V1cm0gqyZX8TAHp4AaQ81yKC4LfWtJFQz_aHc25G-Tww
- VAPID_PRIVATE_KEY=KgVQVO7XhfCklrJ3o9wowzK90AxI6Exg9pXPq76Qx4A
- VAPID_EMAIL=mailto:admin@nextstep.local
- VAPID_EMAIL=mailto:admin@example.com
depends_on:
db:
condition: service_healthy

View File

@@ -1,18 +1,23 @@
'use client'
import { useEffect, useState } from 'react'
import { ArrowLeft, Edit2 } from 'lucide-react'
import { ArrowLeft, Edit2, Heart } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useLiveQuery } from 'dexie-react-hooks'
import { db } from '@/lib/sync'
import { EmergencyCard } from '@/components/emergency/EmergencyCard'
import { Button, LoadingState } from '@/components/ui'
import { LoadingState } from '@/components/ui'
import { useApp } from '../provider'
export default function EmergencyPage() {
const router = useRouter()
const { currentWorkspace } = useApp()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// Fetch workspace from IndexedDB for offline access
const workspace = useLiveQuery(
@@ -32,7 +37,11 @@ export default function EmergencyPage() {
)
if (!workspace) {
return <LoadingState message="Loading emergency info..." />
return (
<div className="min-h-screen paper-texture flex items-center justify-center">
<LoadingState message="Loading emergency info..." />
</div>
)
}
const emergencyInfo = {
@@ -56,60 +65,76 @@ export default function EmergencyPage() {
})) || []
return (
<div className="min-h-screen bg-red-50">
<div className={`min-h-screen paper-texture transition-opacity duration-500 ${mounted ? 'opacity-100' : 'opacity-0'}`}>
{/* Header */}
<div className="bg-red-600 text-white safe-top">
<div className="flex items-center justify-between px-4 py-3">
<div className="bg-gradient-to-r from-alert-500 to-alert-600 text-white safe-area-top sticky top-0 z-10">
<div className="flex items-center justify-between px-6 py-4">
<button
onClick={() => router.back()}
className="flex items-center gap-2 text-white/90 hover:text-white"
className="flex items-center gap-2 text-white/90 hover:text-white transition-colors"
>
<div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center">
<ArrowLeft className="w-5 h-5" />
<span>Back</span>
</div>
<span className="font-medium">Back</span>
</button>
{currentWorkspace.role !== 'VIEWER' && (
<button
onClick={() => router.push('/settings/emergency')}
className="flex items-center gap-2 text-white/90 hover:text-white"
className="flex items-center gap-2 text-white/90 hover:text-white transition-colors"
>
<Edit2 className="w-4 h-4" />
<span>Edit</span>
<span className="font-medium">Edit</span>
<div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center">
<Edit2 className="w-5 h-5" />
</div>
</button>
)}
</div>
</div>
<div className="p-4">
<div className="p-6 pb-24">
{hasInfo ? (
<div className="animate-fade-up">
<EmergencyCard info={emergencyInfo} medications={medsList} />
) : (
<div className="text-center py-12">
<div className="w-16 h-16 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4">
<Edit2 className="w-8 h-8 text-red-400" />
</div>
<h2 className="text-lg font-semibold text-secondary-900 mb-2">
) : (
<div className="section-warm text-center py-12 animate-fade-up">
<div className="w-20 h-20 rounded-full bg-alert-100 flex items-center justify-center mx-auto mb-6">
<Heart className="w-10 h-10 text-alert-400" />
</div>
<h2 className="font-display text-2xl text-secondary-900 mb-3">
No Emergency Info Set
</h2>
<p className="text-secondary-600 mb-4">
Add important medical information for emergencies.
<p className="text-secondary-600 mb-8 max-w-sm mx-auto">
Add important medical information that could be crucial in an emergency situation.
</p>
{currentWorkspace.role !== 'VIEWER' && (
<Button onClick={() => router.push('/settings/emergency')}>
<button
onClick={() => router.push('/settings/emergency')}
className="btn-primary"
>
Add Emergency Info
</Button>
</button>
)}
</div>
)}
</div>
{/* Offline indicator */}
<div className="fixed bottom-4 left-4 right-4">
<div className="bg-green-100 border border-green-300 rounded-lg p-3 text-center">
<p className="text-sm text-green-800 font-medium">
<div className="fixed bottom-6 left-6 right-6">
<div className="bg-primary-50 border border-primary-200 rounded-card p-4 text-center shadow-elevated">
<div className="flex items-center justify-center gap-2">
<div className="w-2 h-2 rounded-full bg-primary-500 animate-pulse" />
<p className="text-sm text-primary-700 font-medium">
This information is available offline
</p>
</div>
</div>
</div>
</div>
)
}

View File

@@ -4,7 +4,7 @@ import { useEffect, useState, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { format, isToday, isTomorrow } from 'date-fns'
import { toZonedTime } from 'date-fns-tz'
import { Phone, MapPin, Clock, ChevronRight, Pill, Calendar, Plus, AlertTriangle, ClipboardCheck } from 'lucide-react'
import { Phone, MapPin, Clock, ChevronRight, Pill, Calendar, Plus, AlertTriangle, ClipboardCheck, Heart } from 'lucide-react'
import { useLiveQuery } from 'dexie-react-hooks'
import { db, logDose, undoDose } from '@/lib/sync'
@@ -23,6 +23,11 @@ export default function TodayPage() {
const [now, setNow] = useState(() => new Date())
const [quickNote, setQuickNote] = useState('')
const [isAddingNote, setIsAddingNote] = useState(false)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// Update time every minute
useEffect(() => {
@@ -149,7 +154,14 @@ export default function TodayPage() {
const date = toZonedTime(new Date(datetime), TIMEZONE)
if (isToday(date)) return `Today at ${format(date, 'h:mm a')}`
if (isTomorrow(date)) return `Tomorrow at ${format(date, 'h:mm a')}`
return format(date, 'EEE, MMM d \'at\' h:mm a')
return format(date, "EEE, MMM d 'at' h:mm a")
}
const getGreeting = () => {
const hour = now.getHours()
if (hour < 12) return 'Good morning'
if (hour < 17) return 'Good afternoon'
return 'Good evening'
}
if (!appointments || !medications) {
@@ -166,27 +178,41 @@ export default function TodayPage() {
return (
<>
<Header title="Today" />
<PageContainer className="pt-4 space-y-6">
{/* Greeting */}
<div className="mb-2">
<p className="text-secondary-500 text-sm">
<PageContainer className="pt-6 pb-24 space-y-8">
{/* Greeting Section with decorative elements */}
<div className={`relative transition-all duration-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
{/* Decorative blob */}
<div className="blob blob-primary w-32 h-32 -top-4 -left-4" />
<div className="relative">
<p className="text-secondary-500 text-sm font-medium tracking-wide uppercase mb-1">
{format(toZonedTime(now, TIMEZONE), 'EEEE, MMMM d')}
</p>
<h1 className="font-display text-display-sm text-secondary-900">
{getGreeting()}
</h1>
<p className="text-secondary-600 mt-2 flex items-center gap-2">
<Heart className="w-4 h-4 text-accent-500" />
<span>Take it one step at a time</span>
</p>
</div>
</div>
{/* Emergency & Call Clinic Buttons */}
<div className="flex gap-3">
{/* Emergency & Call Clinic Buttons - Floating cards */}
<div className={`flex gap-3 transition-all duration-700 delay-100 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
{/* Emergency Info Button */}
<button
onClick={() => router.push('/emergency')}
className="flex items-center gap-3 p-4 bg-red-50 rounded-card border border-red-200 hover:bg-red-100 transition-colors flex-1"
className="flex-1 group relative overflow-hidden"
>
<div className="w-10 h-10 rounded-full bg-red-600 flex items-center justify-center">
<AlertTriangle className="w-5 h-5 text-white" />
<div className="relative flex items-center gap-3 p-4 bg-alert-50/80 backdrop-blur-sm rounded-card border border-alert-200/60 hover:border-alert-300 hover:shadow-elevated transition-all duration-300">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-alert-500 to-alert-600 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform duration-300">
<AlertTriangle className="w-6 h-6 text-white" />
</div>
<div className="text-left">
<p className="font-medium text-red-800">Emergency</p>
<p className="text-sm text-red-600">Medical info</p>
<p className="font-semibold text-alert-800">Emergency</p>
<p className="text-sm text-alert-600">Medical info</p>
</div>
</div>
</button>
@@ -194,26 +220,28 @@ export default function TodayPage() {
{currentWorkspace.clinicPhone && (
<a
href={`tel:${currentWorkspace.clinicPhone}`}
className="flex items-center gap-3 p-4 bg-primary-50 rounded-card border border-primary-100 hover:bg-primary-100 transition-colors flex-1"
className="flex-1 group"
>
<div className="w-10 h-10 rounded-full bg-primary-500 flex items-center justify-center">
<Phone className="w-5 h-5 text-white" />
<div className="flex items-center gap-3 p-4 bg-primary-50/80 backdrop-blur-sm rounded-card border border-primary-200/60 hover:border-primary-300 hover:shadow-elevated transition-all duration-300">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform duration-300">
<Phone className="w-6 h-6 text-white" />
</div>
<div className="text-left">
<p className="font-medium text-primary-800">Call Clinic</p>
<p className="text-sm text-primary-600 truncate">{currentWorkspace.clinicPhone}</p>
<p className="font-semibold text-primary-800">Call Clinic</p>
<p className="text-sm text-primary-600 truncate max-w-[100px]">{currentWorkspace.clinicPhone}</p>
</div>
</div>
</a>
)}
</div>
{/* Next Appointment */}
<section>
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-secondary-900">Next Appointment</h2>
{/* Next Appointment - Hero Card */}
<section className={`transition-all duration-700 delay-200 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<div className="flex items-center justify-between mb-4">
<h2 className="font-display text-xl text-secondary-900">Next Appointment</h2>
<button
onClick={() => router.push('/appointments')}
className="text-sm text-primary-600 font-medium flex items-center"
className="text-sm text-primary-600 font-medium flex items-center gap-0.5 hover:text-primary-700 transition-colors"
>
View all
<ChevronRight className="w-4 h-4" />
@@ -221,30 +249,30 @@ export default function TodayPage() {
</div>
{nextAppointment ? (
<Card
className="card-appointment"
<div
className="card-appointment cursor-pointer group"
onClick={() => router.push(`/appointments/${nextAppointment.id}`)}
>
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0">
<Calendar className="w-5 h-5 text-primary-600" />
<div className="flex items-start gap-4">
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-primary-100 to-primary-200 flex items-center justify-center flex-shrink-0 shadow-inner">
<Calendar className="w-7 h-7 text-primary-600" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-secondary-900 truncate">
<h3 className="font-display text-lg text-secondary-900 truncate group-hover:text-primary-700 transition-colors">
{nextAppointment.title}
</h3>
<p className="text-sm text-secondary-600 flex items-center gap-1 mt-1">
<Clock className="w-4 h-4" />
<p className="text-sm text-secondary-600 flex items-center gap-1.5 mt-1.5">
<Clock className="w-4 h-4 text-primary-500" />
{formatAppointmentDate(nextAppointment.datetime)}
</p>
{nextAppointment.location && (
<p className="text-sm text-secondary-500 flex items-center gap-1 mt-0.5">
<MapPin className="w-4 h-4" />
<p className="text-sm text-secondary-500 flex items-center gap-1.5 mt-1">
<MapPin className="w-4 h-4 text-cream-600" />
<span className="truncate">{nextAppointment.location}</span>
</p>
)}
</div>
<ChevronRight className="w-5 h-5 text-secondary-400" />
<ChevronRight className="w-5 h-5 text-secondary-300 group-hover:text-primary-500 group-hover:translate-x-1 transition-all" />
</div>
{nextAppointment.mapUrl && (
<a
@@ -252,27 +280,27 @@ export default function TodayPage() {
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-1.5 mt-3 text-sm text-primary-600 font-medium hover:text-primary-700"
className="inline-flex items-center gap-1.5 mt-4 text-sm text-primary-600 font-medium hover:text-primary-700 hover:underline"
>
<MapPin className="w-4 h-4" />
Open in Maps
</a>
)}
</Card>
</div>
) : (
<Card variant="outline" className="text-center py-6">
<Calendar className="w-8 h-8 text-secondary-300 mx-auto mb-2" />
<p className="text-secondary-500">No upcoming appointments</p>
<Button
variant="ghost"
size="sm"
className="mt-2"
<div className="section-warm text-center py-8">
<div className="w-16 h-16 rounded-full bg-cream-100 flex items-center justify-center mx-auto mb-4">
<Calendar className="w-8 h-8 text-cream-600" />
</div>
<p className="text-secondary-600 font-medium">No upcoming appointments</p>
<button
onClick={() => router.push('/appointments/new')}
className="mt-4 inline-flex items-center gap-2 text-primary-600 font-medium hover:text-primary-700"
>
<Plus className="w-4 h-4 mr-1" />
<Plus className="w-4 h-4" />
Add one
</Button>
</Card>
</button>
</div>
)}
</section>
@@ -283,26 +311,26 @@ export default function TodayPage() {
)
if (tomorrowAppt) {
return (
<section>
<Card
className="bg-green-50 border border-green-200 cursor-pointer hover:bg-green-100 transition-colors"
<section className={`transition-all duration-700 delay-300 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<div
className="bg-gradient-to-r from-cream-100 to-cream-50 border border-cream-200 rounded-card p-5 cursor-pointer hover:shadow-elevated transition-all duration-300 group"
onClick={() => router.push(`/appointments/${tomorrowAppt.id}/prep`)}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0">
<ClipboardCheck className="w-5 h-5 text-white" />
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-accent-400 to-accent-500 flex items-center justify-center flex-shrink-0 shadow-lg group-hover:scale-105 transition-transform">
<ClipboardCheck className="w-7 h-7 text-white" />
</div>
<div className="flex-1">
<p className="font-medium text-green-800">
<p className="font-display text-lg text-secondary-900">
Prepare for tomorrow
</p>
<p className="text-sm text-green-600">
<p className="text-sm text-secondary-600">
{tomorrowAppt.title} at {format(toZonedTime(new Date(tomorrowAppt.datetime), TIMEZONE), 'h:mm a')}
</p>
</div>
<ChevronRight className="w-5 h-5 text-green-500" />
<ChevronRight className="w-5 h-5 text-accent-500 group-hover:translate-x-1 transition-transform" />
</div>
</div>
</Card>
</section>
)
}
@@ -311,6 +339,7 @@ export default function TodayPage() {
{/* Refill Alerts */}
{medications && medications.length > 0 && (
<div className={`transition-all duration-700 delay-400 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<RefillAlert
medications={medications.map(m => ({
id: m.id,
@@ -319,15 +348,16 @@ export default function TodayPage() {
refillThreshold: m.refillThreshold,
}))}
/>
</div>
)}
{/* Meds Due */}
<section>
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-secondary-900">Medications</h2>
<section className={`transition-all duration-700 delay-500 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<div className="flex items-center justify-between mb-4">
<h2 className="font-display text-xl text-secondary-900">Medications</h2>
<button
onClick={() => router.push('/meds')}
className="text-sm text-primary-600 font-medium flex items-center"
className="text-sm text-primary-600 font-medium flex items-center gap-0.5 hover:text-primary-700 transition-colors"
>
View all
<ChevronRight className="w-4 h-4" />
@@ -335,21 +365,25 @@ export default function TodayPage() {
</div>
{medsDueSoon.length > 0 ? (
<div className="space-y-3">
{medsDueSoon.map((status) => (
<div className="space-y-4">
{medsDueSoon.map((status, index) => (
<MedicationCard
key={status.medication.id}
status={status}
now={now}
onTake={() => handleTakeMed(status)}
index={index}
/>
))}
</div>
) : medications.length > 0 ? (
<Card variant="outline" className="text-center py-6">
<Pill className="w-8 h-8 text-secondary-300 mx-auto mb-2" />
<p className="text-secondary-500">All caught up! No meds due soon.</p>
</Card>
<div className="section-warm text-center py-8">
<div className="w-16 h-16 rounded-full bg-primary-50 flex items-center justify-center mx-auto mb-4">
<Pill className="w-8 h-8 text-primary-400" />
</div>
<p className="text-secondary-600 font-medium">All caught up!</p>
<p className="text-sm text-secondary-400 mt-1">No medications due soon</p>
</div>
) : (
<EmptyState
type="medications"
@@ -364,16 +398,16 @@ export default function TodayPage() {
</section>
{/* Quick Note */}
<section>
<h2 className="text-lg font-semibold text-secondary-900 mb-3">Quick Note</h2>
<Card padding="sm">
<div className="flex gap-2">
<section className={`transition-all duration-700 delay-600 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
<h2 className="font-display text-xl text-secondary-900 mb-4">Quick Note</h2>
<div className="section-warm">
<div className="flex gap-3">
<input
type="text"
value={quickNote}
onChange={(e) => setQuickNote(e.target.value)}
placeholder="Jot down a thought..."
className="flex-1 px-3 py-2.5 border border-border rounded-button text-base focus:outline-none focus:ring-2 focus:ring-primary-200 focus:border-primary-500"
className="input-sanctuary flex-1"
onKeyDown={(e) => {
if (e.key === 'Enter' && quickNote.trim()) {
handleAddQuickNote()
@@ -384,11 +418,12 @@ export default function TodayPage() {
onClick={handleAddQuickNote}
disabled={!quickNote.trim() || isAddingNote}
loading={isAddingNote}
className="btn-primary whitespace-nowrap"
>
Add
</Button>
</div>
</Card>
</div>
</section>
</PageContainer>
</>
@@ -399,9 +434,10 @@ interface MedicationCardProps {
status: MedicationDueStatus
now: Date
onTake: () => void
index: number
}
function MedicationCard({ status, now, onTake }: MedicationCardProps) {
function MedicationCard({ status, now, onTake, index }: MedicationCardProps) {
const { medication, isOverdue, isPRN, prnAvailable, prnAvailableAt, nextDueAt } = status
const getTimeLabel = () => {
@@ -426,35 +462,38 @@ function MedicationCard({ status, now, onTake }: MedicationCardProps) {
const canTake = !isPRN || prnAvailable
return (
<Card className={isOverdue ? 'overdue' : ''}>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0">
<Pill className="w-5 h-5 text-primary-600" />
<div className={`card-medication ${isOverdue ? 'overdue' : ''} animate-fade-up`} style={{ animationDelay: `${index * 0.1}s` }}>
<div className="flex items-center gap-4">
<div className={`w-14 h-14 rounded-full flex items-center justify-center flex-shrink-0 shadow-inner transition-all duration-300 ${
isOverdue
? 'bg-gradient-to-br from-accent-100 to-accent-200'
: 'bg-gradient-to-br from-primary-100 to-primary-200'
}`}>
<Pill className={`w-7 h-7 ${isOverdue ? 'text-accent-600' : 'text-primary-600'}`} />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-secondary-900">{medication.name}</h3>
<p className={`text-sm ${isOverdue ? 'text-red-600 font-medium' : 'text-secondary-500'}`}>
<h3 className="font-display text-lg text-secondary-900">{medication.name}</h3>
<p className={`text-sm ${isOverdue ? 'text-accent-600 font-medium' : 'text-secondary-500'}`}>
{getTimeLabel()}
{isPRN && ' • As needed'}
</p>
</div>
<Button
<button
onClick={(e) => {
e.stopPropagation()
onTake()
}}
variant="success"
size="md"
disabled={!canTake}
className={`btn-primary text-sm px-5 py-2.5 ${!canTake ? 'opacity-50 cursor-not-allowed' : ''}`}
>
Taken
</Button>
</button>
</div>
{medication.instructions && (
<p className="text-sm text-secondary-500 mt-2 ml-13">
<p className="text-sm text-secondary-500 mt-3 ml-[72px]">
{medication.instructions}
</p>
)}
</Card>
</div>
)
}

View File

@@ -4,10 +4,14 @@
@tailwind components;
@tailwind utilities;
/* Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Source+Sans+3:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap');
@layer base {
:root {
--background: 250 251 252;
--foreground: 31 38 49;
--background: 250 247 242;
--foreground: 38 35 32;
--surface: 255 255 255;
}
html {
@@ -17,6 +21,16 @@
body {
@apply bg-background text-secondary-900 antialiased;
font-feature-settings: 'rlig' 1, 'calt' 1;
font-family: 'Source Sans 3', system-ui, sans-serif;
}
/* Paper texture background */
.paper-texture {
background-color: #faf7f2;
background-image:
url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
background-blend-mode: soft-light;
background-size: 200px 200px;
}
/* Large text mode */
@@ -38,12 +52,12 @@
}
.large-text .text-sm {
font-size: 1rem; /* text-base equivalent */
font-size: 1rem;
line-height: 1.5rem;
}
.large-text .text-xs {
font-size: 0.875rem; /* text-sm equivalent */
font-size: 0.875rem;
line-height: 1.25rem;
}
@@ -56,9 +70,9 @@
padding-bottom: env(safe-area-inset-bottom);
}
/* Focus styles for accessibility */
/* Focus styles for accessibility - warm glow */
*:focus-visible {
@apply outline-none ring-2 ring-primary-500 ring-offset-2;
@apply outline-none ring-2 ring-primary-300 ring-offset-2 ring-offset-background;
}
/* Better touch targets */
@@ -79,64 +93,151 @@
}
@layer components {
/* Primary taken button */
.btn-taken {
/* Warm sanctuary card styles */
.card-sanctuary {
@apply bg-surface rounded-card shadow-card;
@apply border border-cream-200/50;
@apply transition-all duration-300 ease-sanctuary;
}
.card-sanctuary:hover {
@apply shadow-card-hover;
@apply border-cream-300;
}
/* Primary action button - warm green */
.btn-primary {
@apply bg-primary-500 hover:bg-primary-600 text-white font-semibold;
@apply py-3 px-6 rounded-button min-h-touch;
@apply shadow-button transition-all duration-200;
@apply active:scale-95;
@apply py-3.5 px-6 rounded-button min-h-touch;
@apply shadow-button transition-all duration-300 ease-sanctuary;
@apply active:scale-[0.98] hover:shadow-button-hover;
}
/* Card styles */
/* Secondary button - cream */
.btn-secondary {
@apply bg-cream-100 hover:bg-cream-200 text-secondary-800 font-medium;
@apply py-3.5 px-6 rounded-button min-h-touch;
@apply border border-cream-300;
@apply transition-all duration-300 ease-sanctuary;
@apply active:scale-[0.98];
}
/* Accent button - terracotta */
.btn-accent {
@apply bg-accent-500 hover:bg-accent-600 text-white font-semibold;
@apply py-3.5 px-6 rounded-button min-h-touch;
@apply shadow-button transition-all duration-300 ease-sanctuary;
@apply active:scale-[0.98] hover:shadow-button-hover;
}
/* Ghost button */
.btn-ghost {
@apply bg-transparent hover:bg-cream-100 text-secondary-700 font-medium;
@apply py-3.5 px-6 rounded-button min-h-touch;
@apply transition-all duration-300 ease-sanctuary;
@apply active:scale-[0.98];
}
/* Taken button */
.btn-taken {
@apply btn-primary;
}
/* Appointment card */
.card-appointment {
@apply bg-surface rounded-card shadow-card p-4;
@apply border-l-4 border-primary-500;
@apply card-sanctuary p-5;
@apply border-l-[6px] border-l-primary-400;
}
/* Medication card */
.card-medication {
@apply bg-surface rounded-card shadow-card p-4;
@apply flex items-center justify-between;
@apply card-sanctuary p-5;
}
/* Overdue styles */
/* Overdue styles - warm terracotta */
.overdue {
@apply border-l-4 border-red-500 bg-red-50;
@apply border-l-[6px] border-l-accent-500;
@apply bg-accent-50/50;
}
/* Emergency card - alert red but softened */
.card-emergency {
@apply bg-alert-50 border-2 border-alert-200 rounded-card-lg;
@apply overflow-hidden;
}
/* Section styling */
.section-warm {
@apply bg-surface rounded-card-lg shadow-soft p-6;
@apply border border-cream-200/60;
}
/* Input styling */
.input-sanctuary {
@apply bg-surface border border-cream-300 rounded-button;
@apply px-4 py-3.5 text-secondary-900 placeholder:text-secondary-400;
@apply focus:outline-none focus:border-primary-400 focus:ring-2 focus:ring-primary-200;
@apply transition-all duration-200;
}
/* Timeline styles */
.timeline-item {
@apply relative pl-6 pb-4;
@apply relative pl-8 pb-6;
}
.timeline-item::before {
content: '';
@apply absolute left-0 top-2 w-2 h-2 rounded-full bg-primary-400;
@apply absolute left-0 top-2 w-3 h-3 rounded-full bg-primary-300;
@apply ring-4 ring-primary-100;
}
.timeline-item::after {
content: '';
@apply absolute left-[3px] top-4 w-0.5 h-full bg-border;
@apply absolute left-[5px] top-5 w-0.5 h-full bg-cream-300;
}
.timeline-item:last-child::after {
@apply hidden;
}
/* Glass effect for overlays */
.glass {
@apply bg-surface/80 backdrop-blur-md;
@apply border border-cream-200/50;
}
/* Decorative blob shapes */
.blob {
@apply absolute rounded-full blur-3xl opacity-30 pointer-events-none;
}
.blob-primary {
@apply bg-primary-200;
}
.blob-accent {
@apply bg-accent-200;
}
.blob-cream {
@apply bg-cream-300;
}
}
@layer utilities {
/* Animation utilities */
.animate-in {
animation: animateIn 0.2s ease-out;
animation: animateIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-out {
animation: animateOut 0.15s ease-in forwards;
animation: animateOut 0.2s ease-in forwards;
}
@keyframes animateIn {
from {
opacity: 0;
transform: translateY(8px);
transform: translateY(12px);
}
to {
opacity: 1;
@@ -151,55 +252,46 @@
}
to {
opacity: 0;
transform: translateY(8px);
transform: translateY(12px);
}
}
.slide-in-from-bottom-4 {
animation: slideInFromBottom 0.2s ease-out;
}
.slide-out-to-bottom-4 {
animation: slideOutToBottom 0.15s ease-in forwards;
}
@keyframes slideInFromBottom {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideOutToBottom {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(16px);
}
}
/* Stagger animation delays */
.stagger-1 { animation-delay: 0.05s; }
.stagger-2 { animation-delay: 0.1s; }
.stagger-3 { animation-delay: 0.15s; }
.stagger-4 { animation-delay: 0.2s; }
.stagger-5 { animation-delay: 0.25s; }
.stagger-6 { animation-delay: 0.3s; }
/* Fade utilities */
.fade-in {
animation: fadeIn 0.2s ease-out;
animation: fadeIn 0.3s ease-out forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-up {
animation: fadeUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Scale utilities */
.zoom-in-95 {
animation: zoomIn 0.2s ease-out;
animation: zoomIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes zoomIn {
@@ -212,4 +304,29 @@
transform: scale(1);
}
}
/* Soft pulse for live elements */
.pulse-soft {
animation: pulseSoft 3s ease-in-out infinite;
}
@keyframes pulseSoft {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* Gradient text */
.gradient-text {
@apply bg-clip-text text-transparent;
background-image: linear-gradient(135deg, #528252 0%, #3f663f 100%);
}
/* Hide scrollbar but keep functionality */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}

View File

@@ -1,10 +1,7 @@
import type { Metadata, Viewport } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { Toaster } from '@/components/ui'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Next Step - Health Management',
description: 'A calm, reliable app to help manage appointments, medications, and notes for health care.',
@@ -21,7 +18,7 @@ export const viewport: Viewport = {
initialScale: 1,
maximumScale: 1,
userScalable: false,
themeColor: '#3a9563',
themeColor: '#528252',
}
export default function RootLayout({
@@ -33,8 +30,11 @@ export default function RootLayout({
<html lang="en">
<head>
<link rel="apple-touch-icon" href="/icon-192.png" />
{/* Preconnect to Google Fonts for performance */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
</head>
<body className={inter.className}>
<body className="paper-texture">
{children}
<Toaster />
</body>

View File

@@ -1,9 +1,9 @@
'use client'
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Heart, Shield, ArrowRight } from 'lucide-react'
import { Button, Input, Card, showToast } from '@/components/ui'
import { Heart, Shield, ArrowRight, Sparkles, Users, Bell } from 'lucide-react'
import { Button, Input, showToast } from '@/components/ui'
export default function OnboardingPage() {
const router = useRouter()
@@ -12,6 +12,11 @@ export default function OnboardingPage() {
const [clinicPhone, setClinicPhone] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const handleAcceptDisclaimer = () => {
setStep('workspace')
@@ -45,7 +50,7 @@ export default function OnboardingPage() {
})
}
showToast('All set! Welcome to Next Step.', 'success')
showToast('Welcome to Next Step', 'success')
router.push('/today')
router.refresh()
} catch (err) {
@@ -57,102 +62,177 @@ export default function OnboardingPage() {
if (step === 'disclaimer') {
return (
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4">
<div className="min-h-screen paper-texture flex flex-col items-center justify-center p-6">
<div className="w-full max-w-md">
{/* Decorative blobs */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="blob blob-primary w-96 h-96 -top-48 -right-48" />
<div className="blob blob-accent w-80 h-80 bottom-20 -left-40" />
<div className="blob blob-cream w-64 h-64 top-1/2 right-1/4" />
</div>
<div className={`relative transition-all duration-1000 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
{/* Logo/Icon */}
<div className="text-center mb-8">
<div className="w-16 h-16 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Shield className="w-8 h-8 text-amber-600" />
</div>
<h1 className="text-2xl font-bold text-secondary-900">Important Notice</h1>
<div className="w-24 h-24 rounded-card-lg bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center mx-auto mb-6 shadow-elevated">
<Heart className="w-12 h-12 text-white" />
</div>
<Card className="mb-6">
<div className="space-y-4 text-secondary-700">
<p>
<strong>Next Step is a tracking tool only.</strong> It helps you and your family
stay organized with appointments and medications.
<h1 className="font-display text-display-md text-secondary-900 mb-2">
Next Step
</h1>
<p className="text-secondary-500 text-lg">
Supporting you through every step
</p>
</div>
{/* Disclaimer Card */}
<div className="section-warm mb-6">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 rounded-full bg-accent-100 flex items-center justify-center">
<Shield className="w-6 h-6 text-accent-600" />
</div>
<h2 className="font-display text-xl text-secondary-900">Important Notice</h2>
</div>
<div className="space-y-4 text-secondary-700">
<div className="flex gap-3">
<Sparkles className="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" />
<p>
<strong className="text-red-600">This app does not provide medical advice.</strong>{' '}
<strong className="text-secondary-900">Next Step is a tracking tool only.</strong>{' '}
It helps you and your family stay organized with appointments and medications.
</p>
</div>
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-alert-100 flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-alert-600 text-xs font-bold">!</span>
</div>
<p>
<strong className="text-alert-600">This app does not provide medical advice.</strong>{' '}
Always consult your healthcare team for medical decisions.
</p>
</div>
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-alert-500 flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-white text-xs font-bold">000</span>
</div>
<p>
<strong>For emergencies:</strong> Call 000 (Australia) or your local emergency
services immediately.
<strong>For emergencies:</strong> Call 000 (Australia) or your local emergency services immediately.
</p>
</div>
<div className="flex gap-3">
<Users className="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" />
<p>
If you have questions about your treatment, contact your clinic directly using the
button we'll help you set up.
</p>
<div className="pt-4 border-t border-border">
<p className="text-sm text-secondary-500">
By continuing, you acknowledge that Next Step is for tracking purposes only and
does not replace professional medical advice.
Have questions about your treatment? Contact your clinic directly using the button we'll help you set up.
</p>
</div>
</div>
</Card>
<Button onClick={handleAcceptDisclaimer} fullWidth>
<div className="mt-6 pt-6 border-t border-cream-200">
<p className="text-sm text-secondary-500 text-center">
By continuing, you acknowledge that Next Step is for tracking purposes only and does not replace professional medical advice.
</p>
</div>
</div>
<button
onClick={handleAcceptDisclaimer}
className="btn-primary w-full flex items-center justify-center gap-2 text-lg py-4"
>
I Understand
<ArrowRight className="w-5 h-5 ml-2" />
</Button>
<ArrowRight className="w-5 h-5" />
</button>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4">
<div className="w-full max-w-sm">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-primary-500 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Heart className="w-8 h-8 text-white" />
</div>
<h1 className="text-2xl font-bold text-secondary-900">Set Up Your Plan</h1>
<p className="text-secondary-500 mt-1">Create a workspace to get started</p>
<div className="min-h-screen paper-texture flex flex-col items-center justify-center p-6">
<div className="w-full max-w-md">
{/* Decorative blobs */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="blob blob-primary w-80 h-80 -top-32 right-0" />
<div className="blob blob-cream w-64 h-64 bottom-0 left-0" />
</div>
<Card className="mb-6">
<form onSubmit={handleCreateWorkspace} className="space-y-4">
<Input
label="Workspace Name"
<div className={`relative transition-all duration-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
{/* Header */}
<div className="text-center mb-8">
<div className="w-20 h-20 rounded-card-lg bg-gradient-to-br from-accent-400 to-accent-500 flex items-center justify-center mx-auto mb-6 shadow-elevated">
<Sparkles className="w-10 h-10 text-white" />
</div>
<h1 className="font-display text-display-sm text-secondary-900 mb-2">
Set Up Your Plan
</h1>
<p className="text-secondary-500 text-lg">Create a workspace to get started</p>
</div>
{/* Form */}
<form onSubmit={handleCreateWorkspace} className="section-warm space-y-6">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Workspace Name
<span className="text-alert-500 ml-1">*</span>
</label>
<input
type="text"
value={workspaceName}
onChange={(e) => setWorkspaceName(e.target.value)}
placeholder="e.g., Grace's Plan"
helperText="This is how family members will identify this workspace"
className="input-sanctuary w-full"
required
/>
<Input
label="Clinic Phone Number"
<p className="text-xs text-secondary-400 mt-2">
This is how family members will identify this workspace
</p>
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Clinic Phone Number
<span className="text-secondary-400 font-normal ml-1">(optional)</span>
</label>
<div className="relative">
<Bell className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-secondary-400" />
<input
type="tel"
value={clinicPhone}
onChange={(e) => setClinicPhone(e.target.value)}
placeholder="e.g., 08 9400 1234"
helperText="We'll add a 'Call Clinic' button for quick access"
className="input-sanctuary w-full pl-10"
/>
</div>
<p className="text-xs text-secondary-400 mt-2">
We'll add a quick "Call Clinic" button for easy access
</p>
</div>
{error && (
<p className="text-sm text-red-600 bg-red-50 px-3 py-2 rounded-button">
{error}
</p>
<div className="bg-alert-50 border border-alert-200 rounded-card p-4">
<p className="text-sm text-alert-700">{error}</p>
</div>
)}
<Button type="submit" fullWidth loading={loading}>
Create Workspace
</Button>
<button
type="submit"
disabled={loading}
className="btn-primary w-full text-lg py-4 disabled:opacity-50"
>
{loading ? 'Creating...' : 'Create Workspace'}
</button>
</form>
</Card>
<p className="text-center text-sm text-secondary-500">
<p className="text-center text-sm text-secondary-400 mt-6">
You can add family members later from Settings
</p>
</div>
</div>
</div>
)
}

View File

@@ -1,6 +1,6 @@
'use client'
import { Phone, User, Droplets, AlertTriangle, Activity, Stethoscope } from 'lucide-react'
import { Phone, User, Droplets, AlertTriangle, Activity, Stethoscope, HeartPulse, FileText } from 'lucide-react'
import { format } from 'date-fns'
interface EmergencyInfo {
@@ -39,49 +39,66 @@ export function EmergencyCard({ info, medications, variant = 'full' }: Emergency
}
return (
<div className="bg-red-50 border-2 border-red-200 rounded-xl overflow-hidden">
<div className="bg-surface border-2 border-alert-200 rounded-card-lg overflow-hidden shadow-elevated">
{/* Header */}
<div className="bg-red-600 text-white px-4 py-3">
<div className="flex items-center gap-2">
<AlertTriangle className="w-6 h-6" />
<h2 className="text-xl font-bold">Emergency Information</h2>
<div className="bg-gradient-to-r from-alert-500 to-alert-600 text-white px-6 py-5">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center">
<AlertTriangle className="w-7 h-7" />
</div>
<div>
<h2 className="font-display text-2xl">Emergency Information</h2>
<p className="text-alert-100 text-sm">Critical medical details for emergencies</p>
</div>
</div>
</div>
<div className="p-4 space-y-4">
<div className="p-6 space-y-6">
{/* Patient Info */}
{info.patientName && (
<div className="flex items-start gap-3">
<User className="w-5 h-5 text-red-600 mt-0.5" />
<div>
<p className="text-sm text-red-700 font-medium">Patient Name</p>
<p className="text-lg font-bold text-secondary-900">{info.patientName}</p>
<div className="flex items-start gap-4 bg-cream-50 rounded-card p-4 border border-cream-200">
<div className="w-12 h-12 rounded-full bg-cream-200 flex items-center justify-center flex-shrink-0">
<User className="w-6 h-6 text-cream-700" />
</div>
<div className="flex-1">
<p className="text-xs text-secondary-500 uppercase tracking-wide font-semibold mb-1">
Patient
</p>
<p className="font-display text-xl text-secondary-900">{info.patientName}</p>
{info.patientDOB && (
<p className="text-sm text-secondary-600">DOB: {formatDate(info.patientDOB)}</p>
<p className="text-sm text-secondary-600 mt-1">
Born: {formatDate(info.patientDOB)}
</p>
)}
</div>
</div>
)}
{/* Blood Type */}
{/* Blood Type - Large and prominent */}
{info.bloodType && (
<div className="flex items-start gap-3">
<Droplets className="w-5 h-5 text-red-600 mt-0.5" />
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-alert-100 to-alert-200 flex items-center justify-center flex-shrink-0">
<Droplets className="w-6 h-6 text-alert-600" />
</div>
<div>
<p className="text-sm text-red-700 font-medium">Blood Type</p>
<p className="text-2xl font-bold text-red-600">{info.bloodType}</p>
<p className="text-xs text-secondary-500 uppercase tracking-wide font-semibold">
Blood Type
</p>
<p className="text-3xl font-display text-alert-600">{info.bloodType}</p>
</div>
</div>
)}
{/* Allergies - High visibility */}
{info.allergies && (
<div className="bg-red-100 border border-red-300 rounded-lg p-3">
<div className="bg-alert-50 border-l-4 border-alert-500 rounded-r-card p-5">
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
<AlertTriangle className="w-6 h-6 text-alert-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-red-700 font-bold uppercase">Allergies</p>
<p className="text-secondary-900 font-medium mt-1">{info.allergies}</p>
<p className="text-sm text-alert-700 font-bold uppercase tracking-wide mb-2">
Allergies
</p>
<p className="text-secondary-900 font-medium text-lg">{info.allergies}</p>
</div>
</div>
</div>
@@ -89,10 +106,14 @@ export function EmergencyCard({ info, medications, variant = 'full' }: Emergency
{/* Medical Conditions */}
{info.medicalConditions && (
<div className="flex items-start gap-3">
<Activity className="w-5 h-5 text-red-600 mt-0.5" />
<div>
<p className="text-sm text-red-700 font-medium">Medical Conditions</p>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0">
<HeartPulse className="w-6 h-6 text-primary-600" />
</div>
<div className="flex-1">
<p className="text-xs text-secondary-500 uppercase tracking-wide font-semibold mb-1">
Medical Conditions
</p>
<p className="text-secondary-900">{info.medicalConditions}</p>
</div>
</div>
@@ -100,34 +121,49 @@ export function EmergencyCard({ info, medications, variant = 'full' }: Emergency
{/* Current Medications */}
{variant === 'full' && medications && medications.length > 0 && (
<div className="border-t border-red-200 pt-4">
<p className="text-sm text-red-700 font-bold mb-2">Current Medications</p>
<ul className="space-y-1">
<div className="border-t-2 border-cream-200 pt-6">
<div className="flex items-center gap-2 mb-4">
<FileText className="w-5 h-5 text-primary-500" />
<p className="text-sm text-secondary-700 font-semibold uppercase tracking-wide">
Current Medications
</p>
</div>
<div className="bg-cream-50 rounded-card p-4 border border-cream-200">
<ul className="space-y-3">
{medications.map((med, i) => (
<li key={i} className="text-secondary-900">
<span className="font-medium">{med.name}</span>
<li key={i} className="flex items-start gap-3">
<span className="w-2 h-2 rounded-full bg-primary-400 mt-2 flex-shrink-0" />
<div>
<span className="font-semibold text-secondary-900">{med.name}</span>
{med.instructions && (
<span className="text-secondary-600"> - {med.instructions}</span>
<span className="text-secondary-600"> {med.instructions}</span>
)}
</div>
</li>
))}
</ul>
</div>
</div>
)}
{/* Doctor Info */}
{info.primaryPhysician && (
<div className="border-t border-red-200 pt-4">
<div className="flex items-start gap-3">
<Stethoscope className="w-5 h-5 text-red-600 mt-0.5" />
<div>
<p className="text-sm text-red-700 font-medium">Primary Physician</p>
<p className="text-secondary-900 font-medium">{info.primaryPhysician}</p>
<div className="border-t-2 border-cream-200 pt-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-secondary-100 flex items-center justify-center flex-shrink-0">
<Stethoscope className="w-6 h-6 text-secondary-600" />
</div>
<div className="flex-1">
<p className="text-xs text-secondary-500 uppercase tracking-wide font-semibold mb-1">
Primary Physician
</p>
<p className="font-display text-lg text-secondary-900">{info.primaryPhysician}</p>
{info.physicianPhone && (
<a
href={`tel:${info.physicianPhone}`}
className="text-primary-600 hover:underline"
className="inline-flex items-center gap-2 mt-2 text-primary-600 font-medium hover:text-primary-700 hover:underline"
>
<Phone className="w-4 h-4" />
{info.physicianPhone}
</a>
)}
@@ -138,20 +174,23 @@ export function EmergencyCard({ info, medications, variant = 'full' }: Emergency
{/* Emergency Contacts */}
{(info.clinicPhone || info.emergencyPhone) && (
<div className="border-t border-red-200 pt-4 space-y-3">
<p className="text-sm text-red-700 font-bold">Emergency Contacts</p>
<div className="border-t-2 border-cream-200 pt-6">
<p className="text-sm text-secondary-700 font-semibold uppercase tracking-wide mb-4 flex items-center gap-2">
<Phone className="w-5 h-5 text-alert-500" />
Emergency Contacts
</p>
<div className="grid gap-3">
{info.clinicPhone && (
<a
href={`tel:${info.clinicPhone}`}
className="flex items-center gap-3 p-3 bg-white rounded-lg border border-red-200 hover:bg-red-50 transition-colors"
className="flex items-center gap-4 p-4 bg-alert-50 rounded-card border border-alert-200 hover:bg-alert-100 hover:border-alert-300 hover:shadow-soft transition-all duration-300 group"
>
<div className="w-10 h-10 rounded-full bg-red-600 flex items-center justify-center">
<Phone className="w-5 h-5 text-white" />
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-alert-500 to-alert-600 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
<Phone className="w-7 h-7 text-white" />
</div>
<div>
<p className="font-medium text-secondary-900">Call Clinic</p>
<p className="text-sm text-secondary-600">{info.clinicPhone}</p>
<p className="font-semibold text-secondary-900 text-lg">Call Clinic</p>
<p className="text-alert-600 font-medium">{info.clinicPhone}</p>
</div>
</a>
)}
@@ -159,18 +198,19 @@ export function EmergencyCard({ info, medications, variant = 'full' }: Emergency
{info.emergencyPhone && (
<a
href={`tel:${info.emergencyPhone}`}
className="flex items-center gap-3 p-3 bg-white rounded-lg border border-red-200 hover:bg-red-50 transition-colors"
className="flex items-center gap-4 p-4 bg-cream-50 rounded-card border border-cream-200 hover:bg-cream-100 hover:border-cream-300 hover:shadow-soft transition-all duration-300 group"
>
<div className="w-10 h-10 rounded-full bg-red-600 flex items-center justify-center">
<Phone className="w-5 h-5 text-white" />
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-secondary-500 to-secondary-600 flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform">
<Phone className="w-7 h-7 text-white" />
</div>
<div>
<p className="font-medium text-secondary-900">Emergency Contact</p>
<p className="text-sm text-secondary-600">{info.emergencyPhone}</p>
<p className="font-semibold text-secondary-900 text-lg">Emergency Contact</p>
<p className="text-secondary-600 font-medium">{info.emergencyPhone}</p>
</div>
</a>
)}
</div>
</div>
)}
</div>
</div>

View File

@@ -2,26 +2,27 @@
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Button, Input, Textarea, Select, Card, showToast } from '@/components/ui'
import { Clock, Calendar, Repeat, Pill, Package, ChevronDown, Plus, X } from 'lucide-react'
import { Button, Input, Textarea, Select, showToast } from '@/components/ui'
import { useApp } from '@/app/(app)/provider'
type ScheduleType = 'FIXED_TIMES' | 'INTERVAL' | 'WEEKDAYS' | 'PRN'
const scheduleTypeOptions = [
{ value: 'FIXED_TIMES', label: 'Fixed times daily' },
{ value: 'INTERVAL', label: 'Every X hours' },
{ value: 'WEEKDAYS', label: 'Specific days of week' },
{ value: 'PRN', label: 'As needed (PRN)' },
{ value: 'FIXED_TIMES', label: 'Fixed times daily', icon: Clock, desc: 'Same times every day' },
{ value: 'INTERVAL', label: 'Every X hours', icon: Repeat, desc: 'Regular intervals' },
{ value: 'WEEKDAYS', label: 'Specific days', icon: Calendar, desc: 'Certain days of the week' },
{ value: 'PRN', label: 'As needed (PRN)', icon: Pill, desc: 'When you need it' },
]
const weekdays = [
{ value: 0, label: 'Sun' },
{ value: 1, label: 'Mon' },
{ value: 2, label: 'Tue' },
{ value: 3, label: 'Wed' },
{ value: 4, label: 'Thu' },
{ value: 5, label: 'Fri' },
{ value: 6, label: 'Sat' },
{ value: 0, label: 'Sun', full: 'Sunday' },
{ value: 1, label: 'Mon', full: 'Monday' },
{ value: 2, label: 'Tue', full: 'Tuesday' },
{ value: 3, label: 'Wed', full: 'Wednesday' },
{ value: 4, label: 'Thu', full: 'Thursday' },
{ value: 5, label: 'Fri', full: 'Friday' },
{ value: 6, label: 'Sat', full: 'Saturday' },
]
interface MedicationFormProps {
@@ -42,6 +43,7 @@ interface MedicationFormProps {
export function MedicationForm({ initialData, isEditing = false }: MedicationFormProps) {
const router = useRouter()
const { currentWorkspace, refreshData } = useApp()
const [mounted, setMounted] = useState(false)
const [name, setName] = useState(initialData?.name || '')
const [instructions, setInstructions] = useState(initialData?.instructions || '')
@@ -72,7 +74,10 @@ export function MedicationForm({ initialData, isEditing = false }: MedicationFor
const [error, setError] = useState('')
useEffect(() => {
// Reset defaults if switching types and no initial data for that type
setMounted(true)
}, [])
useEffect(() => {
if (initialData?.scheduleType !== scheduleType) {
// Keep current state if user is just switching around in new mode
}
@@ -134,13 +139,11 @@ export function MedicationForm({ initialData, isEditing = false }: MedicationFor
scheduleType,
scheduleData: buildScheduleData(),
active: true,
// Refill tracking
...(trackRefills && pillCount !== '' && {
pillCount: Number(pillCount),
pillsPerDose,
refillThreshold,
}),
// Explicitly nullify if disabled during edit
...(isEditing && !trackRefills && {
pillCount: null,
pillsPerDose: null,
@@ -164,197 +167,337 @@ export function MedicationForm({ initialData, isEditing = false }: MedicationFor
}
}
const currentScheduleOption = scheduleTypeOptions.find(opt => opt.value === scheduleType)
const ScheduleIcon = currentScheduleOption?.icon || Clock
return (
<Card>
<form onSubmit={handleSubmit} className="space-y-5">
<Input
label="Medication Name"
<div className={`space-y-6 transition-all duration-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
{/* Form Header */}
<div className="text-center mb-8">
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-primary-100 to-primary-200 flex items-center justify-center mx-auto mb-4 shadow-lg">
<Pill className="w-10 h-10 text-primary-600" />
</div>
<h2 className="font-display text-display-sm text-secondary-900">
{isEditing ? 'Edit Medication' : 'Add Medication'}
</h2>
<p className="text-secondary-500 mt-2">
{isEditing ? 'Update your medication details' : 'Keep track of your medications'}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Basic Info Section */}
<div className="section-warm space-y-5">
<h3 className="font-display text-lg text-secondary-900 flex items-center gap-2">
<span className="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center text-primary-600 text-sm font-semibold">1</span>
Basic Information
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Medication Name
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Paracetamol 500mg"
className="input-sanctuary w-full"
required
/>
</div>
<Textarea
label="Instructions (optional)"
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Instructions
<span className="text-secondary-400 font-normal ml-1">(optional)</span>
</label>
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="e.g., Take with food"
placeholder="e.g., Take with food, Avoid grapefruit..."
rows={2}
className="input-sanctuary w-full resize-none"
/>
</div>
</div>
</div>
<Select
label="Schedule Type"
value={scheduleType}
onChange={(e) => setScheduleType(e.target.value as ScheduleType)}
options={scheduleTypeOptions}
/>
{/* Schedule Section */}
<div className="section-warm space-y-5">
<h3 className="font-display text-lg text-secondary-900 flex items-center gap-2">
<span className="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center text-primary-600 text-sm font-semibold">2</span>
Schedule
</h3>
{/* Schedule Type Selector */}
<div className="grid grid-cols-2 gap-3">
{scheduleTypeOptions.map((option) => {
const Icon = option.icon
const isSelected = scheduleType === option.value
return (
<button
key={option.value}
type="button"
onClick={() => setScheduleType(option.value as ScheduleType)}
className={`p-4 rounded-card border-2 text-left transition-all duration-300 ${
isSelected
? 'border-primary-400 bg-primary-50/50 shadow-soft'
: 'border-cream-200 bg-surface hover:border-cream-300 hover:shadow-soft'
}`}
>
<Icon className={`w-6 h-6 mb-2 ${isSelected ? 'text-primary-600' : 'text-secondary-400'}`} />
<p className={`font-semibold text-sm ${isSelected ? 'text-primary-800' : 'text-secondary-700'}`}>
{option.label}
</p>
<p className="text-xs text-secondary-400 mt-0.5">{option.desc}</p>
</button>
)
})}
</div>
{/* Schedule-specific options */}
<div className="bg-cream-50/50 rounded-card p-5 border border-cream-200/60">
{scheduleType === 'FIXED_TIMES' && (
<div className="space-y-3">
<div className="space-y-4">
<label className="block text-sm font-medium text-secondary-700">
Times to take
Times to take each day
</label>
<div className="space-y-3">
{times.map((time, index) => (
<div key={index} className="flex gap-2">
<Input
<div key={index} className="flex gap-2 items-center">
<div className="flex-1 relative">
<Clock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-secondary-400" />
<input
type="time"
value={time}
onChange={(e) => updateTime(index, e.target.value)}
className="flex-1"
className="input-sanctuary w-full pl-10"
/>
</div>
{times.length > 1 && (
<Button
<button
type="button"
variant="ghost"
onClick={() => removeTime(index)}
className="w-10 h-10 rounded-button bg-cream-100 hover:bg-cream-200 flex items-center justify-center text-secondary-500 transition-colors"
>
Remove
</Button>
<X className="w-5 h-5" />
</button>
)}
</div>
))}
<Button type="button" variant="secondary" onClick={addTime} size="sm">
+ Add time
</Button>
</div>
<button
type="button"
onClick={addTime}
className="btn-secondary w-full flex items-center justify-center gap-2 text-sm"
>
<Plus className="w-4 h-4" />
Add another time
</button>
</div>
)}
{scheduleType === 'INTERVAL' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<Input
label="Every (hours)"
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Every (hours)
</label>
<input
type="number"
min={1}
max={72}
value={intervalHours}
onChange={(e) => setIntervalHours(parseInt(e.target.value) || 1)}
className="input-sanctuary w-full"
/>
<Input
label="Starting at"
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Starting at
</label>
<div className="relative">
<Clock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-secondary-400" />
<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="input-sanctuary w-full pl-10"
/>
</div>
</div>
</div>
<p className="text-sm text-secondary-500">
Example: Every {intervalHours} hours starting at {startTime}
</p>
</div>
)}
{scheduleType === 'WEEKDAYS' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Days
<label className="block text-sm font-medium text-secondary-700 mb-3">
Which days?
</label>
<div className="flex gap-2 flex-wrap">
{weekdays.map((day) => (
<div className="grid grid-cols-7 gap-2">
{weekdays.map((day) => {
const isSelected = selectedDays.includes(day.value)
return (
<button
key={day.value}
type="button"
onClick={() => toggleDay(day.value)}
className={`px-3 py-2 rounded-button text-sm font-medium transition-colors ${
selectedDays.includes(day.value)
? 'bg-primary-500 text-white'
: 'bg-muted text-secondary-600 hover:bg-secondary-200'
className={`aspect-square rounded-button text-sm font-medium transition-all duration-200 ${
isSelected
? 'bg-primary-500 text-white shadow-lg scale-105'
: 'bg-cream-100 text-secondary-600 hover:bg-cream-200'
}`}
title={day.full}
>
{day.label}
</button>
))}
)
})}
</div>
</div>
<Input
label="Time"
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
At what time?
</label>
<div className="relative">
<Clock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-secondary-400" />
<input
type="time"
value={weekdayTime}
onChange={(e) => setWeekdayTime(e.target.value)}
className="input-sanctuary w-full pl-10"
/>
</div>
</div>
</div>
)}
{scheduleType === 'PRN' && (
<Input
label="Minimum hours between doses"
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Minimum hours between doses
</label>
<input
type="number"
min={0.5}
max={72}
step={0.5}
value={minHoursBetween}
onChange={(e) => setMinHoursBetween(parseFloat(e.target.value) || 4)}
helperText="Shows 'Available' when enough time has passed since last dose"
className="input-sanctuary w-full"
/>
</div>
<p className="text-sm text-secondary-500">
Shows "Available" when enough time has passed since your last dose
</p>
</div>
)}
</div>
</div>
{/* Refill Tracking (optional) */}
<div className="border-t border-border pt-5">
<div className="flex items-center gap-3 mb-4">
{/* Refill Tracking Section */}
<div className="section-warm space-y-5">
<h3 className="font-display text-lg text-secondary-900 flex items-center gap-2">
<span className="w-8 h-8 rounded-full bg-cream-200 flex items-center justify-center text-secondary-600 text-sm font-semibold">
<Package className="w-4 h-4" />
</span>
Refill Tracking
<span className="text-sm font-normal text-secondary-400 ml-auto">Optional</span>
</h3>
<div className="flex items-center gap-3">
<input
type="checkbox"
id="trackRefills"
checked={trackRefills}
onChange={(e) => setTrackRefills(e.target.checked)}
className="w-5 h-5 rounded border-border text-primary-600 focus:ring-primary-500"
className="w-5 h-5 rounded border-cream-300 text-primary-500 focus:ring-primary-400"
/>
<label htmlFor="trackRefills" className="text-sm font-medium text-secondary-700">
Track pill count for refill reminders (optional)
<label htmlFor="trackRefills" className="text-sm text-secondary-700">
Track pill count and get refill reminders
</label>
</div>
{trackRefills && (
<div className="space-y-4 pl-8">
<Input
label="Current pill count"
<div className="space-y-4 pt-2 pl-8 border-l-2 border-cream-200">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Current pill count
</label>
<input
type="number"
min={0}
value={pillCount}
onChange={(e) => setPillCount(e.target.value === '' ? '' : parseInt(e.target.value))}
placeholder="e.g., 30"
helperText="How many pills do you have now?"
className="input-sanctuary w-full"
/>
<div className="grid grid-cols-2 gap-3">
<Input
label="Pills per dose"
<p className="text-xs text-secondary-400 mt-1.5">How many pills do you have now?</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Pills per dose
</label>
<input
type="number"
min={1}
value={pillsPerDose}
onChange={(e) => setPillsPerDose(parseInt(e.target.value) || 1)}
className="input-sanctuary w-full"
/>
<Input
label="Alert when below"
</div>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-2">
Alert when below
</label>
<input
type="number"
min={0}
value={refillThreshold}
onChange={(e) => setRefillThreshold(parseInt(e.target.value) || 7)}
helperText="pills"
className="input-sanctuary w-full"
/>
<p className="text-xs text-secondary-400 mt-1.5">pills remaining</p>
</div>
</div>
</div>
)}
</div>
{error && (
<p className="text-sm text-red-600 bg-red-50 px-3 py-2 rounded-button">
{error}
</p>
<div className="bg-alert-50 border border-alert-200 rounded-card p-4">
<p className="text-sm text-alert-700">{error}</p>
</div>
)}
<div className="flex gap-3 pt-2">
<Button
{/* Action Buttons */}
<div className="flex gap-3 pt-4">
<button
type="button"
variant="secondary"
fullWidth
onClick={() => router.back()}
className="btn-secondary flex-1"
>
Cancel
</Button>
<Button type="submit" fullWidth loading={loading}>
{isEditing ? 'Update Medication' : 'Save Medication'}
</Button>
</button>
<button
type="submit"
disabled={loading}
className="btn-primary flex-1 disabled:opacity-50"
>
{loading ? 'Saving...' : isEditing ? 'Update Medication' : 'Save Medication'}
</button>
</div>
</form>
</Card>
</div>
)
}

View File

@@ -52,6 +52,18 @@ export function NotificationPermission({ workspaceId }: NotificationPermissionPr
if (registration.pushManager) {
const subscription = await registration.pushManager.getSubscription()
setIsSubscribed(!!subscription)
// Auto-sync subscription to server to ensure it exists
if (subscription) {
fetch('/api/notifications/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
subscription: subscription.toJSON(),
workspaceId,
}),
}).catch(console.error)
}
}
} catch (err) {
console.error('Failed to check subscription:', err)

View File

@@ -9,55 +9,93 @@ const config: Config = {
theme: {
extend: {
colors: {
// Calm, healing palette
// Warm Sanctuary - Primary: Soft sage green (healing, calm)
primary: {
50: '#f0f9f4',
100: '#dcf1e4',
200: '#bbe3cc',
300: '#8dcda8',
400: '#5bb17f',
500: '#3a9563',
600: '#2a784e',
700: '#235f40',
800: '#1f4c35',
900: '#1b3f2d',
950: '#0d2319',
50: '#f4f7f4',
100: '#e3ebe3',
200: '#c5d9c5',
300: '#9bbf9b',
400: '#729f72',
500: '#528252',
600: '#3f663f',
700: '#345234',
800: '#2b412b',
900: '#243624',
950: '#121f12',
},
// Warm neutrals - cream, stone, warm gray
cream: {
50: '#fdfcfa',
100: '#faf7f2',
200: '#f5efe6',
300: '#ede3d5',
400: '#e0d0bc',
500: '#d4bfa3',
600: '#c4a882',
700: '#a88b65',
800: '#8a7255',
900: '#705d47',
950: '#3d3226',
},
// Secondary: Warm stone gray (sophisticated, grounded)
secondary: {
50: '#f5f7fa',
100: '#ebeef3',
200: '#d2dae5',
300: '#aab9ce',
400: '#7c93b3',
500: '#5c769a',
600: '#485e80',
700: '#3b4d68',
800: '#344257',
900: '#2f3a4a',
950: '#1f2631',
50: '#f8f7f6',
100: '#f0eeeb',
200: '#e0dcd5',
300: '#ccc6bb',
400: '#b5ad9f',
500: '#a09484',
600: '#857a6d',
700: '#6d6359',
800: '#5a524a',
900: '#4a443f',
950: '#262320',
},
// Accent: Terracotta (warmth, energy, gentle urgency)
accent: {
50: '#fef6ee',
100: '#fdebd7',
200: '#fad3ae',
300: '#f6b37b',
400: '#f18946',
500: '#ed6b22',
600: '#de5118',
700: '#b83c16',
800: '#93311a',
900: '#772b18',
950: '#40130b',
50: '#fdf8f6',
100: '#faeee9',
200: '#f5dcd2',
300: '#ecc0b0',
400: '#e09b82',
500: '#d67b58',
600: '#c6603e',
700: '#a54c30',
800: '#88402b',
900: '#703728',
950: '#3d1a11',
},
background: '#fafbfc',
// Alert red (emergency - softened)
alert: {
50: '#fdf5f4',
100: '#fce8e6',
200: '#f9d5d2',
300: '#f4b7b1',
400: '#ec8c85',
500: '#e0635a',
600: '#c9453d',
700: '#a83832',
800: '#8b322e',
900: '#742f2c',
950: '#3e1514',
},
// Semantic aliases
background: '#faf7f2',
surface: '#ffffff',
muted: '#f1f5f9',
border: '#e2e8f0',
muted: '#f0eeeb',
border: '#e0dcd5',
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
// Playfair Display for elegant headings
display: ['Playfair Display', 'Georgia', 'serif'],
// Source Sans 3 for warm, readable body text
sans: ['Source Sans 3', 'system-ui', 'sans-serif'],
},
fontSize: {
'display-xl': ['3.5rem', { lineHeight: '1.1', letterSpacing: '-0.02em' }],
'display-lg': ['2.75rem', { lineHeight: '1.15', letterSpacing: '-0.02em' }],
'display-md': ['2.25rem', { lineHeight: '1.2', letterSpacing: '-0.01em' }],
'display-sm': ['1.875rem', { lineHeight: '1.25', letterSpacing: '-0.01em' }],
// Large text mode sizes
'lg-base': '1.125rem',
'lg-lg': '1.25rem',
@@ -67,17 +105,61 @@ const config: Config = {
},
spacing: {
// Touch-friendly spacing
'touch': '44px',
'touch-lg': '56px',
'touch': '48px',
'touch-lg': '60px',
},
borderRadius: {
'card': '16px',
'button': '12px',
'card': '20px',
'card-lg': '28px',
'button': '14px',
'pill': '9999px',
},
boxShadow: {
'card': '0 1px 3px 0 rgb(0 0 0 / 0.04), 0 1px 2px -1px rgb(0 0 0 / 0.04)',
'card-hover': '0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05)',
'button': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
// Soft, warm shadows
'card': '0 2px 8px -2px rgba(93, 82, 70, 0.06), 0 4px 16px -4px rgba(93, 82, 70, 0.04)',
'card-hover': '0 8px 24px -4px rgba(93, 82, 70, 0.08), 0 4px 12px -2px rgba(93, 82, 70, 0.05)',
'button': '0 1px 3px rgba(93, 82, 70, 0.08)',
'button-hover': '0 4px 12px -2px rgba(93, 82, 70, 0.15)',
'soft': '0 2px 16px rgba(93, 82, 70, 0.06)',
'elevated': '0 8px 32px -4px rgba(93, 82, 70, 0.1)',
},
animation: {
// Gentle, breathing animations
'breathe': 'breathe 4s ease-in-out infinite',
'fade-up': 'fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards',
'fade-in': 'fadeIn 0.4s ease-out forwards',
'scale-in': 'scaleIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards',
'slide-up': 'slideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards',
'pulse-soft': 'pulseSoft 2s ease-in-out infinite',
},
keyframes: {
breathe: {
'0%, 100%': { opacity: '1', transform: 'scale(1)' },
'50%': { opacity: '0.9', transform: 'scale(1.02)' },
},
fadeUp: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
scaleIn: {
'0%': { opacity: '0', transform: 'scale(0.95)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(30px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
pulseSoft: {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0.7' },
},
},
transitionTimingFunction: {
'sanctuary': 'cubic-bezier(0.16, 1, 0.3, 1)',
},
},
},