feat: Implement Phase 1 enhancements - components and animations
Enterprise AI Code Review / ai-review (pull_request) Successful in 19s

Components:
- Extract ServiceItem component with hover effects
- Extract SupportItem component with hover animations
- Extract Card component with scroll animations
- Extract Section component for consistent layout

Enhancements:
- Add Intersection Observer for scroll-triggered animations
- Implement hover effects on cards, services, and support items
- Add PWA manifest (site.webmanifest)
- Create SVG OG image placeholder for social sharing
- Add comprehensive favicon meta tags (16x16, 32x32, apple-touch-icon)
- Respect prefers-reduced-motion for accessibility

Files modified:
- src/pages/index.astro - Refactored to use new components
- src/layouts/BaseLayout.astro - Enhanced favicon and meta tags

Files created:
- src/components/Card.astro
- src/components/Section.astro
- src/components/ServiceItem.astro
- src/components/SupportItem.astro
- public/og-image.svg
- public/site.webmanifest
- public/FAVICON_INSTRUCTIONS.md

Note: PNG favicon variants still need to be generated (see FAVICON_INSTRUCTIONS.md)
This commit is contained in:
2025-12-31 18:12:11 +00:00
parent 51db744cc1
commit 6ed0eee514
9 changed files with 622 additions and 351 deletions
+74
View File
@@ -0,0 +1,74 @@
---
interface Props {
class?: string;
}
const { class: className } = Astro.props;
---
<div class:list={["card", "scroll-animate", className]}>
<slot />
</div>
<style>
.card {
background: var(--color-bg-light);
border-radius: 12px;
padding: var(--space-lg);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(212, 165, 116, 0.1);
transition: box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
}
.card :global(h2) {
font-size: 2rem;
margin-bottom: var(--space-md);
}
.card :global(p) {
margin-bottom: var(--space-md);
color: var(--color-text);
}
.card :global(p:last-child) {
margin-bottom: 0;
}
/* Scroll animation states */
.scroll-animate {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.scroll-animate.visible {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
.card:hover {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.scroll-animate {
opacity: 1;
transform: none;
transition: none;
}
}
@media (max-width: 768px) {
.card :global(h2) {
font-size: 1.5rem;
}
}
</style>
+27
View File
@@ -0,0 +1,27 @@
---
interface Props {
class?: string;
ariaLabelledby?: string;
}
const { class: className, ariaLabelledby } = Astro.props;
---
<section class:list={["section", className]} aria-labelledby={ariaLabelledby}>
<div class="container">
<slot />
</div>
</section>
<style>
.section {
padding: var(--space-lg) 0;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 var(--space-md);
width: 100%;
}
</style>
+68
View File
@@ -0,0 +1,68 @@
---
interface Props {
title: string;
description: string;
url?: string;
comingSoon?: boolean;
}
const { title, description, url, comingSoon = false } = Astro.props;
---
<div class="service-item" class:list={{ "coming-soon": comingSoon }} role="listitem">
<h3>
{url ? (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
aria-label={`Visit ${title}`}
>
{title}
</a>
) : (
title
)}
</h3>
<p>{description}</p>
</div>
<style>
.service-item {
background: var(--color-bg);
padding: var(--space-md);
border-radius: 8px;
border-left: 4px solid var(--color-accent);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.service-item:hover {
transform: translateX(4px);
box-shadow: 0 4px 8px rgba(212, 165, 116, 0.2);
}
.service-item.coming-soon {
border-left-color: var(--color-warm);
opacity: 0.8;
}
.service-item h3 {
margin-bottom: var(--space-xs);
font-size: 1.25rem;
}
.service-item p {
color: var(--color-text-dim);
margin: 0;
}
@media (prefers-reduced-motion: reduce) {
.service-item {
transition: none;
}
.service-item:hover {
transform: none;
}
}
</style>
+58
View File
@@ -0,0 +1,58 @@
---
interface Props {
title: string;
description: string;
comingSoonNote?: string;
}
const { title, description, comingSoonNote } = Astro.props;
---
<div class="support-item" role="listitem">
<h3>{title}</h3>
<p>
{description}
{comingSoonNote && (
<span class="coming-soon-text">({comingSoonNote})</span>
)}
</p>
</div>
<style>
.support-item {
background: var(--color-bg);
padding: var(--space-md);
border-radius: 8px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.support-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(212, 165, 116, 0.15);
}
.support-item h3 {
margin-bottom: var(--space-xs);
font-size: 1.25rem;
}
.support-item p {
color: var(--color-text-dim);
margin: 0;
}
.coming-soon-text {
font-style: italic;
color: var(--color-warm);
}
@media (prefers-reduced-motion: reduce) {
.support-item {
transition: none;
}
.support-item:hover {
transform: none;
}
}
</style>