- Add WP Media Library integration for product and variation images - Support images array (URLs) conversion to attachment IDs - Add images array to API responses (Admin & Customer SPA) - Implement drag-and-drop sortable images in Admin product form - Add image gallery thumbnails in Customer SPA product page - Initialize WooCommerce session for guest cart operations - Fix product variations and attributes display in Customer SPA - Add variation image field in Admin SPA Changes: - includes/Api/ProductsController.php: Handle images array, add to responses - includes/Frontend/ShopController.php: Add images array for customer SPA - includes/Frontend/CartController.php: Initialize WC session for guests - admin-spa/src/lib/wp-media.ts: Add openWPMediaGallery function - admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx: WP Media + sortable images - admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx: Add variation image field - customer-spa/src/pages/Product/index.tsx: Add gallery thumbnails display
777 lines
16 KiB
Markdown
777 lines
16 KiB
Markdown
# WooNooW Customer SPA Theme System
|
|
|
|
## 🎨 Design Philosophy
|
|
|
|
**SaaS Approach:** Curated options over infinite flexibility
|
|
|
|
- ✅ 4 master layouts (not infinite themes)
|
|
- Classic, Modern, Boutique (multi-product stores)
|
|
- Launch (single product funnels) 🆕
|
|
- ✅ Design tokens (not custom CSS)
|
|
- ✅ Preset combinations (not freestyle design)
|
|
- ✅ Accessibility built-in (WCAG 2.1 AA)
|
|
- ✅ Performance optimized (Core Web Vitals)
|
|
|
|
---
|
|
|
|
## 🏗️ Theme Architecture
|
|
|
|
### Design Token System
|
|
|
|
All styling is controlled via CSS custom properties (design tokens):
|
|
|
|
```css
|
|
:root {
|
|
/* Colors */
|
|
--color-primary: #3B82F6;
|
|
--color-secondary: #8B5CF6;
|
|
--color-accent: #10B981;
|
|
--color-background: #FFFFFF;
|
|
--color-text: #1F2937;
|
|
|
|
/* Typography */
|
|
--font-heading: 'Inter', sans-serif;
|
|
--font-body: 'Lora', serif;
|
|
--font-size-base: 16px;
|
|
--line-height-base: 1.5;
|
|
|
|
/* Spacing (8px grid) */
|
|
--space-1: 0.5rem; /* 8px */
|
|
--space-2: 1rem; /* 16px */
|
|
--space-3: 1.5rem; /* 24px */
|
|
--space-4: 2rem; /* 32px */
|
|
--space-6: 3rem; /* 48px */
|
|
--space-8: 4rem; /* 64px */
|
|
|
|
/* Border Radius */
|
|
--radius-sm: 0.25rem; /* 4px */
|
|
--radius-md: 0.5rem; /* 8px */
|
|
--radius-lg: 1rem; /* 16px */
|
|
|
|
/* Shadows */
|
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
|
|
|
/* Transitions */
|
|
--transition-fast: 150ms ease;
|
|
--transition-base: 250ms ease;
|
|
--transition-slow: 350ms ease;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📐 Master Layouts
|
|
|
|
### 1. Classic Layout
|
|
|
|
**Target Audience:** Traditional ecommerce, B2B
|
|
|
|
**Characteristics:**
|
|
- Header: Logo left, menu right, search bar
|
|
- Shop: Sidebar filters (left), product grid (right)
|
|
- Product: Image gallery left, details right
|
|
- Footer: 4-column widget areas
|
|
|
|
**File:** `customer-spa/src/layouts/ClassicLayout.tsx`
|
|
|
|
```typescript
|
|
export function ClassicLayout({ children }) {
|
|
return (
|
|
<div className="classic-layout">
|
|
<Header variant="classic" />
|
|
<main className="classic-main">
|
|
{children}
|
|
</main>
|
|
<Footer variant="classic" />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**CSS:**
|
|
```css
|
|
.classic-layout {
|
|
--header-height: 80px;
|
|
--sidebar-width: 280px;
|
|
}
|
|
|
|
.classic-main {
|
|
display: grid;
|
|
grid-template-columns: var(--sidebar-width) 1fr;
|
|
gap: var(--space-6);
|
|
max-width: 1280px;
|
|
margin: 0 auto;
|
|
padding: var(--space-6);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.classic-main {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Modern Layout (Default)
|
|
|
|
**Target Audience:** Fashion, lifestyle, modern brands
|
|
|
|
**Characteristics:**
|
|
- Header: Centered logo, minimal menu
|
|
- Shop: Top filters (no sidebar), large product cards
|
|
- Product: Full-width gallery, sticky details
|
|
- Footer: Minimal, centered
|
|
|
|
**File:** `customer-spa/src/layouts/ModernLayout.tsx`
|
|
|
|
```typescript
|
|
export function ModernLayout({ children }) {
|
|
return (
|
|
<div className="modern-layout">
|
|
<Header variant="modern" />
|
|
<main className="modern-main">
|
|
{children}
|
|
</main>
|
|
<Footer variant="modern" />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**CSS:**
|
|
```css
|
|
.modern-layout {
|
|
--header-height: 100px;
|
|
--content-max-width: 1440px;
|
|
}
|
|
|
|
.modern-main {
|
|
max-width: var(--content-max-width);
|
|
margin: 0 auto;
|
|
padding: var(--space-8) var(--space-4);
|
|
}
|
|
|
|
.modern-layout .product-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: var(--space-6);
|
|
}
|
|
```
|
|
|
|
### 3. Boutique Layout
|
|
|
|
**Target Audience:** Luxury, high-end fashion
|
|
|
|
**Characteristics:**
|
|
- Header: Full-width, transparent overlay
|
|
- Shop: Masonry grid, elegant typography
|
|
- Product: Minimal UI, focus on imagery
|
|
- Footer: Elegant, serif typography
|
|
|
|
**File:** `customer-spa/src/layouts/BoutiqueLayout.tsx`
|
|
|
|
```typescript
|
|
export function BoutiqueLayout({ children }) {
|
|
return (
|
|
<div className="boutique-layout">
|
|
<Header variant="boutique" />
|
|
<main className="boutique-main">
|
|
{children}
|
|
</main>
|
|
<Footer variant="boutique" />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**CSS:**
|
|
```css
|
|
.boutique-layout {
|
|
--header-height: 120px;
|
|
--content-max-width: 1600px;
|
|
font-family: var(--font-heading);
|
|
}
|
|
|
|
.boutique-main {
|
|
max-width: var(--content-max-width);
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.boutique-layout .product-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
|
gap: var(--space-8);
|
|
}
|
|
```
|
|
|
|
### 4. Launch Layout 🆕 (Single Product Funnel)
|
|
|
|
**Target Audience:** Single product sellers, course creators, SaaS, product launchers
|
|
|
|
**Important:** Landing page is **fully custom** (user builds with their page builder). WooNooW SPA only takes over **from checkout onwards** after CTA button is clicked.
|
|
|
|
**Characteristics:**
|
|
- **Landing page:** User's custom design (Elementor, Divi, etc.) - NOT controlled by WooNooW
|
|
- **Checkout onwards:** WooNooW SPA takes full control
|
|
- **No traditional header/footer** on SPA pages (distraction-free)
|
|
- **Streamlined checkout** (one-page, minimal fields, no cart)
|
|
- **Upsell opportunity** on thank you page
|
|
- **Direct access** to product in My Account
|
|
|
|
**Page Flow:**
|
|
```
|
|
Landing Page (Custom - User's Page Builder)
|
|
↓
|
|
[CTA Button Click] ← User directs to /checkout
|
|
↓
|
|
Checkout (WooNooW SPA - Full screen, no distractions)
|
|
↓
|
|
Thank You (WooNooW SPA - Upsell/downsell opportunity)
|
|
↓
|
|
My Account (WooNooW SPA - Access product/download)
|
|
```
|
|
|
|
**Technical Note:**
|
|
- Landing page URL: Any (/, /landing, /offer, etc.)
|
|
- CTA button links to: `/checkout` or `/checkout?add-to-cart=123`
|
|
- WooNooW SPA activates only on checkout, thank you, and account pages
|
|
- This is essentially **Checkout-Only mode** with optimized funnel design
|
|
|
|
**File:** `customer-spa/src/layouts/LaunchLayout.tsx`
|
|
|
|
```typescript
|
|
export function LaunchLayout({ children }) {
|
|
const location = useLocation();
|
|
const isLandingPage = location.pathname === '/' || location.pathname === '/shop';
|
|
|
|
return (
|
|
<div className="launch-layout">
|
|
{/* Minimal header only on non-landing pages */}
|
|
{!isLandingPage && <Header variant="minimal" />}
|
|
|
|
<main className="launch-main">
|
|
{children}
|
|
</main>
|
|
|
|
{/* No footer on landing page */}
|
|
{!isLandingPage && <Footer variant="minimal" />}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**CSS:**
|
|
```css
|
|
.launch-layout {
|
|
--content-max-width: 1200px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.launch-main {
|
|
max-width: var(--content-max-width);
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Landing page: full-screen hero */
|
|
.launch-landing {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
text-align: center;
|
|
padding: var(--space-8);
|
|
}
|
|
|
|
.launch-landing .hero-title {
|
|
font-size: var(--text-5xl);
|
|
font-weight: 700;
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.launch-landing .hero-subtitle {
|
|
font-size: var(--text-xl);
|
|
margin-bottom: var(--space-8);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.launch-landing .cta-button {
|
|
font-size: var(--text-xl);
|
|
padding: var(--space-4) var(--space-8);
|
|
min-width: 300px;
|
|
}
|
|
|
|
/* Checkout: streamlined, no distractions */
|
|
.launch-checkout {
|
|
max-width: 600px;
|
|
margin: var(--space-8) auto;
|
|
padding: var(--space-6);
|
|
}
|
|
|
|
/* Thank you: upsell opportunity */
|
|
.launch-thankyou {
|
|
max-width: 800px;
|
|
margin: var(--space-8) auto;
|
|
text-align: center;
|
|
}
|
|
|
|
.launch-thankyou .upsell-section {
|
|
margin-top: var(--space-8);
|
|
padding: var(--space-6);
|
|
border: 2px solid var(--color-primary);
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
```
|
|
|
|
**Perfect For:**
|
|
- Digital products (courses, ebooks, software)
|
|
- SaaS trial → paid conversions
|
|
- Webinar funnels
|
|
- High-ticket consulting
|
|
- Limited-time offers
|
|
- Crowdfunding campaigns
|
|
- Product launches
|
|
|
|
**Competitive Advantage:**
|
|
Replaces expensive tools like CartFlows ($297-997/year) with built-in, optimized funnel.
|
|
|
|
---
|
|
|
|
## 🎨 Color System
|
|
|
|
### Color Palette Generation
|
|
|
|
When user sets primary color, we auto-generate shades:
|
|
|
|
```typescript
|
|
function generateColorShades(baseColor: string) {
|
|
return {
|
|
50: lighten(baseColor, 0.95),
|
|
100: lighten(baseColor, 0.90),
|
|
200: lighten(baseColor, 0.75),
|
|
300: lighten(baseColor, 0.60),
|
|
400: lighten(baseColor, 0.40),
|
|
500: baseColor, // Base color
|
|
600: darken(baseColor, 0.10),
|
|
700: darken(baseColor, 0.20),
|
|
800: darken(baseColor, 0.30),
|
|
900: darken(baseColor, 0.40),
|
|
};
|
|
}
|
|
```
|
|
|
|
### Contrast Checking
|
|
|
|
Ensure WCAG AA compliance:
|
|
|
|
```typescript
|
|
function ensureContrast(textColor: string, bgColor: string) {
|
|
const contrast = getContrastRatio(textColor, bgColor);
|
|
|
|
if (contrast < 4.5) {
|
|
// Adjust text color for better contrast
|
|
return adjustColorForContrast(textColor, bgColor, 4.5);
|
|
}
|
|
|
|
return textColor;
|
|
}
|
|
```
|
|
|
|
### Dark Mode Support
|
|
|
|
```css
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--color-background: #1F2937;
|
|
--color-text: #F9FAFB;
|
|
/* Invert shades */
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 Typography System
|
|
|
|
### Typography Presets
|
|
|
|
#### Professional
|
|
```css
|
|
:root {
|
|
--font-heading: 'Inter', -apple-system, sans-serif;
|
|
--font-body: 'Lora', Georgia, serif;
|
|
--font-weight-heading: 700;
|
|
--font-weight-body: 400;
|
|
}
|
|
```
|
|
|
|
#### Modern
|
|
```css
|
|
:root {
|
|
--font-heading: 'Poppins', -apple-system, sans-serif;
|
|
--font-body: 'Roboto', -apple-system, sans-serif;
|
|
--font-weight-heading: 600;
|
|
--font-weight-body: 400;
|
|
}
|
|
```
|
|
|
|
#### Elegant
|
|
```css
|
|
:root {
|
|
--font-heading: 'Playfair Display', Georgia, serif;
|
|
--font-body: 'Source Sans Pro', -apple-system, sans-serif;
|
|
--font-weight-heading: 700;
|
|
--font-weight-body: 400;
|
|
}
|
|
```
|
|
|
|
#### Tech
|
|
```css
|
|
:root {
|
|
--font-heading: 'Space Grotesk', monospace;
|
|
--font-body: 'IBM Plex Mono', monospace;
|
|
--font-weight-heading: 700;
|
|
--font-weight-body: 400;
|
|
}
|
|
```
|
|
|
|
### Type Scale
|
|
|
|
```css
|
|
:root {
|
|
--text-xs: 0.75rem; /* 12px */
|
|
--text-sm: 0.875rem; /* 14px */
|
|
--text-base: 1rem; /* 16px */
|
|
--text-lg: 1.125rem; /* 18px */
|
|
--text-xl: 1.25rem; /* 20px */
|
|
--text-2xl: 1.5rem; /* 24px */
|
|
--text-3xl: 1.875rem; /* 30px */
|
|
--text-4xl: 2.25rem; /* 36px */
|
|
--text-5xl: 3rem; /* 48px */
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🧩 Component Theming
|
|
|
|
### Button Component
|
|
|
|
```typescript
|
|
// components/ui/button.tsx
|
|
export function Button({ variant = 'primary', ...props }) {
|
|
return (
|
|
<button
|
|
className={cn('btn', `btn-${variant}`)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
```css
|
|
.btn {
|
|
font-family: var(--font-heading);
|
|
font-weight: 600;
|
|
padding: var(--space-2) var(--space-4);
|
|
border-radius: var(--radius-md);
|
|
transition: all var(--transition-base);
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--color-primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: var(--color-primary-600);
|
|
transform: translateY(-1px);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
```
|
|
|
|
### Product Card Component
|
|
|
|
```typescript
|
|
// components/ProductCard.tsx
|
|
export function ProductCard({ product, layout }) {
|
|
const theme = useTheme();
|
|
|
|
return (
|
|
<div className={cn('product-card', `product-card-${layout}`)}>
|
|
<img src={product.image} alt={product.name} />
|
|
<h3>{product.name}</h3>
|
|
<p className="price">{product.price}</p>
|
|
<Button variant="primary">Add to Cart</Button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
```css
|
|
.product-card {
|
|
background: var(--color-background);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
transition: all var(--transition-base);
|
|
}
|
|
|
|
.product-card:hover {
|
|
box-shadow: var(--shadow-lg);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.product-card-modern {
|
|
/* Modern layout specific styles */
|
|
padding: var(--space-4);
|
|
}
|
|
|
|
.product-card-boutique {
|
|
/* Boutique layout specific styles */
|
|
padding: 0;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎭 Theme Provider (React Context)
|
|
|
|
### Implementation
|
|
|
|
**File:** `customer-spa/src/contexts/ThemeContext.tsx`
|
|
|
|
```typescript
|
|
import { createContext, useContext, useEffect, ReactNode } from 'react';
|
|
|
|
interface ThemeConfig {
|
|
layout: 'classic' | 'modern' | 'boutique';
|
|
colors: {
|
|
primary: string;
|
|
secondary: string;
|
|
accent: string;
|
|
background: string;
|
|
text: string;
|
|
};
|
|
typography: {
|
|
preset: string;
|
|
customFonts?: {
|
|
heading: string;
|
|
body: string;
|
|
};
|
|
};
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeConfig | null>(null);
|
|
|
|
export function ThemeProvider({
|
|
config,
|
|
children
|
|
}: {
|
|
config: ThemeConfig;
|
|
children: ReactNode;
|
|
}) {
|
|
useEffect(() => {
|
|
// Inject CSS variables
|
|
const root = document.documentElement;
|
|
|
|
// Colors
|
|
root.style.setProperty('--color-primary', config.colors.primary);
|
|
root.style.setProperty('--color-secondary', config.colors.secondary);
|
|
root.style.setProperty('--color-accent', config.colors.accent);
|
|
root.style.setProperty('--color-background', config.colors.background);
|
|
root.style.setProperty('--color-text', config.colors.text);
|
|
|
|
// Typography
|
|
loadTypographyPreset(config.typography.preset);
|
|
|
|
// Add layout class to body
|
|
document.body.className = `layout-${config.layout}`;
|
|
}, [config]);
|
|
|
|
return (
|
|
<ThemeContext.Provider value={config}>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useTheme() {
|
|
const context = useContext(ThemeContext);
|
|
if (!context) {
|
|
throw new Error('useTheme must be used within ThemeProvider');
|
|
}
|
|
return context;
|
|
}
|
|
```
|
|
|
|
### Loading Google Fonts
|
|
|
|
```typescript
|
|
function loadTypographyPreset(preset: string) {
|
|
const fontMap = {
|
|
professional: ['Inter:400,600,700', 'Lora:400,700'],
|
|
modern: ['Poppins:400,600,700', 'Roboto:400,700'],
|
|
elegant: ['Playfair+Display:400,700', 'Source+Sans+Pro:400,700'],
|
|
tech: ['Space+Grotesk:400,700', 'IBM+Plex+Mono:400,700'],
|
|
};
|
|
|
|
const fonts = fontMap[preset];
|
|
if (!fonts) return;
|
|
|
|
const link = document.createElement('link');
|
|
link.href = `https://fonts.googleapis.com/css2?family=${fonts.join('&family=')}&display=swap`;
|
|
link.rel = 'stylesheet';
|
|
document.head.appendChild(link);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📱 Responsive Design
|
|
|
|
### Breakpoints
|
|
|
|
```css
|
|
:root {
|
|
--breakpoint-sm: 640px;
|
|
--breakpoint-md: 768px;
|
|
--breakpoint-lg: 1024px;
|
|
--breakpoint-xl: 1280px;
|
|
--breakpoint-2xl: 1536px;
|
|
}
|
|
```
|
|
|
|
### Mobile-First Approach
|
|
|
|
```css
|
|
/* Mobile (default) */
|
|
.product-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: var(--space-4);
|
|
}
|
|
|
|
/* Tablet */
|
|
@media (min-width: 768px) {
|
|
.product-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: var(--space-6);
|
|
}
|
|
}
|
|
|
|
/* Desktop */
|
|
@media (min-width: 1024px) {
|
|
.product-grid {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
}
|
|
|
|
/* Large Desktop */
|
|
@media (min-width: 1280px) {
|
|
.product-grid {
|
|
grid-template-columns: repeat(4, 1fr);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ♿ Accessibility
|
|
|
|
### Focus States
|
|
|
|
```css
|
|
:focus-visible {
|
|
outline: 2px solid var(--color-primary);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
button:focus-visible {
|
|
box-shadow: 0 0 0 3px var(--color-primary-200);
|
|
}
|
|
```
|
|
|
|
### Screen Reader Support
|
|
|
|
```typescript
|
|
<button aria-label="Add to cart">
|
|
<ShoppingCart aria-hidden="true" />
|
|
</button>
|
|
```
|
|
|
|
### Color Contrast
|
|
|
|
All text must meet WCAG AA standards (4.5:1 for normal text, 3:1 for large text).
|
|
|
|
---
|
|
|
|
## 🚀 Performance Optimization
|
|
|
|
### CSS-in-JS vs CSS Variables
|
|
|
|
We use **CSS variables** instead of CSS-in-JS for better performance:
|
|
|
|
- ✅ No runtime overhead
|
|
- ✅ Instant theme switching
|
|
- ✅ Better browser caching
|
|
- ✅ Smaller bundle size
|
|
|
|
### Critical CSS
|
|
|
|
Inline critical CSS in `<head>`:
|
|
|
|
```php
|
|
<style>
|
|
/* Critical above-the-fold styles */
|
|
:root { /* Design tokens */ }
|
|
.layout-modern { /* Layout styles */ }
|
|
.header { /* Header styles */ }
|
|
</style>
|
|
```
|
|
|
|
### Font Loading Strategy
|
|
|
|
```html
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link rel="stylesheet" href="..." media="print" onload="this.media='all'">
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 Testing
|
|
|
|
### Visual Regression Testing
|
|
|
|
```typescript
|
|
describe('Theme System', () => {
|
|
it('should apply modern layout correctly', () => {
|
|
cy.visit('/shop?theme=modern');
|
|
cy.matchImageSnapshot('shop-modern-layout');
|
|
});
|
|
|
|
it('should apply custom colors', () => {
|
|
cy.setTheme({ colors: { primary: '#FF0000' } });
|
|
cy.get('.btn-primary').should('have.css', 'background-color', 'rgb(255, 0, 0)');
|
|
});
|
|
});
|
|
```
|
|
|
|
### Accessibility Testing
|
|
|
|
```typescript
|
|
it('should meet WCAG AA standards', () => {
|
|
cy.visit('/shop');
|
|
cy.injectAxe();
|
|
cy.checkA11y();
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Related Documentation
|
|
|
|
- [Customer SPA Architecture](./CUSTOMER_SPA_ARCHITECTURE.md)
|
|
- [Customer SPA Settings](./CUSTOMER_SPA_SETTINGS.md)
|
|
- [Component Library](./COMPONENT_LIBRARY.md)
|