diff --git a/estore/src/app/admin/page.tsx b/estore/src/app/admin/page.tsx
index d94909d..690853d 100644
--- a/estore/src/app/admin/page.tsx
+++ b/estore/src/app/admin/page.tsx
@@ -7,6 +7,7 @@ import type { CatalogItem, ModifierList } from '@/data/mock-catalog'
import type { ItemOverride } from '@/lib/overrides'
import { DEFAULT_HOURS, minsToTime, timeToMins } from '@/lib/hours-config'
import type { HoursConfig, DayHours } from '@/lib/hours-config'
+import AdminColorFilter from '@/components/AdminColorFilter'
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -753,6 +754,8 @@ function ItemEditor({
const [chromeSurcharge, setChromeSurcharge] = useState(
ov.chromeSurchargePerColor ? String(ov.chromeSurchargePerColor / 100) : ''
)
+ const [disabledColors, setDisabledColors] = useState(ov.disabledColors ?? [])
+ const [showColorFilter, setShowColorFilter] = useState(false)
const [quantityUnit, setQuantityUnit] = useState(ov.quantityUnit ?? '')
// Create category
@@ -789,6 +792,7 @@ function ItemEditor({
if (colorMin !== '') patch.colorMin = Number(colorMin)
if (colorMax !== '') patch.colorMax = Number(colorMax)
if (chromeSurcharge !== '') patch.chromeSurchargePerColor = Math.round(Number(chromeSurcharge) * 100)
+ patch.disabledColors = disabledColors.length ? disabledColors : undefined
if (quantityUnit.trim()) patch.quantityUnit = quantityUnit.trim()
else patch.quantityUnit = undefined
@@ -1177,9 +1181,37 @@ function ItemEditor({
Each chrome color selected adds this amount. Leave blank (or 0) to use a flat "Chrome" Square variation instead.
{item.chromeSurchargePerColor > 0 && ` Current: $${(item.chromeSurchargePerColor / 100).toFixed(2)}/color.`}
+
+ {/* Color availability */}
+
+ setShowColorFilter(true)}
+ >
+ 🎨 Manage available colors
+ {disabledColors.length > 0 && (
+
+ {disabledColors.length} hidden
+
+ )}
+
+
)}
+ {showColorFilter && (
+ setShowColorFilter(false)}
+ />
+ )}
+
{/* Quantity unit */}
Quantity unit
diff --git a/estore/src/app/api/catalog/route.ts b/estore/src/app/api/catalog/route.ts
index 17c8bc3..de95b0b 100644
--- a/estore/src/app/api/catalog/route.ts
+++ b/estore/src/app/api/catalog/route.ts
@@ -18,6 +18,7 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
colorMin: ov.colorMin ?? item.colorMin,
colorMax: ov.colorMax !== undefined ? ov.colorMax : item.colorMax,
chromeSurchargePerColor: ov.chromeSurchargePerColor ?? item.chromeSurchargePerColor,
+ disabledColors: ov.disabledColors?.length ? ov.disabledColors : item.disabledColors,
quantityUnit: ov.quantityUnit ?? item.quantityUnit,
description: ov.descriptionOverride ?? item.description,
variations: item.variations
diff --git a/estore/src/components/AdminColorFilter.tsx b/estore/src/components/AdminColorFilter.tsx
new file mode 100644
index 0000000..45d9532
--- /dev/null
+++ b/estore/src/components/AdminColorFilter.tsx
@@ -0,0 +1,221 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { BASE } from '@/lib/basepath'
+
+interface ColorEntry {
+ name: string
+ hex: string
+ metallic?: boolean
+ pearlType?: string
+ chromeType?: string
+ image?: string
+}
+
+interface ColorFamily {
+ family: string
+ colors: ColorEntry[]
+}
+
+interface Props {
+ disabledColors: string[]
+ onSave: (disabled: string[]) => void
+ onClose: () => void
+}
+
+export default function AdminColorFilter({ disabledColors, onSave, onClose }: Props) {
+ const [families, setFamilies] = useState
([])
+ const [disabled, setDisabled] = useState>(() => new Set(disabledColors))
+ const [openFamily, setOpenFamily] = useState(null)
+
+ useEffect(() => {
+ fetch(BASE + '/colors.json')
+ .then((r) => r.json())
+ .then((data: ColorFamily[]) => setFamilies(data))
+ }, [])
+
+ const toggle = (name: string) =>
+ setDisabled((prev) => {
+ const next = new Set(prev)
+ next.has(name) ? next.delete(name) : next.add(name)
+ return next
+ })
+
+ const disabledCount = disabled.size
+ const totalColors = families.reduce((n, f) => n + f.colors.length, 0)
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+
+ Available Colors
+
+
+
+
+
+
+ Click a color to hide it from customers ordering this item.{' '}
+ {disabledCount > 0
+ ? {disabledCount} of {totalColors} hidden.
+ : All {totalColors} colors are currently shown. }
+
+
+ {families.map((family) => {
+ const isOpen = openFamily === family.family
+ const hiddenInFam = family.colors.filter((c) => disabled.has(c.name)).length
+
+ return (
+
+ {/* Family header — same style as customer view */}
+
setOpenFamily(isOpen ? null : family.family)}
+ style={{
+ width: '100%',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ background: isOpen ? '#f0fafb' : '#fafafa',
+ border: '1px solid ' + (isOpen ? '#b2e0e4' : '#e8e8e8'),
+ borderRadius: isOpen ? '10px 10px 0 0' : '10px',
+ padding: '0.55rem 0.9rem',
+ cursor: 'pointer',
+ textAlign: 'left',
+ transition: 'background 0.15s',
+ }}
+ type="button"
+ >
+
+ {/* Preview dots */}
+
+ {family.colors.slice(0, 7).map((c) => (
+
+ ))}
+
+
+ {family.family}
+
+
+
+ {hiddenInFam > 0 && (
+
+ {hiddenInFam} hidden
+
+ )}
+ {isOpen ? '▲' : '▼'}
+
+
+
+ {/* Expanded color grid — same balloon swatch layout as customer */}
+ {isOpen && (
+
+
+ {family.colors.map((color) => {
+ const isDisabled = disabled.has(color.name)
+ const imageSrc = color.image ? `/color/${color.image}` : null
+
+ return (
+
toggle(color.name)}
+ title={isDisabled ? `Enable ${color.name}` : `Hide ${color.name}`}
+ style={{ opacity: isDisabled ? 0.3 : 1, cursor: 'pointer' }}
+ >
+
+ {imageSrc ? (
+
+ ) : (
+
+ )}
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+
+ {isDisabled ? '✕ ' : ''}{color.name}
+
+
+ )
+ })}
+
+
+ )}
+
+ )
+ })}
+
+
+
+
+ setDisabled(new Set())}
+ >
+ Enable all
+
+ setDisabled(new Set(families.flatMap((f) => f.colors.map((c) => c.name))))}
+ >
+ Disable all
+
+
+
+ Cancel
+ { onSave(Array.from(disabled)); onClose() }}
+ >
+ Apply
+
+
+
+
+
+ )
+}
diff --git a/estore/src/components/ColorPicker.tsx b/estore/src/components/ColorPicker.tsx
index 6d6fdba..2684134 100644
--- a/estore/src/components/ColorPicker.tsx
+++ b/estore/src/components/ColorPicker.tsx
@@ -51,10 +51,18 @@ export default function ColorPicker({ product, maxColors, onClose, editingEntry
)
useEffect(() => {
+ const disabled = new Set(product.disabledColors ?? [])
fetch(BASE + '/colors.json')
.then((r) => r.json())
- .then((data: ColorFamily[]) => setFamilies(data))
- }, [])
+ .then((data: ColorFamily[]) => {
+ if (!disabled.size) { setFamilies(data); return }
+ setFamilies(
+ data
+ .map((f) => ({ ...f, colors: f.colors.filter((c) => !disabled.has(c.name)) }))
+ .filter((f) => f.colors.length > 0)
+ )
+ })
+ }, [product.disabledColors])
useEffect(() => {
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
diff --git a/estore/src/data/mock-catalog.ts b/estore/src/data/mock-catalog.ts
index 2f3f611..8774a82 100644
--- a/estore/src/data/mock-catalog.ts
+++ b/estore/src/data/mock-catalog.ts
@@ -39,6 +39,7 @@ export interface CatalogItem {
colorMin: number // minimum colors required when showColors=true (default 1)
colorMax: number | null // maximum colors allowed (null = unlimited)
chromeSurchargePerColor: number // extra cents per chrome color selected (0 = flat chrome variation instead)
+ disabledColors?: string[] // color names hidden from the picker for this item
variations: CatalogVariation[] // all enabled variations; first is the default
/** Unit label for quantity, e.g. "ft". Omitted for plain count items. */
quantityUnit?: string
diff --git a/estore/src/lib/overrides.ts b/estore/src/lib/overrides.ts
index 715a44f..642d9b9 100644
--- a/estore/src/lib/overrides.ts
+++ b/estore/src/lib/overrides.ts
@@ -19,6 +19,8 @@ export interface ItemOverride {
colorMax?: number
/** Extra charge in cents added per chrome color selected (0 = no per-color surcharge). */
chromeSurchargePerColor?: number
+ /** Color names that are hidden from the customer picker for this item. */
+ disabledColors?: string[]
/** Unit label for the quantity field, e.g. "ft". When set, the quantity control shows "X ft". */
quantityUnit?: string
}