@pradip1995/segment-product-card 0.3.21 → 0.3.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pradip1995/segment-product-card",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -20,6 +20,7 @@
20
20
  "peerDependencies": {
21
21
  "@pradip1995/commerce-core": "^4.0.0",
22
22
  "@pradip1995/plugin-sdk": "^0.2.0",
23
+ "@pradip1995/segment-animation-login-popup": "*",
23
24
  "next": ">=15",
24
25
  "react": ">=19",
25
26
  "react-dom": ">=19"
@@ -29,10 +30,14 @@
29
30
  "@pradip1995/segment-tokens": "^0.3.7"
30
31
  },
31
32
  "devDependencies": {
33
+ "@medusajs/types": "^2.0.0",
32
34
  "@pradip1995/plugin-sdk": "^0.2.0",
33
35
  "@types/react": "^19",
36
+ "@types/react-dom": "^19",
37
+ "next": ">=15",
34
38
  "react": "19.0.3",
35
- "typescript": "^5.7.2"
39
+ "typescript": "^5.7.2",
40
+ "@pradip1995/segment-animation-login-popup": "0.1.1"
36
41
  },
37
42
  "scripts": {
38
43
  "typecheck": "tsc --noEmit",
@@ -5,6 +5,10 @@ import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
5
5
  import { formatPrice } from "@pradip1995/segment-primitives/format-price"
6
6
  import type { HttpTypes } from "@medusajs/types"
7
7
  import ProductCardActions from "./product-card-actions"
8
+ import {
9
+ getProductColorSwatches,
10
+ isLightSwatch,
11
+ } from "./color-swatches"
8
12
 
9
13
  type Props = {
10
14
  product: HttpTypes.StoreProduct
@@ -46,6 +50,7 @@ export default function BeautyProductCard({
46
50
  : null
47
51
 
48
52
  const subtitle = productSubtitle(product)
53
+ const colors = getProductColorSwatches(product)
49
54
 
50
55
  return (
51
56
  <article
@@ -89,6 +94,28 @@ export default function BeautyProductCard({
89
94
  <p className="beauty-product-card__eyebrow">{subtitle}</p>
90
95
  <h3 className="beauty-product-card__title">{product.title}</h3>
91
96
 
97
+ {colors.length > 0 ? (
98
+ <div className="beauty-product-card__swatches" aria-label="Available colors">
99
+ {colors.map((color) => {
100
+ const swatch = color.swatch
101
+ const light = isLightSwatch(swatch)
102
+ return (
103
+ <span
104
+ key={color.name}
105
+ title={color.name}
106
+ aria-label={color.name}
107
+ className={`beauty-product-card__swatch${light ? " is-light" : ""}${
108
+ !swatch ? " is-label" : ""
109
+ }`}
110
+ style={swatch ? { backgroundColor: swatch } : undefined}
111
+ >
112
+ {!swatch ? color.name.slice(0, 1) : null}
113
+ </span>
114
+ )
115
+ })}
116
+ </div>
117
+ ) : null}
118
+
92
119
  <div className="beauty-product-card__footer">
93
120
  {rating?.rating != null && rating.rating > 0 ? (
94
121
  <p className="beauty-product-card__rating" aria-label={`${rating.rating} out of 5 stars`}>
@@ -0,0 +1,177 @@
1
+ /** Shared admin color → CSS swatch resolution (matches PDP logic). */
2
+
3
+ const ADMIN_COLOR_SWATCHES: Record<string, string> = {
4
+ black: "#111111",
5
+ white: "#ffffff",
6
+ red: "#c1272d",
7
+ blue: "#2563eb",
8
+ green: "#16a34a",
9
+ yellow: "#eab308",
10
+ orange: "#ea580c",
11
+ pink: "#ec4899",
12
+ purple: "#7c3aed",
13
+ brown: "#8b5e3c",
14
+ gray: "#9ca3af",
15
+ grey: "#9ca3af",
16
+ silver: "#c0c0c0",
17
+ gold: "#c9a86c",
18
+ navy: "#1e3a5f",
19
+ maroon: "#800000",
20
+ teal: "#0d9488",
21
+ cyan: "#06b6d4",
22
+ lime: "#84cc16",
23
+ indigo: "#4f46e5",
24
+ violet: "#8b5cf6",
25
+ magenta: "#d946ef",
26
+ beige: "#d4b896",
27
+ ivory: "#fffff0",
28
+ khaki: "#c3b091",
29
+ coral: "#ff7f50",
30
+ salmon: "#fa8072",
31
+ chocolate: "#7b3f00",
32
+ olive: "#556b2f",
33
+ tan: "#d2b48c",
34
+ cream: "#f5ebe0",
35
+ "off white": "#f5f5f0",
36
+ offwhite: "#f5f5f0",
37
+ nude: "#e8c4a8",
38
+ charcoal: "#36454f",
39
+ burgundy: "#800020",
40
+ rose: "#e8a4bf",
41
+ "navy blue": "#1e3a5f",
42
+ "light blue": "#93c5fd",
43
+ "sky blue": "#7dd3fc",
44
+ sky: "#7dd3fc",
45
+ camel: "#c19a6b",
46
+ rust: "#b7410e",
47
+ mustard: "#e1ad01",
48
+ wine: "#722f37",
49
+ peach: "#ffcba4",
50
+ mint: "#98d8c8",
51
+ lavender: "#b57edc",
52
+ }
53
+
54
+ function isHexColor(value: string) {
55
+ return /^#?[0-9a-f]{3}([0-9a-f]{3})?([0-9a-f]{2})?$/i.test(value.trim())
56
+ }
57
+
58
+ function normalizeColorKey(value: string) {
59
+ return value.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim()
60
+ }
61
+
62
+ function readOptionMeta(meta: Record<string, unknown> | null | undefined, key: string) {
63
+ const value = meta?.[key]
64
+ return typeof value === "string" ? value : null
65
+ }
66
+
67
+ /** Resolve swatch from admin value/metadata only — no random invented colors. */
68
+ export function resolveAdminSwatch(
69
+ ...candidates: Array<string | null | undefined>
70
+ ): string | null {
71
+ for (const raw of candidates) {
72
+ const value = (raw || "").trim()
73
+ if (!value) continue
74
+
75
+ if (isHexColor(value)) {
76
+ return value.startsWith("#") ? value : `#${value}`
77
+ }
78
+ if (/^(rgb|hsl)a?\(/i.test(value)) return value
79
+
80
+ const key = normalizeColorKey(value)
81
+ if (ADMIN_COLOR_SWATCHES[key]) return ADMIN_COLOR_SWATCHES[key]
82
+
83
+ const firstWord = key.split(" ")[0]
84
+ if (ADMIN_COLOR_SWATCHES[firstWord]) return ADMIN_COLOR_SWATCHES[firstWord]
85
+ }
86
+ return null
87
+ }
88
+
89
+ export type ProductColorSwatch = {
90
+ name: string
91
+ swatch: string | null
92
+ }
93
+
94
+ type OptionValueLike = {
95
+ value?: string | null
96
+ metadata?: Record<string, unknown> | null
97
+ }
98
+
99
+ type OptionLike = {
100
+ title?: string | null
101
+ values?: OptionValueLike[] | null
102
+ }
103
+
104
+ type VariantLike = {
105
+ options?: Array<{ value?: string | null; option?: { title?: string | null } }> | null
106
+ }
107
+
108
+ type ProductLike = {
109
+ options?: OptionLike[] | null
110
+ variants?: VariantLike[] | null
111
+ }
112
+
113
+ export function getProductColorSwatches(product: ProductLike): ProductColorSwatch[] {
114
+ const colorOption = product.options?.find((opt) => {
115
+ const title = (opt.title || "").toLowerCase()
116
+ return title === "color" || title === "colour"
117
+ })
118
+
119
+ if (colorOption?.values?.length) {
120
+ const unique = Array.from(
121
+ new Set(
122
+ colorOption.values
123
+ .map((v) => String(v.value || "").trim())
124
+ .filter(Boolean)
125
+ )
126
+ )
127
+ return unique.map((name) => {
128
+ const optionValue = colorOption.values?.find((ov) => String(ov.value) === name)
129
+ const meta = optionValue?.metadata
130
+ return {
131
+ name,
132
+ swatch: resolveAdminSwatch(
133
+ name,
134
+ readOptionMeta(meta, "color"),
135
+ readOptionMeta(meta, "hex"),
136
+ readOptionMeta(meta, "swatch"),
137
+ readOptionMeta(meta, "value")
138
+ ),
139
+ }
140
+ })
141
+ }
142
+
143
+ const fromVariants = Array.from(
144
+ new Set(
145
+ (product.variants ?? [])
146
+ .flatMap((variant) =>
147
+ (variant.options ?? [])
148
+ .filter((o) => {
149
+ const t = (o.option?.title || "").toLowerCase()
150
+ return t === "color" || t === "colour"
151
+ })
152
+ .map((o) => String(o.value || "").trim())
153
+ )
154
+ .filter(Boolean)
155
+ )
156
+ )
157
+
158
+ return fromVariants.map((name) => ({
159
+ name,
160
+ swatch: resolveAdminSwatch(name),
161
+ }))
162
+ }
163
+
164
+ export function isLightSwatch(swatch: string | null): boolean {
165
+ if (!swatch) return false
166
+ const s = swatch.toLowerCase()
167
+ return (
168
+ s === "#fff" ||
169
+ s === "#ffffff" ||
170
+ s === "white" ||
171
+ s === "ivory" ||
172
+ s === "snow" ||
173
+ s === "#fffff0" ||
174
+ s === "#f5f5f0" ||
175
+ s === "#f5ebe0"
176
+ )
177
+ }
@@ -4,6 +4,10 @@ import Image from "next/image"
4
4
  import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
5
5
  import { formatPrice } from "@pradip1995/segment-primitives/format-price"
6
6
  import type { HttpTypes } from "@medusajs/types"
7
+ import {
8
+ getProductColorSwatches,
9
+ isLightSwatch,
10
+ } from "./color-swatches"
7
11
 
8
12
  type Props = {
9
13
  product: HttpTypes.StoreProduct
@@ -18,22 +22,16 @@ export default function MotionProductCard({
18
22
  product,
19
23
  region,
20
24
  countryCode: countryCodeProp,
21
- rating,
22
- showWishlist = true,
23
- wishlistProductIds,
24
25
  }: Props) {
25
26
  const variant = product.variants?.[0]
26
27
  const price = variant?.calculated_price?.calculated_amount
27
- const originalPrice = variant?.calculated_price?.original_amount
28
28
  const currency = region?.currency_code || variant?.calculated_price?.currency_code || "inr"
29
29
  const countryCode =
30
30
  countryCodeProp?.toLowerCase() ||
31
31
  region?.countries?.[0]?.iso_2?.toLowerCase() ||
32
32
  "in"
33
33
 
34
- // Get color options for swatches
35
- const colorOption = product.options?.find((opt: any) => opt.title?.toLowerCase() === 'color' || opt.title?.toLowerCase() === 'colour')
36
- const colorValues = colorOption?.values?.map((v: any) => v.value) || []
34
+ const colors = getProductColorSwatches(product)
37
35
 
38
36
  return (
39
37
  <article
@@ -50,13 +48,13 @@ export default function MotionProductCard({
50
48
  fill
51
49
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 50vw"
52
50
  className={`object-cover transition-all duration-[800ms] ease-[cubic-bezier(0.16,1,0.3,1)] ${
53
- product.images && product.images.length > 1
54
- ? 'group-hover/media:opacity-0'
55
- : 'group-hover/media:scale-105'
51
+ product.images && product.images.length > 1
52
+ ? "group-hover/media:opacity-0"
53
+ : "group-hover/media:scale-105"
56
54
  }`}
57
55
  loading="lazy"
58
56
  />
59
- {product.images && product.images.length > 1 && (
57
+ {product.images && product.images.length > 1 ? (
60
58
  <Image
61
59
  src={product.images[1].url.trim()}
62
60
  alt={product.title || ""}
@@ -65,16 +63,17 @@ export default function MotionProductCard({
65
63
  className="absolute inset-0 object-cover opacity-0 scale-100 group-hover/media:opacity-100 group-hover/media:scale-105 transition-all duration-[800ms] ease-[cubic-bezier(0.16,1,0.3,1)]"
66
64
  loading="lazy"
67
65
  />
68
- )}
66
+ ) : null}
69
67
  </>
70
68
  ) : (
71
69
  <div className="w-full h-full bg-[#F6F6F6]" />
72
70
  )}
73
71
 
74
- {/* Quick view floating circle */}
75
72
  <div className="absolute top-3 right-3 z-10 opacity-0 group-hover/media:opacity-100 transition-all duration-300 pointer-events-none translate-y-1 group-hover/media:translate-y-0">
76
73
  <div className="w-14 h-14 rounded-full bg-white text-gray-900 flex items-center justify-center text-[10px] uppercase font-bold tracking-widest text-center leading-[1.1] shadow-[0_4px_15px_rgba(0,0,0,0.1)] pointer-events-auto cursor-pointer hover:bg-gray-50 hover:scale-105 transition-transform">
77
- Quick<br/>view
74
+ Quick
75
+ <br />
76
+ view
78
77
  </div>
79
78
  </div>
80
79
  </div>
@@ -90,19 +89,27 @@ export default function MotionProductCard({
90
89
  </div>
91
90
  ) : null}
92
91
 
93
- {/* Color Swatches */}
94
- {colorValues.length > 0 && (
95
- <div className="mt-2 flex items-center space-x-1.5">
96
- {colorValues.map((color: string, idx: number) => (
97
- <div
98
- key={idx}
99
- className="w-[14px] h-[14px] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.1)]"
100
- style={{ backgroundColor: color.toLowerCase() }}
101
- title={color}
102
- />
103
- ))}
92
+ {colors.length > 0 ? (
93
+ <div className="mt-2 flex items-center gap-1.5" aria-label="Available colors">
94
+ {colors.map((color) => {
95
+ const swatch = color.swatch
96
+ const light = isLightSwatch(swatch)
97
+ return (
98
+ <span
99
+ key={color.name}
100
+ title={color.name}
101
+ aria-label={color.name}
102
+ className={`inline-flex items-center justify-center w-[14px] h-[14px] text-[8px] font-semibold uppercase shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12)] ${
103
+ light ? "border border-gray-300" : ""
104
+ } ${!swatch ? "bg-[#f3f3f3] text-[#111]" : ""}`}
105
+ style={swatch ? { backgroundColor: swatch } : undefined}
106
+ >
107
+ {!swatch ? color.name.slice(0, 1) : null}
108
+ </span>
109
+ )
110
+ })}
104
111
  </div>
105
- )}
112
+ ) : null}
106
113
  </div>
107
114
  </LocalizedLink>
108
115
  </article>
@@ -10,23 +10,24 @@ import {
10
10
  } from "@pradip1995/commerce-core/client/actions/wishlist"
11
11
  import type { HttpTypes } from "@medusajs/types"
12
12
  import { isNextRedirect } from "@pradip1995/segment-primitives/is-next-redirect"
13
- import LoginRequiredModal from "@pradip1995/segment-login-popup/required"
13
+ import LoginRequiredModal from "@pradip1995/segment-animation-login-popup/required"
14
14
  import QuickAddPanel from "./quick-add-panel"
15
15
 
16
16
  type Props = {
17
17
  product: HttpTypes.StoreProduct
18
18
  countryCode: string
19
19
  wishlistProductIds?: string[]
20
+ showWishlist?: boolean
20
21
  }
21
22
 
22
23
  function findVariantForOptions(
23
24
  product: HttpTypes.StoreProduct,
24
25
  selectedOptions: Record<string, string>
25
26
  ) {
26
- return product.variants?.find((variant) => {
27
+ return product.variants?.find((variant: { options?: Array<{ option_id?: string | null; value?: string | null }> }) => {
27
28
  const variantOptions =
28
29
  variant.options?.reduce(
29
- (acc, option) => {
30
+ (acc: Record<string, string>, option: { option_id?: string | null; value?: string | null }) => {
30
31
  if (option.option_id) acc[option.option_id] = option.value ?? ""
31
32
  return acc
32
33
  },
@@ -38,7 +39,12 @@ function findVariantForOptions(
38
39
  })
39
40
  }
40
41
 
41
- export default function ProductCardActions({ product, countryCode, wishlistProductIds }: Props) {
42
+ export default function ProductCardActions({
43
+ product,
44
+ countryCode,
45
+ wishlistProductIds,
46
+ showWishlist = true,
47
+ }: Props) {
42
48
  const router = useRouter()
43
49
  const [wishlisted, setWishlisted] = useState(false)
44
50
  const [wishlistLoading, setWishlistLoading] = useState(false)
@@ -176,23 +182,25 @@ export default function ProductCardActions({ product, countryCode, wishlistProdu
176
182
  return (
177
183
  <>
178
184
  <div className="product-card__actions absolute bottom-3.5 right-3.5 z-30 opacity-0 group-hover/media:opacity-100 transition-opacity duration-300 pointer-events-none group-hover/media:pointer-events-auto" onClick={stopCardNav}>
179
- <button
180
- type="button"
181
- className={`product-card__wishlist-btn${wishlisted ? " product-card__wishlist-btn--active" : ""}${
182
- wishlistLoading ? " product-card__wishlist-btn--loading" : ""
183
- }`}
184
- onClick={handleWishlistToggle}
185
- disabled={wishlistLoading}
186
- aria-label={wishlisted ? "Remove from wishlist" : "Add to wishlist"}
187
- >
188
- <svg
189
- viewBox="0 0 24 24"
190
- className="product-card__wishlist-icon"
191
- aria-hidden
185
+ {showWishlist ? (
186
+ <button
187
+ type="button"
188
+ className={`product-card__wishlist-btn${wishlisted ? " product-card__wishlist-btn--active" : ""}${
189
+ wishlistLoading ? " product-card__wishlist-btn--loading" : ""
190
+ }`}
191
+ onClick={handleWishlistToggle}
192
+ disabled={wishlistLoading}
193
+ aria-label={wishlisted ? "Remove from wishlist" : "Add to wishlist"}
192
194
  >
193
- <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
194
- </svg>
195
- </button>
195
+ <svg
196
+ viewBox="0 0 24 24"
197
+ className="product-card__wishlist-icon"
198
+ aria-hidden
199
+ >
200
+ <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
201
+ </svg>
202
+ </button>
203
+ ) : null}
196
204
 
197
205
  <button
198
206
  type="button"
package/src/segment.css CHANGED
@@ -154,6 +154,35 @@
154
154
  min-height: 2.5rem;
155
155
  }
156
156
 
157
+ .beauty-product-card__swatches {
158
+ display: flex;
159
+ flex-wrap: wrap;
160
+ align-items: center;
161
+ gap: 0.35rem;
162
+ margin: 0.45rem 0 0.15rem;
163
+ }
164
+
165
+ .beauty-product-card__swatch {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ width: 0.875rem;
170
+ height: 0.875rem;
171
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
172
+ font-size: 0.5rem;
173
+ font-weight: 700;
174
+ text-transform: uppercase;
175
+ color: #111;
176
+ }
177
+
178
+ .beauty-product-card__swatch.is-light {
179
+ border: 1px solid #d4d4d4;
180
+ }
181
+
182
+ .beauty-product-card__swatch.is-label {
183
+ background: #f3f3f3;
184
+ }
185
+
157
186
  .beauty-product-card__footer {
158
187
  display: flex;
159
188
  align-items: flex-end;
@@ -7,6 +7,10 @@ import { formatPrice } from "@pradip1995/segment-primitives/format-price"
7
7
  import type { HttpTypes } from "@medusajs/types"
8
8
  import ProductCardActions from "./product-card-actions"
9
9
  import { resolveProductCardImages } from "./resolve-product-images"
10
+ import {
11
+ getProductColorSwatches,
12
+ isLightSwatch,
13
+ } from "./color-swatches"
10
14
 
11
15
  type Props = {
12
16
  product: HttpTypes.StoreProduct
@@ -17,13 +21,11 @@ type Props = {
17
21
  wishlistProductIds?: string[]
18
22
  }
19
23
 
20
- type OptionWithValue = { value?: string | null }
21
- type OptionItem = { title?: string | null; values?: OptionWithValue[] | null }
22
-
23
24
  export default function ShopProductCard({
24
25
  product,
25
26
  region,
26
27
  countryCode: countryCodeProp,
28
+ showWishlist = false,
27
29
  wishlistProductIds,
28
30
  }: Props) {
29
31
  const variant = product.variants?.[0]
@@ -38,26 +40,9 @@ export default function ShopProductCard({
38
40
  const [mainImgSrc, setMainImgSrc] = useState(primary)
39
41
  const [hoverImgSrc, setHoverImgSrc] = useState(hover)
40
42
 
41
- // Extract color options dynamically from product options
42
- const colorOption = (product.options as OptionItem[] | undefined)?.find(
43
- (opt) => opt.title?.toLowerCase() === "color" || opt.title?.toLowerCase() === "colour"
44
- )
45
- const rawColorValues: string[] = colorOption?.values
46
- ?.map((v: OptionWithValue) => v.value)
47
- .filter((val: string | null | undefined): val is string => Boolean(val)) || []
48
-
49
- // Fallback to variant options if no top-level color options exist
50
- const variantColors: string[] = Array.from(
51
- new Set(
52
- (product.variants ?? [])
53
- .flatMap((v: unknown) => (v as { options?: OptionWithValue[] }).options?.map((o: OptionWithValue) => o.value))
54
- .filter((val: string | null | undefined): val is string => Boolean(val))
55
- )
56
- )
57
- const colorValues: string[] = rawColorValues.length > 0 ? rawColorValues : variantColors
58
-
43
+ const colors = getProductColorSwatches(product)
59
44
  const [selectedColor, setSelectedColor] = useState<string | null>(
60
- colorValues.length > 0 ? (colorValues[0] as string) : null
45
+ colors.length > 0 ? colors[0].name : null
61
46
  )
62
47
 
63
48
  const originalPrice = variant?.calculated_price?.original_amount
@@ -65,9 +50,11 @@ export default function ShopProductCard({
65
50
  originalPrice && price && originalPrice > price
66
51
  ? Math.round(((originalPrice - price) / originalPrice) * 100)
67
52
  : 0
68
-
53
+
69
54
  const isBestSelling = product.tags?.some(
70
- (tag: any) => tag.value?.toLowerCase() === "best selling" || tag.value?.toLowerCase() === "bestseller"
55
+ (tag: { value?: string }) =>
56
+ tag.value?.toLowerCase() === "best selling" ||
57
+ tag.value?.toLowerCase() === "bestseller"
71
58
  )
72
59
 
73
60
  return (
@@ -150,6 +137,7 @@ export default function ShopProductCard({
150
137
  product={product}
151
138
  countryCode={countryCode}
152
139
  wishlistProductIds={wishlistProductIds}
140
+ showWishlist={showWishlist}
153
141
  />
154
142
  ) : null}
155
143
  </div>
@@ -166,32 +154,34 @@ export default function ShopProductCard({
166
154
  ) : null}
167
155
 
168
156
  {/* Dynamic Color Swatches below price */}
169
- {colorValues.length > 0 && (
157
+ {colors.length > 0 && (
170
158
  <div className="mt-2.5 flex items-center gap-1.5">
171
- {colorValues.map((color: string, idx: number) => {
172
- const isSelected = selectedColor === color
173
- const lowerColor = color.toLowerCase()
174
- const isWhite = lowerColor === "#ffffff" || lowerColor === "white"
159
+ {colors.map((color) => {
160
+ const isSelected = selectedColor === color.name
161
+ const swatch = color.swatch
162
+ const light = isLightSwatch(swatch)
175
163
  return (
176
164
  <button
177
- key={idx}
165
+ key={color.name}
178
166
  type="button"
179
167
  onClick={(e) => {
180
168
  e.preventDefault()
181
169
  e.stopPropagation()
182
- setSelectedColor(color)
170
+ setSelectedColor(color.name)
183
171
  }}
184
- className={`w-4 h-4 transition-all relative rounded-none ${
185
- isWhite ? "border border-gray-300" : ""
172
+ className={`inline-flex items-center justify-center w-4 h-4 transition-all relative rounded-none text-[8px] font-semibold uppercase ${
173
+ light ? "border border-gray-300" : ""
186
174
  } ${
187
175
  isSelected
188
176
  ? "ring-1 ring-black ring-offset-1 scale-110 z-10"
189
177
  : "hover:scale-105 opacity-90 hover:opacity-100"
190
- }`}
191
- style={{ backgroundColor: lowerColor }}
192
- title={color}
193
- aria-label={`Select ${color} color`}
194
- />
178
+ } ${!swatch ? "bg-[#f3f3f3] text-[#111]" : ""}`}
179
+ style={swatch ? { backgroundColor: swatch } : undefined}
180
+ title={color.name}
181
+ aria-label={`Select ${color.name} color`}
182
+ >
183
+ {!swatch ? color.name.slice(0, 1) : null}
184
+ </button>
195
185
  )
196
186
  })}
197
187
  </div>