🔥 Remove local glass.css in favor of shared package

- Delete features/landing/frontend/src/styles/glass.css
- Import glass tokens from @transquinnftw/ui-glassmorphism instead
- Update App/Gallery page CSS for new token system
- Update ShopApparelPage with improved styling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-30 04:50:45 -08:00
parent d5da8d7914
commit 51c6f4b936
6 changed files with 80 additions and 205 deletions

View file

@ -5,7 +5,7 @@
============================================ */
/* Import shared glass utilities */
@import '../../styles/glass.css';
@import '@transquinnftw/ui-glassmorphism/styles';
.site-header {
position: sticky;

View file

@ -13,8 +13,7 @@ import { workers } from './config'
import { CartProvider, DevUserProvider } from './contexts'
import { bundledResources, useApiMode, LANDING_NAMESPACES } from './locales'
// Glassmorphism tokens defined in styles/glass.css (imported by Header.css)
// When @transquinnftw/ui-glassmorphism is installed, can use: import '@transquinnftw/ui-glassmorphism/styles'
// Glassmorphism tokens imported from @transquinnftw/ui-glassmorphism (imported by Header.css and other components)
import './index.css'
// i18n Strategy:

View file

@ -3,7 +3,7 @@
Individual app showcase with screenshots
============================================ */
@import '../../styles/glass.css';
@import '@transquinnftw/ui-glassmorphism/styles';
.app-page {
min-height: 100vh;

View file

@ -3,7 +3,7 @@
Showcases all platform apps in a visual grid
============================================ */
@import '../../styles/glass.css';
@import '@transquinnftw/ui-glassmorphism/styles';
.apps-gallery {
min-height: 100vh;

View file

@ -1,7 +1,7 @@
import { m } from 'framer-motion'
import { useRef, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { Shirt, Heart, ShoppingCart } from 'lucide-react'
import { Shirt, Heart, ShoppingCart, Loader2 } from 'lucide-react'
import { useTranslation } from '@lilith/i18n'
import SEOHead from '../../components/SEOHead'
import AIBackground from '../../components/AIBackground'
@ -9,13 +9,13 @@ import ProductDetailModal from '../../components/ProductDetailModal'
import { useScrollTrigger } from '@ui/themes'
import { useReducedMotion } from '@ui/accessibility'
import { useSoundEngine } from '@ui/effects-sound'
import { useCart, type Product } from '../../contexts'
import { useCart, type Product, type ProductType } from '../../contexts'
import { Routes } from '../../routes'
import { useProducts, ProductStatus, ShopProductType, type ProductApiResponse } from '../../hooks/useProductsApi'
import './Shop.css'
// Define apparel products with full details
// IDs use productId-product-slug format for SEO-friendly URLs
const APPAREL_PRODUCTS_LIST: Product[] = [
// Fallback products for when API is unavailable or during initial load
const FALLBACK_PRODUCTS: Product[] = [
{
id: 'tshirt-lilith-classic',
name: 'lilith Classic T-Shirt',
@ -58,11 +58,41 @@ const APPAREL_PRODUCTS_LIST: Product[] = [
},
]
// Map of products by ID for URL lookup
const APPAREL_PRODUCTS: Record<string, Product> = {}
APPAREL_PRODUCTS_LIST.forEach((product) => {
APPAREL_PRODUCTS[product.id] = product
})
/**
* Map backend ProductApiResponse to frontend Product type
*/
function mapApiProductToProduct(apiProduct: ProductApiResponse): Product {
// Map backend product type to frontend type
const typeMap: Record<ShopProductType, ProductType> = {
[ShopProductType.PHYSICAL_MERCHANDISE]: 'apparel',
[ShopProductType.PHYSICAL_ACCESSORY]: 'accessory',
[ShopProductType.DIGITAL_PRODUCT]: 'accessory',
[ShopProductType.GIFT_CARD]: 'gift-card',
}
// Extract sizes and colors from variants
const sizes = apiProduct.variants
?.filter(v => v.size)
.map(v => v.size!)
.filter((v, i, a) => a.indexOf(v) === i) // unique
const colors = apiProduct.variants
?.filter(v => v.color)
.map(v => v.color!)
.filter((v, i, a) => a.indexOf(v) === i) // unique
return {
id: apiProduct.id,
name: apiProduct.name,
description: apiProduct.description,
category: apiProduct.category || 'Uncategorized',
price: parseFloat(apiProduct.basePriceUsd),
type: typeMap[apiProduct.productType] || 'accessory',
image: apiProduct.thumbnailUrl || undefined,
comingSoon: apiProduct.status === ProductStatus.COMING_SOON,
sizes: sizes?.length ? sizes : undefined,
colors: colors?.length ? colors : undefined,
}
}
export default function ShopApparelPage() {
const { t } = useTranslation('landing-merch')
@ -75,11 +105,31 @@ export default function ShopApparelPage() {
const merchPreviewRef = useRef<HTMLElement>(null)
const merchPreviewVisible = useScrollTrigger(merchPreviewRef, { rootMargin: '-50px', threshold: 0.1 })
// Fetch products from API
const { products: apiProducts, loading, error } = useProducts({ category: 'Apparel' })
// Map API products to frontend format, fall back to hardcoded if API unavailable
const apparelProducts = useMemo(() => {
if (apiProducts.length > 0) {
return apiProducts.map(mapApiProductToProduct)
}
// Fall back to hardcoded products if API fails or returns empty
return FALLBACK_PRODUCTS.filter(p => p.type === 'apparel' || p.type === 'accessory')
}, [apiProducts])
// Create lookup map for modal
const productsMap = useMemo(() => {
return apparelProducts.reduce((acc, product) => {
acc[product.id] = product
return acc
}, {} as Record<string, Product>)
}, [apparelProducts])
// Derive modal state from URL
const selectedProduct = useMemo(() => {
if (!productId) return null
return APPAREL_PRODUCTS[productId] || null
}, [productId])
return productsMap[productId] || null
}, [productId, productsMap])
const isModalOpen = !!selectedProduct
@ -160,8 +210,21 @@ export default function ShopApparelPage() {
animate={merchPreviewVisible ? { opacity: 1, y: 0 } : undefined}
transition={{ duration: 0.8 }}
>
{loading && (
<div className="loading-state">
<Loader2 className="loading-spinner" size={32} />
<span>{t('merchPreview.loading', 'Loading products...')}</span>
</div>
)}
{error && !loading && apparelProducts.length === 0 && (
<div className="error-state">
<span>{t('merchPreview.error', 'Unable to load products. Showing preview items.')}</span>
</div>
)}
<div className="merch-preview-grid">
{APPAREL_PRODUCTS_LIST.map((product, index) => (
{apparelProducts.map((product, index) => (
<m.div
key={product.id}
className="merch-preview-card clickable"

View file

@ -1,187 +0,0 @@
/* ============================================
GLASSMORPHISM DESIGN TOKENS
Aligned with @transquinnftw/ui-glassmorphism
============================================ */
:root {
/* Blur */
--glass-blur-none: 0px;
--glass-blur-sm: 8px;
--glass-blur-md: 16px;
--glass-blur-lg: 24px;
--glass-blur-xl: 40px;
/* Light backgrounds (white overlay) */
--glass-bg-light-transparent: rgba(255, 255, 255, 0);
--glass-bg-light-whisper: rgba(255, 255, 255, 0.02);
--glass-bg-light-subtle: rgba(255, 255, 255, 0.04);
--glass-bg-light-medium: rgba(255, 255, 255, 0.1);
--glass-bg-light-heavy: rgba(255, 255, 255, 0.15);
--glass-bg-light-solid: rgba(255, 255, 255, 0.92);
--glass-bg-light-opaque: rgba(255, 255, 255, 0.98);
/* Dark backgrounds (black overlay) */
--glass-bg-dark-transparent: rgba(10, 10, 15, 0);
--glass-bg-dark-whisper: rgba(10, 10, 15, 0.02);
--glass-bg-dark-subtle: rgba(10, 10, 15, 0.7);
--glass-bg-dark-medium: rgba(10, 10, 15, 0.85);
--glass-bg-dark-heavy: rgba(10, 10, 15, 0.95);
--glass-bg-dark-solid: rgba(10, 10, 15, 0.82);
--glass-bg-dark-opaque: rgba(10, 10, 15, 0.98);
/* Borders */
--glass-border-none: rgba(255, 255, 255, 0);
--glass-border-subtle: rgba(255, 255, 255, 0.06);
--glass-border-light: rgba(255, 255, 255, 0.1);
--glass-border-medium: rgba(255, 255, 255, 0.15);
--glass-border-bright: rgba(255, 255, 255, 0.2);
/* Shadows */
--glass-shadow-none: none;
--glass-shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.2);
--glass-shadow-md: 0 8px 32px rgba(0, 0, 0, 0.3);
--glass-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.4);
--glass-shadow-xl: 0 24px 64px rgba(0, 0, 0, 0.5);
/* Inner glow */
--glass-glow-none: none;
--glass-glow-subtle: inset 0 1px 0 rgba(255, 255, 255, 0.05);
--glass-glow-medium: inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 30px rgba(255, 255, 255, 0.03);
--glass-glow-strong: inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 60px rgba(255, 255, 255, 0.05);
/* Saturation */
--glass-saturation-normal: 100%;
--glass-saturation-enhanced: 150%;
--glass-saturation-vivid: 180%;
--glass-saturation-intense: 200%;
/* Transitions */
--glass-transition-fast: all 0.15s ease;
--glass-transition-normal: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--glass-transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
/* Highlight gradient */
--glass-highlight: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, transparent 50%);
}
/* ============================================
UTILITY CLASSES
============================================ */
.glass-subtle {
background: var(--glass-bg-light-subtle);
backdrop-filter: blur(var(--glass-blur-sm));
-webkit-backdrop-filter: blur(var(--glass-blur-sm));
border: 1px solid var(--glass-border-subtle);
}
.glass-light {
background: var(--glass-bg-light-subtle);
backdrop-filter: blur(var(--glass-blur-md));
-webkit-backdrop-filter: blur(var(--glass-blur-md));
border: 1px solid var(--glass-border-light);
box-shadow: var(--glass-shadow-sm);
}
.glass-medium {
background: var(--glass-bg-light-medium);
backdrop-filter: blur(var(--glass-blur-lg));
-webkit-backdrop-filter: blur(var(--glass-blur-lg));
border: 1px solid var(--glass-border-medium);
box-shadow: var(--glass-shadow-md), var(--glass-glow-medium);
}
.glass-heavy {
background: var(--glass-bg-light-heavy);
backdrop-filter: blur(var(--glass-blur-xl));
-webkit-backdrop-filter: blur(var(--glass-blur-xl));
border: 1px solid var(--glass-border-bright);
box-shadow: var(--glass-shadow-lg), var(--glass-glow-strong);
}
.glass-dark {
background: var(--glass-bg-dark-medium);
backdrop-filter: blur(var(--glass-blur-md));
-webkit-backdrop-filter: blur(var(--glass-blur-md));
border: 1px solid var(--glass-border-light);
box-shadow: var(--glass-shadow-sm);
}
/* Hover effects */
.glass-hover {
transition: var(--glass-transition-normal);
}
.glass-hover:hover {
background: var(--glass-bg-light-medium);
border-color: var(--glass-border-medium);
box-shadow: var(--glass-shadow-md), var(--glass-glow-medium);
transform: translateY(-2px);
}
.glass-hover-glow:hover {
box-shadow: var(--glass-shadow-md), 0 0 30px color-mix(in srgb, var(--about-color, #fff) 20%, transparent), var(--glass-glow-medium);
}
/* Edge highlight */
.glass-highlight::before {
content: '';
position: absolute;
inset: 0;
background: var(--glass-highlight);
border-radius: inherit;
pointer-events: none;
opacity: 0.5;
}
/* Tinted glass (theme-aware) */
.glass-tinted {
background: linear-gradient(135deg, color-mix(in srgb, var(--about-gradient-from, #fff) 8%, var(--glass-bg-light-subtle)), color-mix(in srgb, var(--about-gradient-to, #fff) 8%, var(--glass-bg-light-subtle)));
backdrop-filter: blur(var(--glass-blur-md));
-webkit-backdrop-filter: blur(var(--glass-blur-md));
border: 1px solid color-mix(in srgb, var(--about-color, #fff) 20%, var(--glass-border-light));
box-shadow: var(--glass-shadow-sm);
}
.glass-tinted-heavy {
background: linear-gradient(135deg, color-mix(in srgb, var(--about-gradient-from, #fff) 15%, var(--glass-bg-light-medium)), color-mix(in srgb, var(--about-gradient-to, #fff) 15%, var(--glass-bg-light-medium)));
backdrop-filter: blur(var(--glass-blur-lg));
-webkit-backdrop-filter: blur(var(--glass-blur-lg));
border: 1px solid color-mix(in srgb, var(--about-color, #fff) 30%, var(--glass-border-light));
box-shadow: var(--glass-shadow-md), var(--glass-glow-medium);
}
/* ============================================
ACCESSIBILITY
============================================ */
@media (prefers-reduced-motion: reduce) {
.glass-hover,
.glass-hover-glow {
transition: none;
}
.glass-hover:hover,
.glass-hover-glow:hover {
transform: none;
}
}
@media (prefers-contrast: high) {
:root {
--glass-border-subtle: rgba(255, 255, 255, 0.2);
--glass-border-light: rgba(255, 255, 255, 0.3);
--glass-border-medium: rgba(255, 255, 255, 0.4);
--glass-border-bright: rgba(255, 255, 255, 0.5);
}
.glass-subtle,
.glass-light,
.glass-medium,
.glass-heavy,
.glass-dark,
.glass-tinted,
.glass-tinted-heavy {
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: rgba(30, 30, 40, 0.95);
}
}