Files
nademitex_admin/src/views/products/ProductsView.vue
T
2026-10-01 20:50:23 +03:30

884 lines
28 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import AdminPageSkeleton from '@/components/ui/AdminPageSkeleton.vue'
import BaseModal from '@/components/ui/BaseModal.vue'
import AdminImagePicker from '@/components/ui/AdminImagePicker.vue'
import BaseSwitch from '@/components/ui/BaseSwitch.vue'
import BaseEmptyState from '@/components/ui/BaseEmptyState.vue'
import ActiveSwitchCell from '@/components/ui/ActiveSwitchCell.vue'
import AdminIcon from '@/components/icons/AdminIcon.vue'
import AdminSelect from '@/components/ui/AdminSelect.vue'
import { companiesApi, productsApi, settingsApi } from '@/services/api'
import { ApiError } from '@/services/http'
import { useToast } from '@/composables/useToast'
import { confirm } from '@/composables/useConfirm'
import { formatCurrency, formatNumber } from '@/utils/format'
import type { Company, Product } from '@/types'
import AdminCurrencyInput from '@/components/ui/AdminCurrencyInput.vue'
import { toCssBackgroundUrl } from '@/utils/catalogImage'
import {
PAYMENT_RECIPIENT_COMPANY_SLUG,
productCompanyIdForForm,
resolveProductCompanyPayload,
} from '@/utils/company'
const productList = ref<Product[]>([])
const companies = ref<Company[]>([])
const usageOptions = ref<Array<{ value: string; label: string }>>([
{ value: 'trousers', label: 'شلوار' },
{ value: 'suit', label: 'کت‌وشلوار' },
{ value: 'shirt', label: 'پیراهن' },
{ value: 'other', label: 'سایر' },
])
const seasonOptions = ref<Array<{ value: string; label: string }>>([
{ value: 'summer', label: 'تابستان' },
{ value: 'winter', label: 'زمستان' },
{ value: 'all_season', label: 'چهارفصل' },
])
const searchQuery = ref('')
const companyFilter = ref('all')
const loading = ref(true)
const modalMode = ref<'add' | 'edit' | null>(null)
const editingId = ref<string | null>(null)
const formError = ref('')
const saving = ref(false)
const toast = useToast()
const companyFilterOptions = computed(() => [
{ value: 'all', label: 'همه شرکت‌ها' },
...companies.value.map((company) => ({
value: company.id,
label: company.name,
})),
])
const companyFormOptions = computed(() => {
const opts = [{ value: '', label: 'نادمی تکس (پیش‌فرض)' }]
for (const company of companies.value) {
if (!company.isActive || company.slug === PAYMENT_RECIPIENT_COMPANY_SLUG) continue
opts.push({ value: company.id, label: company.name })
}
return opts
})
const emptyForm = () => ({
companyId: '',
name: '',
color: '',
productCode: '',
minMeterage: 100,
orderStep: 50,
price: null as number | null,
internalStock: 0,
isActive: true,
usageCategory: 'other',
season: 'all_season',
composition: '',
weaveType: '',
subtitle: '',
specs: '',
rollMeters: 50,
isNew: false,
generalImages: [] as string[],
colorRows: [{ id: crypto.randomUUID(), name: '', imageUrl: '' }] as Array<{
id: string
name: string
imageUrl: string
}>,
})
const form = reactive(emptyForm())
const isModalOpen = computed(() => modalMode.value !== null)
const modalTitle = computed(() => (modalMode.value === 'edit' ? 'ویرایش محصول' : 'افزودن محصول'))
const isFormValid = computed(() => form.name.trim() && form.minMeterage > 0)
const hasActiveFilters = computed(
() => searchQuery.value.trim().length > 0 || companyFilter.value !== 'all',
)
const filteredProducts = computed(() => {
let list = productList.value
if (companyFilter.value !== 'all') {
list = list.filter((p) => p.companyId === companyFilter.value)
}
const q = searchQuery.value.trim()
if (q) {
list = list.filter(
(p) =>
p.name.includes(q) ||
p.color.includes(q) ||
p.productCode.includes(q) ||
(p.catalogCode ?? p.productCode).includes(q),
)
}
return list
})
async function loadData() {
loading.value = true
try {
const [productsData, companiesData, settings] = await Promise.all([
productsApi.list(),
companiesApi.list(),
settingsApi.get().catch(() => null),
])
productList.value = productsData
companies.value = companiesData
if (settings?.categoryDefsJson?.usage?.length) {
usageOptions.value = [
...settings.categoryDefsJson.usage.map((item) => ({
value: item.id,
label: item.title,
})),
{ value: 'other', label: 'سایر' },
]
}
if (settings?.categoryDefsJson?.seasons?.length) {
seasonOptions.value = settings.categoryDefsJson.seasons.map((item) => ({
value: item.id,
label: item.title,
}))
}
} catch (err) {
toast.error(err instanceof ApiError ? err.message : 'بارگذاری محصولات انجام نشد.')
} finally {
loading.value = false
}
}
onMounted(() => {
void loadData()
})
function openAddModal() {
modalMode.value = 'add'
editingId.value = null
Object.assign(form, emptyForm())
formError.value = ''
}
async function openEditModal(product: Product) {
modalMode.value = 'edit'
editingId.value = product.id
try {
product = await productsApi.getById(product.id)
} catch {
/* از ردیف جدول */
}
form.companyId = productCompanyIdForForm(product.companyId, companies.value)
form.name = product.name
form.color = product.color
form.productCode = product.productCode
form.minMeterage = product.minMeterage
form.orderStep = product.orderStep
form.price = product.price
form.internalStock = product.internalStock
form.isActive = product.isActive
form.usageCategory = (product as { usageCategory?: string }).usageCategory ?? 'other'
form.season = (product as { season?: string }).season ?? 'all_season'
form.composition = (product as { composition?: string }).composition ?? ''
form.weaveType = (product as { weaveType?: string }).weaveType ?? ''
form.subtitle = (product as { subtitle?: string }).subtitle ?? ''
form.specs = (product as { specs?: string }).specs ?? ''
form.rollMeters = (product as { rollMeters?: number }).rollMeters ?? 50
form.isNew = (product as { isNew?: boolean }).isNew ?? false
form.generalImages = [...(product.imageUrls ?? [])]
const colors = product.colors?.length
? product.colors
: product.color?.trim()
? [product.color.trim()]
: []
if (colors.length) {
form.colorRows = colors.map((name, index) => {
const fromList = product.colorVariantImages?.[index]?.[0]
const legacy = product.colorImages?.[index]
return {
id: crypto.randomUUID(),
name,
imageUrl: fromList || legacy || '',
}
})
} else {
form.colorRows = [{ id: crypto.randomUUID(), name: '', imageUrl: '' }]
}
formError.value = ''
}
function addColorRow() {
form.colorRows.push({ id: crypto.randomUUID(), name: '', imageUrl: '' })
}
function removeColorRow(index: number) {
if (form.colorRows.length <= 1) {
form.colorRows[0] = {
id: form.colorRows[0]?.id ?? crypto.randomUUID(),
name: '',
imageUrl: '',
}
return
}
form.colorRows.splice(index, 1)
}
function addGeneralImageSlot() {
form.generalImages.push('')
}
function removeGeneralImage(index: number) {
form.generalImages.splice(index, 1)
}
function uniqueUrls(urls: string[]) {
const out: string[] = []
const seen = new Set<string>()
for (const raw of urls) {
const url = raw.trim()
if (!url || seen.has(url)) continue
seen.add(url)
out.push(url)
}
return out
}
function buildProductMedia() {
const imageUrls = uniqueUrls(form.generalImages)
const colorEntries: Array<{ name: string; imageUrls: string[]; imageUrl?: string }> = []
form.colorRows.forEach((row, index) => {
const imageUrl = row.imageUrl.trim() || undefined
const name =
row.name.trim() ||
(index === 0 ? form.color.trim() : '') ||
(imageUrl ? `رنگ ${index + 1}` : '')
if (!name && !imageUrl) return
if (!name) return
colorEntries.push({
name,
imageUrls: imageUrl ? [imageUrl] : [],
imageUrl,
})
})
return { colorEntries, imageUrls }
}
function closeModal() {
modalMode.value = null
editingId.value = null
formError.value = ''
}
async function submitProduct() {
if (!isFormValid.value) {
formError.value = 'نام محصول و حداقل متراژ الزامی هستند.'
return
}
const companyIdPayload = resolveProductCompanyPayload(form.companyId)
if (form.companyId.trim()) {
const company = companies.value.find((c) => c.id === form.companyId)
if (!company?.isActive) {
formError.value = 'شرکت معتبر انتخاب کنید.'
return
}
}
const { colorEntries, imageUrls } = buildProductMedia()
const catalogPayload = {
usageCategory: form.usageCategory,
season: form.season,
composition: form.composition.trim() || null,
weaveType: form.weaveType.trim() || null,
subtitle: form.subtitle.trim() || null,
specs: form.specs.trim() || undefined,
rollMeters: form.rollMeters,
isNew: form.isNew,
colorsJson: colorEntries,
imageUrlsJson: imageUrls,
}
const productCode = form.productCode.trim() || `PRD-${Date.now()}`
saving.value = true
try {
if (modalMode.value === 'edit' && editingId.value) {
await productsApi.update(editingId.value, {
companyId: companyIdPayload,
name: form.name.trim(),
color: form.color.trim() || colorEntries[0]?.name || '—',
code: productCode,
productCode,
minMeterage: form.minMeterage,
orderStep: form.orderStep,
price: form.price,
internalStock: form.internalStock,
isActive: form.isActive,
...catalogPayload,
})
const fresh = await productsApi.getById(editingId.value)
const index = productList.value.findIndex((p) => p.id === editingId.value)
if (index !== -1) productList.value[index] = fresh
toast.success('اطلاعات محصول به‌روزرسانی شد.')
} else {
const created = await productsApi.create({
companyId: companyIdPayload,
name: form.name.trim(),
color: form.color.trim() || colorEntries[0]?.name || '—',
code: productCode,
productCode,
minMeterage: form.minMeterage,
orderStep: form.orderStep,
price: form.price,
internalStock: form.internalStock,
isActive: form.isActive,
...catalogPayload,
})
productList.value.push(created)
toast.success('محصول جدید ثبت شد.')
}
closeModal()
} catch (err) {
if (err instanceof ApiError && err.status === 0) {
formError.value = err.message
}
toast.error(err instanceof ApiError ? err.message : 'ذخیره محصول انجام نشد.')
} finally {
saving.value = false
}
}
async function persistActive(id: string, isActive: boolean) {
await productsApi.setActive(id, isActive)
}
async function removeProduct(product: Product) {
const ok = await confirm({
title: 'حذف محصول',
message: `آیا از حذف دائمی «${product.name}» مطمئن هستید؟ این عمل قابل بازگشت نیست.`,
confirmLabel: 'حذف',
cancelLabel: 'انصراف',
tone: 'danger',
})
if (!ok) return
try {
await productsApi.remove(product.id)
productList.value = productList.value.filter((item) => item.id !== product.id)
toast.success('محصول حذف شد.')
} catch (err) {
toast.error(err instanceof ApiError ? err.message : 'حذف محصول انجام نشد.')
}
}
function clearFilters() {
searchQuery.value = ''
companyFilter.value = 'all'
}
</script>
<template>
<div class="products-page">
<div class="admin-page-header">
<div>
<h1 class="admin-page-header__title">محصولات</h1>
</div>
<button type="button" class="admin-btn admin-btn--primary" @click="openAddModal">
افزودن محصول
</button>
</div>
<div class="products-page__filters admin-card">
<div class="products-page__search">
<AdminIcon name="search" :size="18" />
<input
v-model="searchQuery"
type="search"
class="products-page__search-input"
placeholder="جستجو در نام، رنگ، کد..."
/>
</div>
<AdminSelect
v-model="companyFilter"
class="products-page__catalog-filter"
aria-label="فیلتر شرکت"
:options="companyFilterOptions"
/>
</div>
<AdminPageSkeleton v-if="loading" variant="table" />
<BaseEmptyState
v-else-if="!filteredProducts.length"
class="admin-card"
icon="products"
:title="hasActiveFilters ? 'نتیجه‌ای یافت نشد' : 'محصولی ثبت نشده'"
:description="
hasActiveFilters
? 'عبارت جستجو را تغییر دهید یا فیلترها را پاک کنید.'
: 'برای شروع، اولین محصول را ثبت کنید.'
"
>
<button v-if="hasActiveFilters" type="button" class="admin-btn admin-btn--outline admin-btn--sm" @click="clearFilters">
پاک کردن فیلترها
</button>
<button v-else type="button" class="admin-btn admin-btn--primary admin-btn--sm" @click="openAddModal">
افزودن محصول
</button>
</BaseEmptyState>
<div v-else class="products-page__table admin-card desktop-only">
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>تصویر</th>
<th>کد محصول</th>
<th>نام</th>
<th>رنگ</th>
<th>شرکت</th>
<th class="numeric">حداقل متراژ</th>
<th class="numeric">ضریب</th>
<th class="numeric">قیمت هر متر</th>
<th class="numeric">موجودی</th>
<th class="status">وضعیت</th>
<th class="actions">عملیات</th>
</tr>
</thead>
<tbody>
<tr v-for="product in filteredProducts" :key="product.id">
<td>
<span
class="product-thumb"
:class="{ 'product-thumb--empty': !product.imageUrls?.[0] }"
:style="product.imageUrls?.[0] ? { backgroundImage: toCssBackgroundUrl(product.imageUrls[0]) } : undefined"
/>
</td>
<td>{{ product.productCode }}</td>
<td>{{ product.name }}</td>
<td>{{ product.color }}</td>
<td>{{ product.companyName }}</td>
<td class="numeric">{{ formatNumber(product.minMeterage) }} متر</td>
<td class="numeric">{{ formatNumber(product.orderStep) }}</td>
<td class="numeric">
{{ product.price ? `${formatCurrency(product.price)} / متر` : '—' }}
</td>
<td class="numeric">{{ formatNumber(product.internalStock) }} متر</td>
<td class="status">
<ActiveSwitchCell
:id="product.id"
v-model="product.isActive"
:entity-name="product.name"
entity-type="محصول"
:persist="persistActive"
/>
</td>
<td class="actions">
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEditModal(product)">
ویرایش
</button>
<button
type="button"
class="admin-btn admin-btn--outline-danger admin-btn--xs"
@click="removeProduct(product)"
>
حذف
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="!loading && filteredProducts.length" class="compact-only admin-card-list">
<article v-for="product in filteredProducts" :key="product.id" class="admin-card-item">
<div class="admin-card-item__header">
<p class="admin-card-item__title">{{ product.name }}</p>
<div class="admin-card-item__header-actions">
<ActiveSwitchCell
:id="product.id"
v-model="product.isActive"
:entity-name="product.name"
entity-type="محصول"
:persist="persistActive"
/>
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEditModal(product)">
ویرایش
</button>
<button
type="button"
class="admin-btn admin-btn--outline-danger admin-btn--xs"
@click="removeProduct(product)"
>
حذف
</button>
</div>
</div>
<div class="admin-card-item__meta">
<span>{{ product.productCode }}</span>
<span>{{ product.color }}</span>
<span dir="ltr">کد {{ product.productCode }}</span>
<span>{{ product.price ? `${formatCurrency(product.price)} / متر` : 'بدون قیمت' }}</span>
</div>
</article>
</div>
<BaseModal :open="isModalOpen" :title="modalTitle" size="lg" @close="closeModal">
<form class="admin-form" @submit.prevent="submitProduct">
<p v-if="formError" class="admin-form__error" role="alert">{{ formError }}</p>
<div class="admin-form__field">
<label>شرکت مرتبط با محصول</label>
<AdminSelect
v-model="form.companyId"
block
aria-label="انتخاب شرکت مرتبط با محصول"
placeholder="نادمی تکس (پیش‌فرض)"
:options="companyFormOptions"
/>
</div>
<div class="admin-form__row">
<div class="admin-form__field">
<label for="product-name">نام محصول <span class="admin-form__required">*</span></label>
<input id="product-name" v-model="form.name" type="text" class="admin-input" />
</div>
<div class="admin-form__field">
<label for="product-color">رنگ / نوع</label>
<input id="product-color" v-model="form.color" type="text" class="admin-input" />
</div>
</div>
<div class="admin-form__field">
<label for="product-code">کد محصول</label>
<input id="product-code" v-model="form.productCode" type="text" class="admin-input" dir="ltr" />
</div>
<div class="admin-form__row">
<div class="admin-form__field">
<label for="product-min">حداقل متراژ</label>
<input id="product-min" v-model.number="form.minMeterage" type="number" min="1" class="admin-input" />
</div>
<div class="admin-form__field">
<label for="product-step">ضریب سفارش</label>
<input id="product-step" v-model.number="form.orderStep" type="number" min="1" class="admin-input" />
</div>
</div>
<div class="admin-form__row">
<div class="admin-form__field">
<label for="product-price">قیمت هر متر (تومان)</label>
<AdminCurrencyInput id="product-price" v-model="form.price" placeholder="مثلاً ۲۵۰٬۰۰۰" />
</div>
<div class="admin-form__field">
<label for="product-stock">موجودی داخلی (متر)</label>
<input id="product-stock" v-model.number="form.internalStock" type="number" min="0" class="admin-input" />
</div>
</div>
<div class="admin-form__row">
<div class="admin-form__field">
<label for="product-usage">کاربرد</label>
<AdminSelect
id="product-usage"
v-model="form.usageCategory"
block
aria-label="کاربرد محصول"
placeholder="انتخاب کاربرد"
:options="usageOptions"
/>
</div>
<div class="admin-form__field">
<label for="product-season">فصل</label>
<AdminSelect
id="product-season"
v-model="form.season"
block
aria-label="فصل محصول"
placeholder="انتخاب فصل"
:options="seasonOptions"
/>
</div>
</div>
<div class="admin-form__row">
<div class="admin-form__field">
<label for="product-composition">جنس / ترکیب</label>
<input id="product-composition" v-model="form.composition" type="text" class="admin-input" />
</div>
<div class="admin-form__field">
<label for="product-weave">نوع بافت</label>
<input id="product-weave" v-model="form.weaveType" type="text" class="admin-input" />
</div>
</div>
<div class="admin-form__row">
<div class="admin-form__field">
<label for="product-subtitle">زیرعنوان</label>
<input id="product-subtitle" v-model="form.subtitle" type="text" class="admin-input" />
</div>
<div class="admin-form__field">
<label for="product-roll">متراژ طاقه</label>
<input id="product-roll" v-model.number="form.rollMeters" type="number" min="1" class="admin-input" />
</div>
</div>
<div class="admin-form__field">
<label for="product-specs">مشخصات محصول</label>
<p class="admin-form__hint">
متن آزاد؛ در بخش «مشاهده همه مشخصات» صفحه محصول در اپ نمایش داده می‌شود.
</p>
<textarea
id="product-specs"
v-model="form.specs"
class="admin-form__textarea"
rows="5"
placeholder="مثلاً جنس، عرض، وزن و نکات نگهداری — هر خط یک بند"
/>
</div>
<div class="admin-form__field">
<label>تصاویر عمومی محصول</label>
<p class="color-variants__hint">
فقط برای گالری اصلی در اپ کاربر (بدون وابستگی به رنگ). با «ثبت تغییرات» ثبت می‌شود.
</p>
<div class="general-images">
<div
v-for="(url, gIdx) in form.generalImages"
:key="`general-${gIdx}`"
class="product-media-row"
role="group"
:aria-label="`تصویر عمومی ${gIdx + 1}`"
>
<AdminImagePicker
compact
:show-remove="false"
:model-value="url || null"
@update:model-value="form.generalImages[gIdx] = $event ?? ''"
/>
<button
type="button"
class="product-media-row__delete"
aria-label="حذف این ردیف تصویر"
title="حذف این ردیف"
@click="removeGeneralImage(gIdx)"
>
<AdminIcon name="trash" :size="17" />
</button>
</div>
<button type="button" class="admin-btn admin-btn--outline product-media-add" @click="addGeneralImageSlot">
+ افزودن تصویر عمومی
</button>
</div>
</div>
<div class="admin-form__field">
<label>رنگ‌بندی‌ها</label>
<p class="color-variants__hint">
برای هر رنگ یک نام و یک تصویر بگذارید. تصویر رنگ در بخش رنگ‌بندی اپ نمایش داده می‌شود، نه در گالری عمومی.
</p>
<div class="color-variants">
<div
v-for="(row, index) in form.colorRows"
:key="row.id"
class="product-media-row product-media-row--color"
role="group"
:aria-label="`رنگ ${index + 1}`"
>
<AdminImagePicker
compact
:show-remove="false"
:model-value="row.imageUrl || null"
@update:model-value="row.imageUrl = $event ?? ''"
/>
<input
v-model="row.name"
type="text"
class="admin-input product-media-row__input"
:placeholder="`نام رنگ ${index + 1}`"
:aria-label="`نام رنگ ${index + 1}`"
/>
<button
type="button"
class="product-media-row__delete"
aria-label="حذف این رنگ"
title="حذف این رنگ"
@click="removeColorRow(index)"
>
<AdminIcon name="trash" :size="17" />
</button>
</div>
<button type="button" class="admin-btn admin-btn--outline product-media-add" @click="addColorRow">
+ افزودن رنگ
</button>
</div>
</div>
<div class="admin-form__field">
<BaseSwitch v-model="form.isNew" label="محصول جدید" />
</div>
<div class="admin-form__field">
<BaseSwitch v-model="form.isActive" label="وضعیت" />
</div>
</form>
<template #footer>
<button type="button" class="admin-btn admin-btn--outline" @click="closeModal">انصراف</button>
<button type="button" class="admin-btn admin-btn--primary" :disabled="!isFormValid || saving" @click="submitProduct">
{{ modalMode === 'edit' ? 'ثبت تغییرات' : 'ثبت محصول' }}
</button>
</template>
</BaseModal>
</div>
</template>
<style scoped>
.products-page__filters {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 12px 14px;
margin-bottom: 16px;
align-items: center;
}
.products-page__search {
flex: 1;
min-width: 200px;
display: flex;
align-items: center;
gap: 10px;
color: var(--color-text-muted);
}
.products-page__search-input {
flex: 1;
border: none;
background: transparent;
font-size: 0.875rem;
outline: none;
}
.products-page__catalog-filter {
width: min(260px, 100%);
}
.admin-form__hint {
margin: 6px 0 0;
font-size: 0.75rem;
line-height: 1.5;
color: var(--color-text-muted);
}
.products-page__table {
overflow: hidden;
}
.product-thumb {
display: block;
width: 44px;
height: 36px;
border-radius: 8px;
background: #e8eceb center / cover no-repeat;
}
.product-thumb--empty {
background: #eef1f0;
}
.color-variants__hint {
margin: 0 0 10px;
color: var(--color-text-muted);
font-size: 0.75rem;
line-height: 1.6;
}
.general-images,
.color-variants {
display: flex;
flex-direction: column;
gap: 10px;
}
.product-media-row {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 12px 14px;
padding: 12px 14px;
border: 1px solid var(--color-border);
border-radius: 14px;
background: var(--color-elevated, #fff);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.product-media-row--color {
grid-template-columns: auto 1fr auto;
align-items: center;
}
.product-media-row :deep(.image-picker) {
min-width: 0;
}
.product-media-row__input {
min-width: 0;
height: 44px;
}
.product-media-row__delete {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
border: 1px solid transparent;
border-radius: 12px;
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
transition:
color 0.15s ease,
background 0.15s ease,
border-color 0.15s ease;
}
.product-media-row__delete:hover {
color: #c53030;
background: rgba(197, 48, 48, 0.08);
border-color: rgba(197, 48, 48, 0.18);
}
.product-media-row__delete:focus-visible {
outline: 2px solid var(--color-primary, #2d6a4f);
outline-offset: 2px;
}
.product-media-add {
align-self: stretch;
justify-content: center;
}
@media (max-width: 520px) {
.product-media-row--color {
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
}
.product-media-row--color :deep(.image-picker) {
grid-column: 1 / -1;
}
.product-media-row--color .product-media-row__delete {
grid-row: 2;
grid-column: 2;
}
.product-media-row--color .product-media-row__input {
grid-row: 2;
grid-column: 1;
}
}
</style>