Add customer and business user-product flows across dashboards.

Ship my-products / customer-products UI with gallery uploads, status controls, module gating, and related shared UI polish.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-10 00:23:03 +03:30
co-authored by Cursor
parent c5bdaad16b
commit 1271d96539
96 changed files with 10532 additions and 345 deletions
@@ -43,12 +43,12 @@
z-index: 1200;
transform: translateX(-50%);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 6px;
flex-direction: column;
gap: 10px;
width: max-content;
min-width: 196px;
max-width: 280px;
padding: 8px 10px;
padding: 10px;
border-radius: var(--radius-sm);
border: 1px solid rgba(255, 255, 255, 0.85);
background: rgba(255, 255, 255, 0.78);
@@ -57,6 +57,63 @@
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.16);
}
.section {
display: flex;
flex-direction: column;
gap: 6px;
}
.sectionLabel {
margin: 0;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-muted);
}
.themeRow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.themeOption {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 32px;
padding: 0 10px;
border-radius: var(--radius-sm);
border: 1px solid rgba(148, 163, 184, 0.28);
background: rgba(255, 255, 255, 0.55);
color: var(--text-secondary);
font-size: 12px;
font-weight: 500;
transition: border-color 0.15s, background 0.15s, color 0.15s, box-shadow 0.15s;
}
.themeOption:hover {
border-color: rgba(148, 163, 184, 0.45);
color: var(--text-primary);
}
.themeOptionSelected {
border-color: var(--primary);
background: rgba(var(--primary-rgb) / 0.1);
color: var(--primary);
box-shadow: 0 0 0 2px rgba(var(--primary-rgb) / 0.12);
}
.colorRow {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: 6px;
}
.option {
width: 28px;
height: 28px;
@@ -1,15 +1,19 @@
import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { Moon, Sun } from 'lucide-react'
import {
BUSINESS_PRIMARY_COLOR_IDS,
BUSINESS_PRIMARY_COLOR_PALETTE,
type BusinessPrimaryColorId,
} from '../utils/businessPrimaryColors'
import type { DashboardThemeMode } from '../services/businessSettingsService'
import styles from './PrimaryColorSwatchControl.module.css'
interface PrimaryColorSwatchControlProps {
value: BusinessPrimaryColorId
themeMode: DashboardThemeMode
onChange: (value: BusinessPrimaryColorId) => void
onThemeModeChange: (value: DashboardThemeMode) => void
disabled?: boolean
}
@@ -20,7 +24,9 @@ type PopoverPosition = {
export function PrimaryColorSwatchControl({
value,
themeMode,
onChange,
onThemeModeChange,
disabled = false,
}: PrimaryColorSwatchControlProps) {
const [open, setOpen] = useState(false)
@@ -60,6 +66,12 @@ export function PrimaryColorSwatchControl({
setOpen(false)
}
function selectThemeMode(mode: DashboardThemeMode) {
if (mode !== themeMode) {
onThemeModeChange(mode)
}
}
useEffect(() => {
if (!open) return
@@ -99,10 +111,10 @@ export function PrimaryColorSwatchControl({
className={`${styles.trigger} ${open ? styles.triggerOpen : ''}`}
onClick={toggleOpen}
disabled={disabled}
aria-label={`Dashboard primary color: ${tokens.label}`}
aria-haspopup="listbox"
aria-label={`Dashboard theme: ${themeMode}, color: ${tokens.label}`}
aria-haspopup="dialog"
aria-expanded={open}
title={`Theme: ${tokens.label}`}
title={`Theme: ${themeMode} · ${tokens.label}`}
>
<span
className={styles.swatchDot}
@@ -117,35 +129,66 @@ export function PrimaryColorSwatchControl({
<div
ref={popoverRef}
className={styles.popover}
role="listbox"
aria-label="Choose primary color"
role="dialog"
aria-label="Choose dashboard theme and primary color"
style={{
top: popoverPos.top,
left: popoverPos.left,
}}
>
{BUSINESS_PRIMARY_COLOR_IDS.map((colorId) => {
const option = BUSINESS_PRIMARY_COLOR_PALETTE[colorId]
const selected = colorId === value
return (
<div className={styles.section}>
<p className={styles.sectionLabel}>Theme</p>
<div className={styles.themeRow} role="radiogroup" aria-label="Theme mode">
<button
key={colorId}
type="button"
role="option"
aria-selected={selected}
aria-label={option.label}
className={`${styles.option} ${selected ? styles.optionSelected : ''}`}
onClick={() => selectColor(colorId)}
role="radio"
aria-checked={themeMode === 'light'}
className={`${styles.themeOption} ${themeMode === 'light' ? styles.themeOptionSelected : ''}`}
onClick={() => selectThemeMode('light')}
>
<span
className={styles.optionDot}
style={{ backgroundColor: option.primary }}
aria-hidden="true"
/>
<Sun size={14} aria-hidden="true" />
<span>Light</span>
</button>
)
})}
<button
type="button"
role="radio"
aria-checked={themeMode === 'dark'}
className={`${styles.themeOption} ${themeMode === 'dark' ? styles.themeOptionSelected : ''}`}
onClick={() => selectThemeMode('dark')}
>
<Moon size={14} aria-hidden="true" />
<span>Dark</span>
</button>
</div>
</div>
<div className={styles.section}>
<p className={styles.sectionLabel}>Color</p>
<div className={styles.colorRow} role="listbox" aria-label="Primary color">
{BUSINESS_PRIMARY_COLOR_IDS.map((colorId) => {
const option = BUSINESS_PRIMARY_COLOR_PALETTE[colorId]
const selected = colorId === value
return (
<button
key={colorId}
type="button"
role="option"
aria-selected={selected}
aria-label={option.label}
className={`${styles.option} ${selected ? styles.optionSelected : ''}`}
onClick={() => selectColor(colorId)}
>
<span
className={styles.optionDot}
style={{ backgroundColor: option.primary }}
aria-hidden="true"
/>
</button>
)
})}
</div>
</div>
</div>,
document.body,
)}