Add password modal and VIP product controls on clothes list.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
1041ab20c5
commit
4a516e31db
+288
-30
@@ -8,6 +8,7 @@ import IconButton, {
|
||||
DiscountIcon,
|
||||
EditIcon,
|
||||
FilterIcon,
|
||||
SubscriptionIcon,
|
||||
TrashIcon,
|
||||
VariantIcon,
|
||||
} from "@/components/ui/IconButton";
|
||||
@@ -94,6 +95,14 @@ export default function ProductListPage() {
|
||||
const [discountProduct, setDiscountProduct] = useState<Product | null>(null);
|
||||
const [discountRows, setDiscountRows] = useState<ProductVariantPrice[]>([]);
|
||||
|
||||
const [vipProduct, setVipProduct] = useState<Product | null>(null);
|
||||
const [vipEnabled, setVipEnabled] = useState(false);
|
||||
const [vipOnly, setVipOnly] = useState(false);
|
||||
const [vipSpecialPrice, setVipSpecialPrice] = useState(false);
|
||||
const [vipRows, setVipRows] = useState<ProductVariantPrice[]>([]);
|
||||
const [vipSaving, setVipSaving] = useState(false);
|
||||
const [vipError, setVipError] = useState<string | null>(null);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setLoadError(null);
|
||||
@@ -178,6 +187,99 @@ export default function ProductListPage() {
|
||||
setDiscountProduct(null);
|
||||
}
|
||||
|
||||
function openVip(product: Product) {
|
||||
setVipProduct(product);
|
||||
setVipEnabled(Boolean(product.vipEnabled));
|
||||
setVipOnly(Boolean(product.vipOnly));
|
||||
setVipSpecialPrice(Boolean(product.vipSpecialPrice));
|
||||
if (product.variantPrices?.length) {
|
||||
setVipRows(product.variantPrices.map((row) => ({ ...row })));
|
||||
} else {
|
||||
setVipRows([
|
||||
{
|
||||
...emptyVariantRow(String(product.price)),
|
||||
vipPrice:
|
||||
typeof product.vipPrice === "number"
|
||||
? String(product.vipPrice)
|
||||
: "",
|
||||
},
|
||||
]);
|
||||
}
|
||||
setVipError(null);
|
||||
}
|
||||
|
||||
function closeVip() {
|
||||
setVipProduct(null);
|
||||
setVipRows([]);
|
||||
setVipError(null);
|
||||
setVipSaving(false);
|
||||
}
|
||||
|
||||
function updateVipPrice(id: string, value: string) {
|
||||
setVipRows((prev) =>
|
||||
prev.map((row) => (row.id === id ? { ...row, vipPrice: value } : row)),
|
||||
);
|
||||
}
|
||||
|
||||
async function saveVip(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!vipProduct) return;
|
||||
setVipSaving(true);
|
||||
setVipError(null);
|
||||
|
||||
const enabled = vipEnabled;
|
||||
const only = enabled && vipOnly;
|
||||
const special = enabled && vipSpecialPrice;
|
||||
const hadVariants = Boolean(vipProduct.variantPrices?.length);
|
||||
|
||||
let nextVipPrice: number | null = null;
|
||||
let nextVariants: ReturnType<typeof toVariantPayload> | undefined;
|
||||
|
||||
if (special) {
|
||||
const nextRows = vipRows.map((row) => ({ ...row }));
|
||||
const priced = nextRows
|
||||
.map((row) => Number(row.vipPrice))
|
||||
.filter((n) => !Number.isNaN(n) && n > 0);
|
||||
const baseVip = nextRows.find((row) => isBaseRow(row))?.vipPrice;
|
||||
nextVipPrice = baseVip
|
||||
? Number(baseVip) || null
|
||||
: priced.length
|
||||
? Math.min(...priced)
|
||||
: null;
|
||||
if (hadVariants) {
|
||||
nextVariants = toVariantPayload(nextRows);
|
||||
}
|
||||
} else if (hadVariants) {
|
||||
nextVariants = toVariantPayload(
|
||||
(vipProduct.variantPrices ?? []).map((row) => ({
|
||||
...row,
|
||||
vipPrice: "",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await updateProduct(vipProduct.id, {
|
||||
vipEnabled: enabled,
|
||||
vipOnly: only,
|
||||
vipSpecialPrice: special,
|
||||
vipPrice: special ? nextVipPrice : null,
|
||||
...(nextVariants ? { variants: nextVariants } : {}),
|
||||
});
|
||||
setItems((prev) =>
|
||||
prev.map((item) => (item.id === updated.id ? updated : item)),
|
||||
);
|
||||
if (detailProduct?.id === updated.id) setDetailProduct(updated);
|
||||
closeVip();
|
||||
} catch (err) {
|
||||
setVipError(
|
||||
err instanceof ApiError ? err.message : "ذخیره دسترسی VIP ناموفق بود.",
|
||||
);
|
||||
} finally {
|
||||
setVipSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
function updateRow(
|
||||
id: string,
|
||||
field: keyof Omit<ProductVariantPrice, "id">,
|
||||
@@ -483,6 +585,16 @@ export default function ProductListPage() {
|
||||
>
|
||||
<DiscountIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
label="دسترسی VIP"
|
||||
variant="ghost"
|
||||
className={
|
||||
product.vipEnabled ? "customer-sub-premium" : undefined
|
||||
}
|
||||
onClick={() => openVip(product)}
|
||||
>
|
||||
<SubscriptionIcon premium={Boolean(product.vipEnabled)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
@@ -845,42 +957,188 @@ export default function ProductListPage() {
|
||||
: "بدون تنوع";
|
||||
|
||||
return (
|
||||
<div key={row.id} className="variant-price-row">
|
||||
<div className="mb-2 text-sm text-ink">{label}</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div className="clothes-field">
|
||||
<span className="block text-xs text-muted">قیمت فعلی</span>
|
||||
<p className="mt-1.5 text-sm text-ink">
|
||||
<span className="font-montserrat">
|
||||
{Number.isNaN(currentPrice)
|
||||
? row.price || "—"
|
||||
: formatPrice(currentPrice)}
|
||||
</span>
|
||||
{!Number.isNaN(currentPrice) && row.price ? (
|
||||
<span className="ms-1">تومان</span>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
<div className="clothes-field">
|
||||
<label htmlFor={`disc-new-${row.id}`}>قیمت تخفیف</label>
|
||||
<input
|
||||
id={`disc-new-${row.id}`}
|
||||
dir="ltr"
|
||||
inputMode="numeric"
|
||||
className="!text-end font-montserrat"
|
||||
value={row.discountPrice ?? ""}
|
||||
onChange={(e) =>
|
||||
updateDiscountPrice(row.id, e.target.value)
|
||||
}
|
||||
placeholder="3570000"
|
||||
/>
|
||||
</div>
|
||||
<div key={row.id} className="variant-price-row variant-price-row-inline">
|
||||
<div className="variant-price-label">{label}</div>
|
||||
<div className="clothes-field">
|
||||
<span className="block text-xs text-muted">قیمت فعلی</span>
|
||||
<p className="text-sm text-ink">
|
||||
<span className="font-montserrat">
|
||||
{Number.isNaN(currentPrice)
|
||||
? row.price || "—"
|
||||
: formatPrice(currentPrice)}
|
||||
</span>
|
||||
{!Number.isNaN(currentPrice) && row.price ? (
|
||||
<span className="ms-1">تومان</span>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
<div className="clothes-field">
|
||||
<label htmlFor={`disc-new-${row.id}`}>قیمت تخفیف</label>
|
||||
<input
|
||||
id={`disc-new-${row.id}`}
|
||||
dir="ltr"
|
||||
inputMode="numeric"
|
||||
className="!text-end font-montserrat"
|
||||
value={row.discountPrice ?? ""}
|
||||
onChange={(e) =>
|
||||
updateDiscountPrice(row.id, e.target.value)
|
||||
}
|
||||
placeholder="3570000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormModal>
|
||||
|
||||
<FormModal
|
||||
open={Boolean(vipProduct)}
|
||||
title={`دسترسی VIP — ${vipProduct?.nameFa ?? ""}`}
|
||||
onClose={closeVip}
|
||||
onSubmit={(e) => void saveVip(e)}
|
||||
submitLabel={vipSaving ? "..." : "ذخیره"}
|
||||
wide="xl"
|
||||
>
|
||||
{vipError ? (
|
||||
<p className="text-sm text-red-600" role="alert">
|
||||
{vipError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<label className="mana-switch">
|
||||
<span className="mana-switch-copy">
|
||||
<span className="mana-switch-title">فعالسازی VIP</span>
|
||||
<span className="mana-switch-hint">
|
||||
{vipEnabled
|
||||
? "تنظیمات VIP برای این محصول فعال است."
|
||||
: "برای تنظیم دسترسی و قیمت VIP، این گزینه را روشن کنید."}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="mana-switch-track"
|
||||
data-on={vipEnabled ? "true" : "false"}
|
||||
>
|
||||
<span className="mana-switch-thumb" />
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
aria-checked={vipEnabled}
|
||||
checked={vipEnabled}
|
||||
onChange={(e) => setVipEnabled(e.target.checked)}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
className="vip-modal-options"
|
||||
data-disabled={vipEnabled ? "false" : "true"}
|
||||
aria-disabled={!vipEnabled}
|
||||
>
|
||||
<label className="mana-switch">
|
||||
<span className="mana-switch-copy">
|
||||
<span className="mana-switch-title">
|
||||
نمایش ندادن به مشتری عادی
|
||||
</span>
|
||||
<span className="mana-switch-hint">
|
||||
محصول اصلاً برای مشتریان عادی نمایش داده نمیشود.
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="mana-switch-track"
|
||||
data-on={vipOnly ? "true" : "false"}
|
||||
>
|
||||
<span className="mana-switch-thumb" />
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
aria-checked={vipOnly}
|
||||
checked={vipOnly}
|
||||
disabled={!vipEnabled}
|
||||
onChange={(e) => setVipOnly(e.target.checked)}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="mana-switch">
|
||||
<span className="mana-switch-copy">
|
||||
<span className="mana-switch-title">قیمت ویژه VIP</span>
|
||||
<span className="mana-switch-hint">
|
||||
برای اعضای VIP قیمت جداگانه تعریف کنید.
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="mana-switch-track"
|
||||
data-on={vipSpecialPrice ? "true" : "false"}
|
||||
>
|
||||
<span className="mana-switch-thumb" />
|
||||
<input
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
aria-checked={vipSpecialPrice}
|
||||
checked={vipSpecialPrice}
|
||||
disabled={!vipEnabled}
|
||||
onChange={(e) => setVipSpecialPrice(e.target.checked)}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{vipSpecialPrice ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs leading-6 text-muted">
|
||||
تنوعهای موجود و قیمت فعلی را ببینید و قیمت VIP را وارد کنید.
|
||||
</p>
|
||||
{vipRows.map((row) => {
|
||||
const currentPrice = Number(row.price);
|
||||
const label = vipProduct
|
||||
? getVariantLabel(
|
||||
row,
|
||||
vipProduct.categoryId,
|
||||
categoryTree,
|
||||
)
|
||||
: "بدون تنوع";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={row.id}
|
||||
className="variant-price-row variant-price-row-inline"
|
||||
>
|
||||
<div className="variant-price-label">{label}</div>
|
||||
<div className="clothes-field">
|
||||
<span className="block text-xs text-muted">قیمت فعلی</span>
|
||||
<p className="text-sm text-ink">
|
||||
<span className="font-montserrat">
|
||||
{Number.isNaN(currentPrice)
|
||||
? row.price || "—"
|
||||
: formatPrice(currentPrice)}
|
||||
</span>
|
||||
{!Number.isNaN(currentPrice) && row.price ? (
|
||||
<span className="ms-1">تومان</span>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
<div className="clothes-field">
|
||||
<label htmlFor={`vip-price-${row.id}`}>قیمت VIP</label>
|
||||
<input
|
||||
id={`vip-price-${row.id}`}
|
||||
dir="ltr"
|
||||
inputMode="numeric"
|
||||
className="!text-end font-montserrat"
|
||||
value={row.vipPrice ?? ""}
|
||||
disabled={!vipEnabled}
|
||||
onChange={(e) =>
|
||||
updateVipPrice(row.id, e.target.value)
|
||||
}
|
||||
placeholder="3570000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</FormModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+193
-32
@@ -7,6 +7,7 @@
|
||||
--muted: #6b6b6b;
|
||||
--line: #e4e4e4;
|
||||
--ink: #0a0a0a;
|
||||
--radius-card: 4px;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -297,7 +298,7 @@ body {
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--surface);
|
||||
padding: 1.15rem 1rem;
|
||||
align-items: center;
|
||||
@@ -366,7 +367,7 @@ body {
|
||||
}
|
||||
|
||||
.dash-chart {
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
/* Clothes admin */
|
||||
@@ -378,7 +379,7 @@ body {
|
||||
background: var(--ink);
|
||||
color: #fff;
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
transition:
|
||||
color 0.35s ease,
|
||||
background-color 0.35s ease,
|
||||
@@ -411,7 +412,7 @@ body {
|
||||
padding: 0.45rem 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--ink);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-card);
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
@@ -445,7 +446,7 @@ body {
|
||||
max-height: 16rem;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.35rem 0;
|
||||
@@ -533,7 +534,7 @@ body {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
transition:
|
||||
border-color 0.3s ease,
|
||||
transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
@@ -548,7 +549,7 @@ body {
|
||||
|
||||
.product-card-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
@@ -566,7 +567,7 @@ body {
|
||||
.tree-box {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -583,7 +584,7 @@ body {
|
||||
gap: 1rem;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 0.85rem 0.95rem;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
@@ -612,7 +613,7 @@ body {
|
||||
justify-content: center;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-card);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
@@ -637,7 +638,7 @@ body {
|
||||
content: attr(data-tooltip);
|
||||
bottom: calc(100% + 8px);
|
||||
white-space: nowrap;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--ink);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
@@ -702,7 +703,7 @@ body {
|
||||
gap: 0.5rem;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 0.65rem;
|
||||
}
|
||||
|
||||
@@ -726,7 +727,7 @@ body {
|
||||
gap: 1rem;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 0.85rem;
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
@@ -741,7 +742,7 @@ body {
|
||||
gap: 1rem;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 0.9rem 1rem;
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
@@ -789,7 +790,7 @@ body {
|
||||
gap: 1rem;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 0.75rem;
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
@@ -804,7 +805,7 @@ body {
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 3 / 2;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #f3f3f1;
|
||||
}
|
||||
|
||||
@@ -816,7 +817,7 @@ body {
|
||||
|
||||
.rich-text {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -878,7 +879,7 @@ body {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin: 0.75rem 0;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
@@ -908,7 +909,7 @@ body {
|
||||
height: 2.25rem;
|
||||
padding: 0 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #fff;
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
@@ -960,7 +961,7 @@ body {
|
||||
max-height: min(92dvh, 40rem);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #fff;
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.16);
|
||||
animation: modal-in 0.22s ease;
|
||||
@@ -1014,7 +1015,7 @@ body {
|
||||
font-size: 1.35rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
@@ -1036,7 +1037,7 @@ body {
|
||||
|
||||
.modal-btn-ghost,
|
||||
.modal-btn-solid {
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 0.55rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
@@ -1092,7 +1093,7 @@ body {
|
||||
z-index: 1;
|
||||
width: min(100%, 22rem);
|
||||
border: 1px solid #e8d0d2;
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #fff;
|
||||
padding: 1.25rem 1.15rem 1.1rem;
|
||||
box-shadow: 0 24px 60px rgba(120, 10, 20, 0.22);
|
||||
@@ -1123,7 +1124,7 @@ body {
|
||||
.confirm-btn-danger {
|
||||
min-width: 5.5rem;
|
||||
padding: 0.55rem 1rem;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-card);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
@@ -1165,7 +1166,7 @@ body {
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
border: 1px dashed var(--line);
|
||||
background: #f3f3f1;
|
||||
flex-shrink: 0;
|
||||
@@ -1189,7 +1190,7 @@ body {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 0.7rem 1.35rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
@@ -1273,7 +1274,7 @@ body {
|
||||
0 8px,
|
||||
8px -8px,
|
||||
-8px 0;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
transition: aspect-ratio 0.25s ease;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
@@ -1307,14 +1308,14 @@ body {
|
||||
|
||||
.address-block {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #fff;
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
.variant-type-block {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
padding: 0.75rem;
|
||||
background: #fafaf8;
|
||||
}
|
||||
@@ -1391,11 +1392,40 @@ body {
|
||||
|
||||
.variant-price-row {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #fff;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.variant-price-row-inline {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 0.75rem 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.variant-price-row-inline {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.variant-price-row-inline .variant-price-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--ink);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.variant-price-row-inline .clothes-field {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.variant-price-row-inline .clothes-field label,
|
||||
.variant-price-row-inline .clothes-field > span {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.variant-price-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@@ -1480,7 +1510,7 @@ body {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
font-size: 1.1rem;
|
||||
@@ -1517,7 +1547,7 @@ body {
|
||||
.gallery-slot {
|
||||
aspect-ratio: 1;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1543,3 +1573,134 @@ body {
|
||||
font-size: 0.75rem;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.mana-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-card);
|
||||
background: #fff;
|
||||
padding: 0.95rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mana-switch-copy {
|
||||
min-width: 0;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.mana-switch-title {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.mana-switch-hint {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.mana-switch-track {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 3.1rem;
|
||||
height: 1.55rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-card);
|
||||
background: #f3f3f1;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.mana-switch-track[data-on="true"] {
|
||||
border-color: var(--ink);
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
.mana-switch-track::before,
|
||||
.mana-switch-track::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
z-index: 0;
|
||||
transform: translateY(-50%);
|
||||
font-family: var(--font-montserrat), system-ui, sans-serif;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.mana-switch-track::before {
|
||||
content: "0";
|
||||
inset-inline-start: 0.35rem;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.mana-switch-track::after {
|
||||
content: "1";
|
||||
inset-inline-end: 0.4rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.mana-switch-track[data-on="true"]::before {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.mana-switch-track[data-on="true"]::after {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mana-switch-thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
inset-inline-start: 2px;
|
||||
z-index: 1;
|
||||
width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
border-radius: calc(var(--radius-card) - 1px);
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
transition: inset-inline-start 0.2s ease;
|
||||
}
|
||||
|
||||
.mana-switch-track[data-on="true"] .mana-switch-thumb {
|
||||
inset-inline-start: calc(100% - 1.15rem - 2px);
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.mana-switch input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vip-modal-options {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
transition:
|
||||
filter 0.25s ease,
|
||||
opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.vip-modal-options[data-disabled="true"] {
|
||||
filter: blur(2.5px);
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vip-modal-options[data-disabled="true"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user