astro-component-kit 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -27
- package/bin/cli.js +67 -0
- package/bin/installer.js +179 -0
- package/package.json +35 -5
- package/src/components/index.ts +2 -0
- package/src/components/lib/Galaxy3D.astro +249 -137
- package/src/components/lib/app/AppSidebar.astro +249 -0
- package/src/components/lib/app/AppTopbar.astro +284 -0
- package/src/components/lib/app/ArrivalsBoard.astro +176 -0
- package/src/components/lib/app/AuthCard.astro +213 -0
- package/src/components/lib/app/DataTable.astro +346 -0
- package/src/components/lib/app/GuestProfileCard.astro +133 -0
- package/src/components/lib/app/HousekeepingBoard.astro +189 -0
- package/src/components/lib/app/PageHeader.astro +157 -0
- package/src/components/lib/app/PlanUsageCard.astro +112 -0
- package/src/components/lib/app/RoomRack.astro +207 -0
- package/src/components/lib/app/SettingsSection.astro +96 -0
- package/src/components/lib/app/SwitchRow.astro +71 -0
- package/src/components/lib/app/TeamMembers.astro +190 -0
- package/src/components/lib/app/app-icons.ts +52 -0
- package/src/components/lib/app/index.ts +19 -0
- package/src/components/lib/charts/AreaChart.astro +4 -4
- package/src/components/lib/charts/BarChart.astro +3 -3
- package/src/components/lib/charts/ChartFrame.astro +10 -10
- package/src/components/lib/charts/DonutChart.astro +2 -2
- package/src/components/lib/charts/DumbbellChart.astro +1 -1
- package/src/components/lib/charts/ForecastLineChart.astro +1 -1
- package/src/components/lib/charts/GaugeChart.astro +1 -1
- package/src/components/lib/charts/HeatmapChart.astro +1 -1
- package/src/components/lib/charts/HorizontalBarChart.astro +2 -2
- package/src/components/lib/charts/KpiStatTile.astro +4 -4
- package/src/components/lib/charts/LinearProgressGauge.astro +4 -4
- package/src/components/lib/charts/MetricDeltaTable.astro +3 -3
- package/src/components/lib/charts/RadarMetricChart.astro +4 -4
- package/src/components/lib/charts/RadialBarChart.astro +1 -1
- package/src/components/lib/charts/RetentionCohortTable.astro +2 -2
- package/src/components/lib/charts/TreemapChart.astro +1 -1
- package/src/components/lib/ecommerce/CartDrawer.astro +483 -0
- package/src/components/lib/ecommerce/CartPageList.astro +290 -0
- package/src/components/lib/ecommerce/CartToast.astro +276 -0
- package/src/components/lib/ecommerce/CategoryTiles.astro +143 -0
- package/src/components/lib/ecommerce/CheckoutForm.astro +423 -0
- package/src/components/lib/ecommerce/CountdownBanner.astro +230 -0
- package/src/components/lib/ecommerce/FilterBar.astro +278 -0
- package/src/components/lib/ecommerce/FilterSidebar.astro +405 -0
- package/src/components/lib/ecommerce/LiveActivityToast.astro +213 -0
- package/src/components/lib/ecommerce/OrderSummary.astro +295 -0
- package/src/components/lib/ecommerce/ProductBuyBox.astro +247 -0
- package/src/components/lib/ecommerce/ProductCardMinimal.astro +229 -0
- package/src/components/lib/ecommerce/ProductGallery.astro +145 -0
- package/src/components/lib/ecommerce/ProductGrid.astro +118 -0
- package/src/components/lib/ecommerce/ProductMedia.astro +113 -0
- package/src/components/lib/ecommerce/ProductRating.astro +78 -0
- package/src/components/lib/ecommerce/ProductToolbar.astro +93 -0
- package/src/components/lib/ecommerce/PromoBar.astro +145 -0
- package/src/components/lib/ecommerce/StockIndicator.astro +127 -0
- package/src/components/lib/ecommerce/StoreHeader.astro +446 -0
- package/src/components/lib/ecommerce/StoreHero.astro +260 -0
- package/src/components/lib/ecommerce/StoreProductCard.astro +307 -0
- package/src/components/lib/ecommerce/TrustBadges.astro +101 -0
- package/src/components/lib/ecommerce/VariantPicker.astro +145 -0
- package/src/components/lib/ecommerce/cart-store.ts +151 -0
- package/src/components/lib/ecommerce/catalog-filter.ts +62 -0
- package/src/components/lib/ecommerce/index.ts +34 -0
- package/src/components/lib/ecommerce/product-icons.ts +28 -0
- package/src/components/lib/ecommerce/types.ts +70 -0
- package/src/components/lib/forms/AnimatedInput.astro +4 -2
- package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
- package/src/components/lib/index.ts +2 -0
- package/src/components/lib/layout/BentoGrid3.astro +6 -4
- package/src/components/lib/layout/CenterFixedBox.astro +2 -2
- package/src/components/lib/layout/CenteredContainer.astro +4 -2
- package/src/components/lib/layout/ClusterHorizontal.astro +2 -2
- package/src/components/lib/layout/FeatureHero.astro +4 -2
- package/src/components/lib/layout/FooterCenteredMinimal.astro +4 -2
- package/src/components/lib/layout/FullBleedSection.astro +2 -2
- package/src/components/lib/layout/GalleryGrid4.astro +2 -2
- package/src/components/lib/layout/HolyGrailGrid.astro +11 -9
- package/src/components/lib/layout/LandingSectionSplit.astro +5 -3
- package/src/components/lib/layout/MasonryGrid.astro +2 -2
- package/src/components/lib/layout/MinimalFooterGrid.astro +1 -1
- package/src/components/lib/layout/ScrollSnapSection.astro +1 -1
- package/src/components/lib/layout/SidebarLayoutWrapper.astro +4 -2
- package/src/components/lib/layout/SplitScreenLayout.astro +3 -3
- package/src/components/lib/layout/StepLayoutVertical.astro +1 -1
- package/src/components/lib/layout/StickySidebarWrapper.astro +4 -2
- package/src/components/lib/sections/AgencyContactBrief.astro +194 -0
- package/src/components/lib/sections/AgencyFooterCta.astro +94 -0
- package/src/components/lib/sections/AgencyHeader.astro +161 -0
- package/src/components/lib/sections/AgencyHeroKinetic.astro +172 -0
- package/src/components/lib/sections/AgencyServicesList.astro +113 -0
- package/src/components/lib/sections/AgencyTeamGrid.astro +111 -0
- package/src/components/lib/sections/AgencyWorkGrid.astro +147 -0
- package/src/components/lib/sections/BlogGridSection.astro +9 -7
- package/src/components/lib/sections/ButtonsShowcase.astro +20 -18
- package/src/components/lib/sections/CardsShowcase.astro +28 -26
- package/src/components/lib/sections/CareersOpenings.astro +10 -8
- package/src/components/lib/sections/ChangelogSection.astro +20 -7
- package/src/components/lib/sections/ContactGlassForm.astro +10 -8
- package/src/components/lib/sections/ContactSplitMap.astro +11 -9
- package/src/components/lib/sections/CtaAppDownload.astro +13 -11
- package/src/components/lib/sections/CtaGlowBanner.astro +12 -10
- package/src/components/lib/sections/CtaNewsletterSection.astro +13 -11
- package/src/components/lib/sections/CtaSplitImage.astro +13 -11
- package/src/components/lib/sections/DataShowcase.astro +23 -21
- package/src/components/lib/sections/FaqAccordionSection.astro +8 -8
- package/src/components/lib/sections/FaqTwoColumn.astro +9 -7
- package/src/components/lib/sections/FeatureAlternatingRows.astro +10 -8
- package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
- package/src/components/lib/sections/FeatureIconGrid.astro +6 -6
- package/src/components/lib/sections/FeatureTabsShowcase.astro +13 -11
- package/src/components/lib/sections/FeatureTimelineFlow.astro +8 -6
- package/src/components/lib/sections/FeedbackShowcase.astro +15 -13
- package/src/components/lib/sections/FooterMegaSection.astro +13 -11
- package/src/components/lib/sections/FormsShowcase.astro +22 -20
- package/src/components/lib/sections/HeroDashboardPreview.astro +11 -9
- package/src/components/lib/sections/HeroGlass.astro +22 -20
- package/src/components/lib/sections/HeroGradient.astro +15 -13
- package/src/components/lib/sections/HeroMinimalCentered.astro +10 -10
- package/src/components/lib/sections/HeroShowcaseSplit.astro +18 -16
- package/src/components/lib/sections/HeroSplit.astro +13 -11
- package/src/components/lib/sections/HeroTerminal.astro +14 -12
- package/src/components/lib/sections/HeroWaitlist.astro +10 -8
- package/src/components/lib/sections/HowItWorksSteps.astro +12 -10
- package/src/components/lib/sections/IntegrationsGrid.astro +7 -7
- package/src/components/lib/sections/LayoutShowcase.astro +21 -19
- package/src/components/lib/sections/LogoCloudSection.astro +4 -4
- package/src/components/lib/sections/LogoWallGrid.astro +6 -4
- package/src/components/lib/sections/NavigationShowcase.astro +15 -13
- package/src/components/lib/sections/OverlaysShowcase.astro +21 -19
- package/src/components/lib/sections/PricingComparisonMatrix.astro +15 -13
- package/src/components/lib/sections/PricingSinglePlan.astro +13 -11
- package/src/components/lib/sections/PricingTierCards.astro +11 -9
- package/src/components/lib/sections/PricingToggleCards.astro +13 -13
- package/src/components/lib/sections/ShopCategoryHero.astro +103 -0
- package/src/components/lib/sections/ShopCollectionMosaic.astro +131 -0
- package/src/components/lib/sections/ShopFooterWordmark.astro +100 -0
- package/src/components/lib/sections/ShopHeaderEditorial.astro +396 -0
- package/src/components/lib/sections/ShopHeroEditorial.astro +151 -0
- package/src/components/lib/sections/ShopLookbook.astro +250 -0
- package/src/components/lib/sections/ShopNewsletterEditorial.astro +128 -0
- package/src/components/lib/sections/ShopProductRail.astro +150 -0
- package/src/components/lib/sections/ShopReviewsEditorial.astro +133 -0
- package/src/components/lib/sections/ShopValuesMarquee.astro +91 -0
- package/src/components/lib/sections/StatsCounterBand.astro +9 -7
- package/src/components/lib/sections/StatsDashboardSection.astro +4 -2
- package/src/components/lib/sections/StatsMetricShowcase.astro +9 -7
- package/src/components/lib/sections/TeamGridSection.astro +12 -10
- package/src/components/lib/sections/TestimonialCarousel.astro +10 -8
- package/src/components/lib/sections/TestimonialFeatured.astro +7 -5
- package/src/components/lib/sections/TestimonialGridSection.astro +7 -7
- package/src/components/lib/sections/TestimonialWallGlass.astro +11 -9
- package/src/components/lib/sections/TypographyShowcase.astro +10 -8
- package/src/components/lib/sections/index.ts +10 -0
- package/src/components/lib/templates/ecommerce/StoreCategoryPage.astro +160 -0
- package/src/components/lib/templates/ecommerce/StoreCheckoutPage.astro +98 -0
- package/src/components/lib/templates/ecommerce/StoreHomePage.astro +191 -0
- package/src/components/lib/templates/ecommerce/StoreLayout.astro +169 -0
- package/src/components/lib/templates/ecommerce/StoreProductPage.astro +165 -0
- package/src/components/lib/templates/ecommerce/data.ts +187 -0
- package/src/components/lib/templates/ecommerce/index.ts +14 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialBagPage.astro +52 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialCheckoutPage.astro +123 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialHomePage.astro +102 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialLayout.astro +216 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialProductPage.astro +205 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialShopPage.astro +112 -0
- package/src/components/lib/templates/ecommerce-editorial/data.ts +125 -0
- package/src/components/lib/templates/ecommerce-editorial/index.ts +16 -0
- package/src/components/lib/templates/saas-hotel/AppLayout.astro +83 -0
- package/src/components/lib/templates/saas-hotel/DashboardPage.astro +66 -0
- package/src/components/lib/templates/saas-hotel/GuestsPage.astro +65 -0
- package/src/components/lib/templates/saas-hotel/LandingPage.astro +213 -0
- package/src/components/lib/templates/saas-hotel/ReportsPage.astro +63 -0
- package/src/components/lib/templates/saas-hotel/ReservationsPage.astro +49 -0
- package/src/components/lib/templates/saas-hotel/RoomsPage.astro +48 -0
- package/src/components/lib/templates/saas-hotel/SettingsPage.astro +115 -0
- package/src/components/lib/templates/saas-hotel/SignInPage.astro +50 -0
- package/src/components/lib/templates/saas-hotel/data.ts +229 -0
- package/src/components/lib/templates/saas-hotel/index.ts +19 -0
- package/src/components/lib/templates/saas-hotel/lobby-theme.css +90 -0
- package/src/data/registry/app.ts +93 -0
- package/src/data/registry/buttons.ts +3279 -0
- package/src/data/registry/cards.ts +1753 -0
- package/src/data/registry/charts-svg.ts +557 -0
- package/src/data/registry/charts.ts +500 -0
- package/src/data/registry/data.ts +986 -0
- package/src/data/registry/ecommerce.ts +269 -0
- package/src/data/registry/feedback.ts +1824 -0
- package/src/data/registry/forms.ts +2513 -0
- package/src/data/registry/index.ts +64 -0
- package/src/data/registry/layout.ts +206 -0
- package/src/data/registry/navigation.ts +936 -0
- package/src/data/registry/overlays.ts +1133 -0
- package/src/data/registry/sections-extra.ts +397 -0
- package/src/data/registry/sections.ts +361 -0
- package/src/data/registry/sources.ts +41 -0
- package/src/data/registry/templates.ts +414 -0
- package/src/data/registry/types.ts +30 -0
- package/src/data/registry/typography.ts +777 -0
- package/src/mcp/http-server.ts +324 -0
- package/src/mcp/index.ts +67 -0
- package/src/mcp/installer.ts +178 -0
- package/src/mcp/registry-adapter.ts +278 -0
- package/src/mcp/stdio-server.ts +11 -0
- package/src/mcp/tools.ts +247 -0
- package/src/mcp/types.ts +53 -0
- package/src/styles/global.css +95 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ProductCardMinimal — Editorial product card: tall media that swaps to a second view on hover,
|
|
4
|
+
* quick-add size buttons, name, price and colour count. Square corners, typography-led.
|
|
5
|
+
*
|
|
6
|
+
* @param {Product} product - Product data. A demo product is used by default.
|
|
7
|
+
* @param {string} href - Product page URL. Default '#'.
|
|
8
|
+
* @param {number} index - Optional catalogue number shown as "N° 01".
|
|
9
|
+
* @param {string} currency - ISO currency. Default 'EUR'.
|
|
10
|
+
* @param {string} locale - Number format locale. Default 'en-GB'.
|
|
11
|
+
* @param {string} cartKey - Cart storage key. Default 'ack-cart'.
|
|
12
|
+
* @param {boolean} quickAdd - Show quick-add (sizes, or a single "Add" button). Default true.
|
|
13
|
+
* @param {string} ratio - Media aspect ratio. Default '3 / 4'.
|
|
14
|
+
* @param {string} variant - Style (plain|framed). Default 'plain'.
|
|
15
|
+
* @param {string} size - Type scale (sm|md|lg). Default 'md'.
|
|
16
|
+
* @param {string} headingLevel - Title element (h2|h3|h4). Default 'h3'.
|
|
17
|
+
* @param {string} className - Extra class.
|
|
18
|
+
*
|
|
19
|
+
* Accessibility: the title is a stretched link; quick-add buttons sit above it with their own
|
|
20
|
+
* labels ("Add size M"), are reachable by keyboard and appear on focus as well as hover.
|
|
21
|
+
*/
|
|
22
|
+
import ProductMedia from './ProductMedia.astro';
|
|
23
|
+
import { formatMoney } from './cart-store';
|
|
24
|
+
import type { Product } from './types';
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
product?: Product;
|
|
28
|
+
href?: string;
|
|
29
|
+
index?: number;
|
|
30
|
+
currency?: string;
|
|
31
|
+
locale?: string;
|
|
32
|
+
cartKey?: string;
|
|
33
|
+
quickAdd?: boolean;
|
|
34
|
+
ratio?: string;
|
|
35
|
+
variant?: 'plain' | 'framed';
|
|
36
|
+
size?: 'sm' | 'md' | 'lg';
|
|
37
|
+
headingLevel?: 'h2' | 'h3' | 'h4';
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
product = {
|
|
43
|
+
slug: 'linen-overshirt',
|
|
44
|
+
name: 'Linen Overshirt',
|
|
45
|
+
price: 145,
|
|
46
|
+
category: 'clothing',
|
|
47
|
+
badge: 'New',
|
|
48
|
+
colors: [{ name: 'Oat', hex: '#d9cbb3' }, { name: 'Ink', hex: '#22252b' }, { name: 'Rust', hex: '#9c3d1c' }],
|
|
49
|
+
sizes: ['XS', 'S', 'M', 'L', 'XL'],
|
|
50
|
+
icon: 'shirt',
|
|
51
|
+
hue: 30,
|
|
52
|
+
},
|
|
53
|
+
href = '#',
|
|
54
|
+
index,
|
|
55
|
+
currency = 'EUR',
|
|
56
|
+
locale = 'en-GB',
|
|
57
|
+
cartKey = 'ack-cart',
|
|
58
|
+
quickAdd = true,
|
|
59
|
+
ratio = '3 / 4',
|
|
60
|
+
variant = 'plain',
|
|
61
|
+
size = 'md',
|
|
62
|
+
headingLevel = 'h3',
|
|
63
|
+
className = '',
|
|
64
|
+
} = Astro.props;
|
|
65
|
+
|
|
66
|
+
const Heading = headingLevel;
|
|
67
|
+
const money = { currency, locale };
|
|
68
|
+
const inStock = product.inStock !== false;
|
|
69
|
+
const base = { id: product.slug, name: product.name, price: product.price, icon: product.icon, hue: product.hue, image: product.image };
|
|
70
|
+
const color = product.colors?.[0]?.name;
|
|
71
|
+
const titleId = `pcm-${product.slug}`;
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
<article
|
|
75
|
+
class:list={['pcm', `pcm--${variant}`, `pcm--${size}`, className]}
|
|
76
|
+
aria-labelledby={titleId}
|
|
77
|
+
data-product
|
|
78
|
+
data-name={product.name.toLowerCase()}
|
|
79
|
+
data-category={product.category}
|
|
80
|
+
data-price={product.price}
|
|
81
|
+
data-rating={product.rating ?? 0}
|
|
82
|
+
data-colors={(product.colors ?? []).map((c) => c.name.toLowerCase()).join(',')}
|
|
83
|
+
data-stock={inStock ? 'in' : 'out'}
|
|
84
|
+
>
|
|
85
|
+
<div class="pcm__media">
|
|
86
|
+
<div class="pcm__view pcm__view--a">
|
|
87
|
+
<ProductMedia image={product.image} alt={product.name} icon={product.icon} hue={product.hue} ratio={ratio} variant="soft" size="lg" />
|
|
88
|
+
</div>
|
|
89
|
+
<div class="pcm__view pcm__view--b" aria-hidden="true">
|
|
90
|
+
<ProductMedia icon={product.icon} hue={(product.hue ?? 30) + 20} ratio={ratio} variant="glow" size="sm" />
|
|
91
|
+
</div>
|
|
92
|
+
{(product.badge || !inStock) && <span class="pcm__badge">{inStock ? product.badge : 'Sold out'}</span>}
|
|
93
|
+
{index !== undefined && <span class="pcm__index" aria-hidden="true">N° {String(index).padStart(2, '0')}</span>}
|
|
94
|
+
|
|
95
|
+
{quickAdd && inStock && (
|
|
96
|
+
<div class="pcm__quick" role="group" aria-label={`Quick add ${product.name}`}>
|
|
97
|
+
{product.sizes?.length ? (
|
|
98
|
+
<>
|
|
99
|
+
<span class="pcm__quick-label" aria-hidden="true">Quick add</span>
|
|
100
|
+
<div class="pcm__sizes">
|
|
101
|
+
{product.sizes.map((s) => (
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
class="pcm__size"
|
|
105
|
+
data-add-to-cart={JSON.stringify({ ...base, variant: [color, s].filter(Boolean).join(' / ') })}
|
|
106
|
+
data-cart-key={cartKey}
|
|
107
|
+
data-open-cart="false"
|
|
108
|
+
aria-label={`Add ${product.name}, size ${s}`}
|
|
109
|
+
><span data-label>{s}</span></button>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</>
|
|
113
|
+
) : (
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
class="pcm__size pcm__size--wide"
|
|
117
|
+
data-add-to-cart={JSON.stringify({ ...base, variant: color })}
|
|
118
|
+
data-cart-key={cartKey}
|
|
119
|
+
data-open-cart="false"
|
|
120
|
+
data-added-label="Added"
|
|
121
|
+
aria-label={`Add ${product.name} to bag`}
|
|
122
|
+
><span data-label>Add to bag +</span></button>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div class="pcm__info">
|
|
129
|
+
<Heading class="pcm__title" id={titleId}><a class="pcm__link" href={href}>{product.name}</a></Heading>
|
|
130
|
+
<p class="pcm__price">
|
|
131
|
+
<span>{formatMoney(product.price, money)}</span>
|
|
132
|
+
{product.compareAt && <s><span class="sr-only">Was </span>{formatMoney(product.compareAt, money)}</s>}
|
|
133
|
+
</p>
|
|
134
|
+
{product.colors && product.colors.length > 0 && (
|
|
135
|
+
<p class="pcm__colors">
|
|
136
|
+
<span class="pcm__dots" aria-hidden="true">{product.colors.slice(0, 4).map((c) => <i style={`--sw: ${c.hex}`}></i>)}</span>
|
|
137
|
+
{product.colors.length} {product.colors.length === 1 ? 'colour' : 'colours'}
|
|
138
|
+
</p>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</article>
|
|
142
|
+
|
|
143
|
+
<script>
|
|
144
|
+
import { bindAddToCart } from './cart-store';
|
|
145
|
+
bindAddToCart(document);
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<style>
|
|
149
|
+
.pcm { position: relative; display: flex; flex-direction: column; gap: var(--sp-3, 0.75rem); min-width: 0; }
|
|
150
|
+
.pcm--framed { padding: var(--sp-3, 0.75rem); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); }
|
|
151
|
+
.pcm__media { position: relative; overflow: hidden; border-radius: var(--r-sm, 2px); background: var(--c-bg-2, #111); }
|
|
152
|
+
.pcm__view { transition: opacity 0.5s ease, transform 0.8s cubic-bezier(0.16, 1, 0.3, 1); }
|
|
153
|
+
.pcm__view--b { position: absolute; inset: 0; opacity: 0; transform: scale(1.04); }
|
|
154
|
+
.pcm__view--b :global(.p-media__art) { transform: rotate(-12deg) scale(1.25); }
|
|
155
|
+
.pcm:hover .pcm__view--b, .pcm:focus-within .pcm__view--b { opacity: 1; transform: none; }
|
|
156
|
+
.pcm:hover .pcm__view--a { transform: scale(1.03); }
|
|
157
|
+
|
|
158
|
+
.pcm__badge, .pcm__index {
|
|
159
|
+
position: absolute;
|
|
160
|
+
top: var(--sp-3, 0.75rem);
|
|
161
|
+
z-index: 2;
|
|
162
|
+
font-size: 0.68rem;
|
|
163
|
+
font-weight: 700;
|
|
164
|
+
letter-spacing: 0.14em;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
}
|
|
167
|
+
.pcm__badge { left: var(--sp-3, 0.75rem); padding: 0.3rem 0.55rem; background: var(--c-bg-1, #0a0a0a); color: var(--c-text-1, #fff); }
|
|
168
|
+
.pcm__index { right: var(--sp-3, 0.75rem); color: var(--c-text-2, #a3a3a3); font-variant-numeric: tabular-nums; }
|
|
169
|
+
|
|
170
|
+
.pcm__quick {
|
|
171
|
+
position: absolute;
|
|
172
|
+
left: var(--sp-2, 0.5rem);
|
|
173
|
+
right: var(--sp-2, 0.5rem);
|
|
174
|
+
bottom: var(--sp-2, 0.5rem);
|
|
175
|
+
z-index: 3;
|
|
176
|
+
display: grid;
|
|
177
|
+
gap: 6px;
|
|
178
|
+
padding: var(--sp-3, 0.75rem);
|
|
179
|
+
background: rgb(var(--paper-rgb, 5 5 5) / 0.92);
|
|
180
|
+
backdrop-filter: blur(10px);
|
|
181
|
+
transform: translateY(calc(100% + 12px));
|
|
182
|
+
transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
|
183
|
+
}
|
|
184
|
+
.pcm:hover .pcm__quick, .pcm__quick:focus-within { transform: none; }
|
|
185
|
+
.pcm__quick-label { font-size: 0.66rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
|
|
186
|
+
.pcm__sizes { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
187
|
+
.pcm__size {
|
|
188
|
+
flex: 1 0 36px;
|
|
189
|
+
min-height: 36px;
|
|
190
|
+
padding: 0 6px;
|
|
191
|
+
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.18);
|
|
192
|
+
border-radius: var(--r-sm, 2px);
|
|
193
|
+
background: transparent;
|
|
194
|
+
color: var(--c-text-1, #fff);
|
|
195
|
+
font: inherit;
|
|
196
|
+
font-size: 0.78rem;
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
200
|
+
}
|
|
201
|
+
.pcm__size:hover { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); border-color: var(--c-text-1, #fff); }
|
|
202
|
+
.pcm__size.is-added { background: var(--shop-accent, var(--c-primary-light, #d4a017)); border-color: transparent; color: var(--c-on-primary, #050505); }
|
|
203
|
+
.pcm__size--wide { flex: 1 1 100%; letter-spacing: 0.04em; }
|
|
204
|
+
.pcm__size:focus-visible { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
|
|
205
|
+
|
|
206
|
+
.pcm__info { display: grid; gap: 2px; }
|
|
207
|
+
.pcm__title { margin: 0; font-family: var(--font-display, Georgia, 'Times New Roman', serif); font-size: 1.05rem; font-weight: 500; letter-spacing: -0.01em; line-height: 1.25; }
|
|
208
|
+
.pcm--sm .pcm__title { font-size: 0.95rem; }
|
|
209
|
+
.pcm--lg .pcm__title { font-size: 1.3rem; }
|
|
210
|
+
.pcm__link { color: var(--c-text-1, #fff); text-decoration: none; }
|
|
211
|
+
.pcm__link::after { content: ''; position: absolute; inset: 0; z-index: 1; }
|
|
212
|
+
.pcm__link:focus-visible { outline: none; }
|
|
213
|
+
.pcm:focus-within:not(:has(.pcm__quick:focus-within)) { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 4px; }
|
|
214
|
+
.pcm__price { display: flex; gap: var(--sp-2, 0.5rem); margin: 0; font-size: 0.9rem; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; }
|
|
215
|
+
.pcm__price s { color: var(--c-text-2, #a3a3a3); }
|
|
216
|
+
.pcm__colors { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); margin: 2px 0 0; font-size: 0.78rem; color: var(--c-text-2, #a3a3a3); }
|
|
217
|
+
.pcm__dots { display: inline-flex; }
|
|
218
|
+
.pcm__dots i { width: 10px; height: 10px; margin-right: -3px; border-radius: 50%; background: var(--sw); box-shadow: 0 0 0 1.5px var(--c-bg, #050505); }
|
|
219
|
+
|
|
220
|
+
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
221
|
+
|
|
222
|
+
@media (hover: none) {
|
|
223
|
+
.pcm__quick { position: static; transform: none; padding: var(--sp-2, 0.5rem) 0 0; background: transparent; backdrop-filter: none; }
|
|
224
|
+
.pcm__quick-label { display: none; }
|
|
225
|
+
}
|
|
226
|
+
@media (prefers-reduced-motion: reduce) {
|
|
227
|
+
.pcm__view, .pcm__quick { transition: none; }
|
|
228
|
+
}
|
|
229
|
+
</style>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ProductGallery — Product image viewer with a large stage and thumbnail rail.
|
|
4
|
+
* Works with real photos (`images`) or generates several illustrated "angles".
|
|
5
|
+
*
|
|
6
|
+
* @param {string[]} images - Photo URLs. When empty, illustrated views are generated.
|
|
7
|
+
* @param {string} alt - Alt text base for photos. Default 'Product photo'.
|
|
8
|
+
* @param {string} icon - Illustration for generated views. Default 'headphones'.
|
|
9
|
+
* @param {number} hue - Backdrop hue for generated views. Default 38.
|
|
10
|
+
* @param {number} views - Number of generated views (2–5). Default 4.
|
|
11
|
+
* @param {string} layout - Thumbnail position (bottom|side). Default 'side'.
|
|
12
|
+
* @param {string} size - Stage ratio preset (sm|md|lg). Default 'md'.
|
|
13
|
+
* @param {string} className - Extra class.
|
|
14
|
+
*
|
|
15
|
+
* Accessibility: thumbnails are buttons with aria-pressed and labels; arrow keys move
|
|
16
|
+
* between thumbnails; the stage announces the current view number.
|
|
17
|
+
*/
|
|
18
|
+
import ProductMedia from './ProductMedia.astro';
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
images?: string[];
|
|
22
|
+
alt?: string;
|
|
23
|
+
icon?: string;
|
|
24
|
+
hue?: number;
|
|
25
|
+
views?: number;
|
|
26
|
+
layout?: 'bottom' | 'side';
|
|
27
|
+
size?: 'sm' | 'md' | 'lg';
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
images = [],
|
|
33
|
+
alt = 'Product photo',
|
|
34
|
+
icon = 'headphones',
|
|
35
|
+
hue = 38,
|
|
36
|
+
views = 4,
|
|
37
|
+
layout = 'side',
|
|
38
|
+
size = 'md',
|
|
39
|
+
className = '',
|
|
40
|
+
} = Astro.props;
|
|
41
|
+
|
|
42
|
+
const generated = Array.from({ length: Math.min(5, Math.max(2, views)) }, (_, i) => ({
|
|
43
|
+
hue: (hue + i * 18) % 360,
|
|
44
|
+
variant: (i % 2 === 0 ? 'glow' : 'soft') as 'glow' | 'soft',
|
|
45
|
+
turn: [0, -14, 12, -4, 20][i] ?? 0,
|
|
46
|
+
zoom: [1, 1.18, 0.92, 1.3, 1.05][i] ?? 1,
|
|
47
|
+
}));
|
|
48
|
+
const slides = images.length
|
|
49
|
+
? images.map((src, i) => ({ src, label: `${alt} ${i + 1}` }))
|
|
50
|
+
: generated.map((g, i) => ({ ...g, label: `View ${i + 1}` }));
|
|
51
|
+
const ratio = size === 'sm' ? '1 / 1' : size === 'lg' ? '4 / 5' : '5 / 5.4';
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
<div class="p-gallery-cq">
|
|
55
|
+
<div class:list={['p-gallery', `p-gallery--${layout}`, `p-gallery--${size}`, className]} data-gallery>
|
|
56
|
+
<div class="p-gallery__stage" aria-live="polite">
|
|
57
|
+
{slides.map((s: any, i) => (
|
|
58
|
+
<div class:list={['p-gallery__slide', { 'is-active': i === 0 }]} data-slide hidden={i !== 0} style={s.turn !== undefined ? `--turn: ${s.turn}deg; --zoom: ${s.zoom}` : ''}>
|
|
59
|
+
{s.src
|
|
60
|
+
? <ProductMedia image={s.src} alt={s.label} ratio={ratio} />
|
|
61
|
+
: <ProductMedia icon={icon} hue={s.hue} variant={s.variant} ratio={ratio} size="lg" />}
|
|
62
|
+
<span class="sr-only">{s.label} of {slides.length}</span>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
<div class="p-gallery__thumbs" role="group" aria-label="Product views">
|
|
67
|
+
{slides.map((s: any, i) => (
|
|
68
|
+
<button class:list={['p-gallery__thumb']} type="button" aria-pressed={i === 0 ? 'true' : 'false'} aria-label={`Show ${s.label}`} data-thumb={i} style={s.turn !== undefined ? `--turn: ${s.turn}deg; --zoom: ${s.zoom}` : ''}>
|
|
69
|
+
{s.src
|
|
70
|
+
? <ProductMedia image={s.src} alt="" ratio="1 / 1" />
|
|
71
|
+
: <ProductMedia icon={icon} hue={s.hue} variant={s.variant} ratio="1 / 1" size="sm" />}
|
|
72
|
+
</button>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<script>
|
|
79
|
+
document.querySelectorAll<HTMLElement>('[data-gallery]:not([data-bound])').forEach((g) => {
|
|
80
|
+
g.dataset.bound = '';
|
|
81
|
+
const slides = Array.from(g.querySelectorAll<HTMLElement>('[data-slide]'));
|
|
82
|
+
const thumbs = Array.from(g.querySelectorAll<HTMLButtonElement>('[data-thumb]'));
|
|
83
|
+
const show = (i: number, focus = false) => {
|
|
84
|
+
slides.forEach((s, n) => { s.hidden = n !== i; s.classList.toggle('is-active', n === i); });
|
|
85
|
+
thumbs.forEach((t, n) => t.setAttribute('aria-pressed', String(n === i)));
|
|
86
|
+
if (focus) thumbs[i]?.focus();
|
|
87
|
+
};
|
|
88
|
+
thumbs.forEach((t, i) => {
|
|
89
|
+
t.addEventListener('click', () => show(i));
|
|
90
|
+
t.addEventListener('keydown', (e) => {
|
|
91
|
+
const keys: Record<string, number> = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 };
|
|
92
|
+
if (!(e.key in keys)) return;
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
show((i + keys[e.key] + thumbs.length) % thumbs.length, true);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<style>
|
|
101
|
+
.p-gallery-cq { container-type: inline-size; }
|
|
102
|
+
.p-gallery { display: grid; gap: var(--sp-3, 0.75rem); }
|
|
103
|
+
.p-gallery--side { grid-template-columns: 84px minmax(0, 1fr); }
|
|
104
|
+
.p-gallery--side .p-gallery__stage { order: 2; }
|
|
105
|
+
.p-gallery--side .p-gallery__thumbs { order: 1; flex-direction: column; }
|
|
106
|
+
|
|
107
|
+
.p-gallery__stage {
|
|
108
|
+
position: relative;
|
|
109
|
+
border-radius: var(--r-xl, 28px);
|
|
110
|
+
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
}
|
|
113
|
+
.p-gallery__slide[hidden] { display: none; }
|
|
114
|
+
.p-gallery__slide.is-active { animation: pg-in 0.35s ease-out; }
|
|
115
|
+
.p-gallery__slide :global(.p-media__art),
|
|
116
|
+
.p-gallery__thumb :global(.p-media__art) { transform: rotate(var(--turn, 0deg)) scale(var(--zoom, 1)); }
|
|
117
|
+
|
|
118
|
+
.p-gallery__thumbs { display: flex; gap: var(--sp-2, 0.5rem); }
|
|
119
|
+
.p-gallery__thumb {
|
|
120
|
+
flex: 0 0 84px;
|
|
121
|
+
width: 84px;
|
|
122
|
+
padding: 0;
|
|
123
|
+
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
|
|
124
|
+
border-radius: var(--r-md, 12px);
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
background: none;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
opacity: 0.6;
|
|
129
|
+
transition: opacity 0.2s ease, border-color 0.2s ease;
|
|
130
|
+
}
|
|
131
|
+
.p-gallery__thumb:hover { opacity: 1; }
|
|
132
|
+
.p-gallery__thumb[aria-pressed='true'] { opacity: 1; border-color: var(--c-primary-light, #d4a017); }
|
|
133
|
+
.p-gallery__thumb:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
|
|
134
|
+
|
|
135
|
+
@keyframes pg-in { from { opacity: 0.4; transform: scale(0.985); } }
|
|
136
|
+
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
137
|
+
|
|
138
|
+
@container (max-width: 700px) {
|
|
139
|
+
.p-gallery--side { grid-template-columns: 1fr; }
|
|
140
|
+
.p-gallery--side .p-gallery__stage { order: 1; }
|
|
141
|
+
.p-gallery--side .p-gallery__thumbs { order: 2; flex-direction: row; }
|
|
142
|
+
.p-gallery__thumb { flex-basis: 64px; width: 64px; }
|
|
143
|
+
}
|
|
144
|
+
@media (prefers-reduced-motion: reduce) { .p-gallery__slide.is-active { animation: none; } }
|
|
145
|
+
</style>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ProductGrid — Responsive grid of StoreProductCard tiles with an empty state.
|
|
4
|
+
* Works on its own or as the target of FilterSidebar / ProductToolbar (same `id`).
|
|
5
|
+
*
|
|
6
|
+
* @param {Product[]} products - Products to render.
|
|
7
|
+
* @param {string} id - Grid id; FilterSidebar and ProductToolbar point at it. Default 'product-grid'.
|
|
8
|
+
* @param {string} productBase - Product URL prefix; links become `${productBase}${slug}/`. Default '#'.
|
|
9
|
+
* @param {Record<string, string>} categoryNames - Category id → label for the card eyebrow.
|
|
10
|
+
* @param {number} columns - Desktop columns (2–5). Default 4.
|
|
11
|
+
* @param {string} cardVariant - StoreProductCard variant (glass|minimal|bordered). Default 'glass'.
|
|
12
|
+
* @param {string} cardSize - StoreProductCard size (sm|md|lg). Default 'md'.
|
|
13
|
+
* @param {string} currency - ISO currency. Default 'USD'.
|
|
14
|
+
* @param {string} locale - Number format locale. Default 'en-US'.
|
|
15
|
+
* @param {string} cartKey - Cart storage key. Default 'ack-cart'.
|
|
16
|
+
* @param {boolean} openCart - Open the drawer after adding (false = toast). Default true.
|
|
17
|
+
* @param {string} emptyTitle - Empty state heading. Default 'No products match'.
|
|
18
|
+
* @param {string} emptyText - Empty state copy.
|
|
19
|
+
* @param {string} className - Extra class.
|
|
20
|
+
*
|
|
21
|
+
* Accessibility: cards keep their own headings; the empty state is a polite status message.
|
|
22
|
+
*/
|
|
23
|
+
import StoreProductCard from './StoreProductCard.astro';
|
|
24
|
+
import type { Product } from './types';
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
products?: Product[];
|
|
28
|
+
id?: string;
|
|
29
|
+
productBase?: string;
|
|
30
|
+
categoryNames?: Record<string, string>;
|
|
31
|
+
columns?: number;
|
|
32
|
+
cardVariant?: 'glass' | 'minimal' | 'bordered';
|
|
33
|
+
cardSize?: 'sm' | 'md' | 'lg';
|
|
34
|
+
currency?: string;
|
|
35
|
+
locale?: string;
|
|
36
|
+
cartKey?: string;
|
|
37
|
+
openCart?: boolean;
|
|
38
|
+
emptyTitle?: string;
|
|
39
|
+
emptyText?: string;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
products = [
|
|
45
|
+
{ slug: 'aurora-headphones', name: 'Aurora Wireless Headphones', price: 189, compareAt: 229, category: 'audio', rating: 4.8, reviews: 312, badge: '-18%', badgeVariant: 'warning', icon: 'headphones', hue: 38 },
|
|
46
|
+
{ slug: 'orbit-watch', name: 'Orbit Smart Watch', price: 249, category: 'wearables', rating: 4.6, reviews: 198, badge: 'New', badgeVariant: 'info', icon: 'watch', hue: 205 },
|
|
47
|
+
{ slug: 'trail-backpack', name: 'Trail 24L Backpack', price: 119, category: 'bags', rating: 4.7, reviews: 87, icon: 'backpack', hue: 150 },
|
|
48
|
+
{ slug: 'halo-lamp', name: 'Halo Desk Lamp', price: 79, category: 'home', rating: 4.5, reviews: 54, icon: 'lamp', hue: 25 },
|
|
49
|
+
],
|
|
50
|
+
id = 'product-grid',
|
|
51
|
+
productBase = '#',
|
|
52
|
+
categoryNames = { audio: 'Audio', wearables: 'Wearables', bags: 'Bags', home: 'Home' },
|
|
53
|
+
columns = 4,
|
|
54
|
+
cardVariant = 'glass',
|
|
55
|
+
cardSize = 'md',
|
|
56
|
+
currency = 'USD',
|
|
57
|
+
locale = 'en-US',
|
|
58
|
+
cartKey = 'ack-cart',
|
|
59
|
+
openCart = true,
|
|
60
|
+
emptyTitle = 'No products match',
|
|
61
|
+
emptyText = 'Try removing a filter or widening the price range.',
|
|
62
|
+
className = '',
|
|
63
|
+
} = Astro.props;
|
|
64
|
+
|
|
65
|
+
const cols = Math.min(5, Math.max(2, columns));
|
|
66
|
+
const hrefFor = (slug: string) => (productBase === '#' ? '#' : `${productBase}${slug}/`);
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
<div class:list={['p-grid-wrap', className]}>
|
|
70
|
+
<div class="p-grid" id={id} style={`--pg-cols: ${cols}`} data-product-list>
|
|
71
|
+
{products.map((p, i) => (
|
|
72
|
+
<StoreProductCard
|
|
73
|
+
product={p}
|
|
74
|
+
href={hrefFor(p.slug)}
|
|
75
|
+
categoryLabel={categoryNames[p.category] ?? ''}
|
|
76
|
+
variant={cardVariant}
|
|
77
|
+
size={cardSize}
|
|
78
|
+
currency={currency}
|
|
79
|
+
locale={locale}
|
|
80
|
+
cartKey={cartKey}
|
|
81
|
+
openCart={openCart}
|
|
82
|
+
/>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
<div class="p-grid__empty" role="status" data-product-empty hidden>
|
|
86
|
+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5M8 11h6"/></svg>
|
|
87
|
+
<p><strong>{emptyTitle}</strong></p>
|
|
88
|
+
<p>{emptyText}</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
.p-grid-wrap { container-type: inline-size; }
|
|
94
|
+
.p-grid {
|
|
95
|
+
display: grid;
|
|
96
|
+
grid-template-columns: repeat(var(--pg-cols), minmax(0, 1fr));
|
|
97
|
+
gap: var(--sp-5, 1.25rem);
|
|
98
|
+
}
|
|
99
|
+
.p-grid > :global([hidden]) { display: none; }
|
|
100
|
+
.p-grid__empty {
|
|
101
|
+
display: grid;
|
|
102
|
+
justify-items: center;
|
|
103
|
+
gap: var(--sp-2, 0.5rem);
|
|
104
|
+
padding: var(--sp-16, 4rem) var(--sp-6, 1.5rem);
|
|
105
|
+
border: 1px dashed rgb(var(--ink-rgb, 255 255 255) / 0.1);
|
|
106
|
+
border-radius: var(--r-lg, 20px);
|
|
107
|
+
text-align: center;
|
|
108
|
+
color: var(--c-text-2, #a3a3a3);
|
|
109
|
+
}
|
|
110
|
+
.p-grid__empty[hidden] { display: none; }
|
|
111
|
+
.p-grid__empty svg { color: var(--c-primary-light, #d4a017); }
|
|
112
|
+
.p-grid__empty p { margin: 0; }
|
|
113
|
+
.p-grid__empty strong { color: var(--c-text-1, #fff); font-size: 1.05rem; }
|
|
114
|
+
|
|
115
|
+
@container (max-width: 1100px) { .p-grid { grid-template-columns: repeat(min(var(--pg-cols), 3), minmax(0, 1fr)); } }
|
|
116
|
+
@container (max-width: 760px) { .p-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3, 0.75rem); } }
|
|
117
|
+
@container (max-width: 380px) { .p-grid { grid-template-columns: 1fr; } }
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ProductMedia — Product visual: a real photo, or a generated backdrop with a
|
|
4
|
+
* line-art illustration so demo catalogs look finished without external images.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} image - Product photo URL. When set, replaces the illustration.
|
|
7
|
+
* @param {string} alt - Alternative text. Required for photos; illustrations are decorative.
|
|
8
|
+
* @param {string} icon - Built-in illustration (headphones|watch|sneaker|backpack|camera|lamp|keyboard|sunglasses|bottle|speaker|jacket|mug|plant|shirt|bag|phone).
|
|
9
|
+
* @param {number} hue - 0–360 hue of the generated backdrop. Default 40 (gold).
|
|
10
|
+
* @param {string} ratio - CSS aspect-ratio. Default '1 / 1'.
|
|
11
|
+
* @param {string} size - Illustration scale (sm|md|lg). Default 'md'.
|
|
12
|
+
* @param {string} variant - Backdrop style (glow|soft|flat). Default 'glow'.
|
|
13
|
+
* @param {string} className - Extra class.
|
|
14
|
+
*
|
|
15
|
+
* Accessibility: illustrations are aria-hidden; photos need `alt`.
|
|
16
|
+
*/
|
|
17
|
+
import { productIcon } from './product-icons';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
image?: string;
|
|
21
|
+
alt?: string;
|
|
22
|
+
icon?: string;
|
|
23
|
+
hue?: number;
|
|
24
|
+
ratio?: string;
|
|
25
|
+
size?: 'sm' | 'md' | 'lg';
|
|
26
|
+
variant?: 'glow' | 'soft' | 'flat';
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
image,
|
|
32
|
+
alt = '',
|
|
33
|
+
icon = 'bag',
|
|
34
|
+
hue = 40,
|
|
35
|
+
ratio = '1 / 1',
|
|
36
|
+
size = 'md',
|
|
37
|
+
variant = 'glow',
|
|
38
|
+
className = '',
|
|
39
|
+
} = Astro.props;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<div
|
|
43
|
+
class:list={['p-media', `p-media--${size}`, `p-media--${variant}`, className]}
|
|
44
|
+
style={`--pm-hue: ${hue}; --pm-ratio: ${ratio};`}
|
|
45
|
+
>
|
|
46
|
+
{image ? (
|
|
47
|
+
<img class="p-media__img" src={image} alt={alt} loading="lazy" decoding="async" />
|
|
48
|
+
) : (
|
|
49
|
+
<svg class="p-media__art" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><Fragment set:html={productIcon(icon)} /></svg>
|
|
50
|
+
)}
|
|
51
|
+
<slot />
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<style>
|
|
55
|
+
.p-media {
|
|
56
|
+
--pm-bg-1: hsl(var(--pm-hue) 70% 50% / 0.28);
|
|
57
|
+
--pm-bg-2: hsl(var(--pm-hue) 60% 30% / 0.14);
|
|
58
|
+
--pm-ink: hsl(var(--pm-hue) var(--pm-ink-s, 80%) var(--pm-ink-l, 80%));
|
|
59
|
+
position: relative;
|
|
60
|
+
display: grid;
|
|
61
|
+
place-items: center;
|
|
62
|
+
aspect-ratio: var(--pm-ratio);
|
|
63
|
+
width: 100%;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
border-radius: inherit;
|
|
66
|
+
background:
|
|
67
|
+
radial-gradient(circle at 50% 42%, var(--pm-bg-1), transparent 62%),
|
|
68
|
+
linear-gradient(160deg, var(--pm-bg-2), var(--c-bg-1, #0a0a0a) 75%);
|
|
69
|
+
isolation: isolate;
|
|
70
|
+
}
|
|
71
|
+
.p-media--soft {
|
|
72
|
+
background: linear-gradient(160deg, hsl(var(--pm-hue) var(--pm-soft-s, 40%) var(--pm-soft-l, 20%) / var(--pm-soft-a, 0.5)), var(--c-bg-1, #0a0a0a));
|
|
73
|
+
}
|
|
74
|
+
.p-media--flat { background: var(--c-bg-2, #111); }
|
|
75
|
+
|
|
76
|
+
.p-media::after {
|
|
77
|
+
content: '';
|
|
78
|
+
position: absolute;
|
|
79
|
+
inset: auto 22% 14% 22%;
|
|
80
|
+
height: 10%;
|
|
81
|
+
border-radius: 50%;
|
|
82
|
+
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.55), transparent 70%);
|
|
83
|
+
z-index: -1;
|
|
84
|
+
}
|
|
85
|
+
.p-media--flat::after { display: none; }
|
|
86
|
+
|
|
87
|
+
.p-media__art {
|
|
88
|
+
width: 46%;
|
|
89
|
+
height: auto;
|
|
90
|
+
color: var(--pm-ink);
|
|
91
|
+
filter: drop-shadow(0 6px 18px hsl(var(--pm-hue) 80% 50% / 0.35));
|
|
92
|
+
transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
|
|
93
|
+
}
|
|
94
|
+
.p-media--sm .p-media__art { width: 58%; stroke-width: 2.4; }
|
|
95
|
+
.p-media--lg .p-media__art { width: 40%; stroke-width: 1.4; }
|
|
96
|
+
|
|
97
|
+
.p-media__img {
|
|
98
|
+
position: absolute;
|
|
99
|
+
inset: 0;
|
|
100
|
+
width: 100%;
|
|
101
|
+
height: 100%;
|
|
102
|
+
object-fit: cover;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
:global(.is-hoverable:hover) .p-media__art,
|
|
106
|
+
:global(.is-hoverable:focus-within) .p-media__art {
|
|
107
|
+
transform: translateY(-4%) rotate(-4deg) scale(1.04);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (prefers-reduced-motion: reduce) {
|
|
111
|
+
.p-media__art { transition: none; }
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ProductRating — Read-only star rating with value and review count.
|
|
4
|
+
*
|
|
5
|
+
* @param {number} value - Average rating, 0–5. Default 4.6.
|
|
6
|
+
* @param {number} count - Number of reviews (optional).
|
|
7
|
+
* @param {string} size - Visual size (sm|md|lg). Default 'sm'.
|
|
8
|
+
* @param {boolean} showValue - Print the numeric value. Default true.
|
|
9
|
+
* @param {string} label - Accessible label template; `{value}` and `{count}` are replaced.
|
|
10
|
+
* @param {string} className - Extra class.
|
|
11
|
+
*
|
|
12
|
+
* Accessibility: exposed as one `role="img"` with a full text label; stars are decorative.
|
|
13
|
+
*/
|
|
14
|
+
interface Props {
|
|
15
|
+
value?: number;
|
|
16
|
+
count?: number;
|
|
17
|
+
size?: 'sm' | 'md' | 'lg';
|
|
18
|
+
showValue?: boolean;
|
|
19
|
+
label?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
value = 4.6,
|
|
25
|
+
count,
|
|
26
|
+
size = 'sm',
|
|
27
|
+
showValue = true,
|
|
28
|
+
label = 'Rated {value} out of 5{count}',
|
|
29
|
+
className = '',
|
|
30
|
+
} = Astro.props;
|
|
31
|
+
|
|
32
|
+
const clamped = Math.max(0, Math.min(5, value));
|
|
33
|
+
const pct = `${(clamped / 5) * 100}%`;
|
|
34
|
+
const aria = label
|
|
35
|
+
.replace('{value}', clamped.toFixed(1))
|
|
36
|
+
.replace('{count}', count !== undefined ? `, ${count} reviews` : '');
|
|
37
|
+
const stars = '★★★★★';
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<span class:list={['p-rating', `p-rating--${size}`, className]} role="img" aria-label={aria}>
|
|
41
|
+
<span class="p-rating__stars" aria-hidden="true" style={`--p-rating-fill: ${pct}`}>
|
|
42
|
+
<span class="p-rating__base">{stars}</span>
|
|
43
|
+
<span class="p-rating__fill">{stars}</span>
|
|
44
|
+
</span>
|
|
45
|
+
{showValue && <span class="p-rating__value" aria-hidden="true">{clamped.toFixed(1)}</span>}
|
|
46
|
+
{count !== undefined && <span class="p-rating__count" aria-hidden="true">({count})</span>}
|
|
47
|
+
</span>
|
|
48
|
+
|
|
49
|
+
<style>
|
|
50
|
+
.p-rating {
|
|
51
|
+
--p-rating-star: var(--c-primary-light, #d4a017);
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 0.35em;
|
|
55
|
+
font-size: 0.8rem;
|
|
56
|
+
line-height: 1;
|
|
57
|
+
color: var(--c-text-2, #a3a3a3);
|
|
58
|
+
}
|
|
59
|
+
.p-rating--md { font-size: 0.95rem; }
|
|
60
|
+
.p-rating--lg { font-size: 1.15rem; }
|
|
61
|
+
|
|
62
|
+
.p-rating__stars {
|
|
63
|
+
position: relative;
|
|
64
|
+
display: inline-block;
|
|
65
|
+
letter-spacing: 0.08em;
|
|
66
|
+
}
|
|
67
|
+
.p-rating__base { color: rgb(var(--ink-rgb, 255 255 255) / 0.14); }
|
|
68
|
+
.p-rating__fill {
|
|
69
|
+
position: absolute;
|
|
70
|
+
inset: 0 auto 0 0;
|
|
71
|
+
width: var(--p-rating-fill);
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
color: var(--p-rating-star);
|
|
75
|
+
}
|
|
76
|
+
.p-rating__value { font-weight: 700; color: var(--c-text-1, #fff); }
|
|
77
|
+
.p-rating__count { color: var(--c-text-3, #737373); }
|
|
78
|
+
</style>
|