feat: 8K Natural Lifestyle Photography Engine, 3-column value strip, smart cutout alpha matting, and full BN/EN localization
This commit is contained in:
+3
-6
@@ -7,12 +7,9 @@ RUN rm -rf /usr/share/nginx/html/*
|
||||
# Copy custom nginx configuration
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Copy all web assets
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
COPY style.css /usr/share/nginx/html/
|
||||
COPY app.js /usr/share/nginx/html/
|
||||
COPY manifest.json /usr/share/nginx/html/
|
||||
COPY sw.js /usr/share/nginx/html/
|
||||
# Copy all web assets, catalogs, and images
|
||||
COPY . /usr/share/nginx/html/
|
||||
RUN rm -f /usr/share/nginx/html/Dockerfile /usr/share/nginx/html/docker-compose.yml /usr/share/nginx/html/nginx.conf
|
||||
|
||||
# Expose port 80 for Coolify Traefik reverse proxy
|
||||
EXPOSE 80
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 619 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 559 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 882 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 653 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+584
-161
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,22 @@
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const urlModule = require('url');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PORT = 8080;
|
||||
const DIRECTORY = __dirname;
|
||||
const DATA_DIR = path.join(DIRECTORY, 'data');
|
||||
const SYNCED_STORES_FILE = path.join(DATA_DIR, 'synced_stores.json');
|
||||
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(SYNCED_STORES_FILE)) {
|
||||
fs.writeFileSync(SYNCED_STORES_FILE, JSON.stringify([], null, 2));
|
||||
}
|
||||
|
||||
const MIME_TYPES = {
|
||||
'.html': 'text/html; charset=UTF-8',
|
||||
@@ -30,10 +42,437 @@ function getLocalIp() {
|
||||
return '127.0.0.1';
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// Utility: Fetch URL with Redirects & Browser User-Agent
|
||||
function fetchUrl(targetUrl, maxRedirects = 5) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (maxRedirects <= 0) return reject(new Error('Too many redirects'));
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(targetUrl);
|
||||
} catch (e) {
|
||||
return reject(new Error(`Invalid URL: ${targetUrl}`));
|
||||
}
|
||||
|
||||
const client = parsed.protocol === 'https:' ? https : http;
|
||||
const req = client.get(targetUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,application/json,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.9,bn;q=0.8'
|
||||
},
|
||||
timeout: 10000
|
||||
}, (res) => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
const nextUrl = new URL(res.headers.location, targetUrl).href;
|
||||
return fetchUrl(nextUrl, maxRedirects - 1).then(resolve).catch(reject);
|
||||
}
|
||||
|
||||
let data = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', chunk => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
data: data,
|
||||
finalUrl: targetUrl
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request timed out'));
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: Convert English digits to Bengali format for prices
|
||||
function toBengaliPrice(num) {
|
||||
const bnDigits = ['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯'];
|
||||
const formattedNum = Math.round(num).toLocaleString('en-US');
|
||||
const bn = formattedNum.replace(/\d/g, d => bnDigits[d]);
|
||||
return `৳ ${bn}`;
|
||||
}
|
||||
|
||||
// Helper: Recursively search nested JSON / state / props for product items
|
||||
function findProductsInObject(obj, results, storeName, origin, category, storeId, depth = 0) {
|
||||
if (!obj || depth > 8 || results.length >= 120) return;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) {
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
const title = item.title || item.name || item.product_name || item.productTitle || '';
|
||||
const price = item.price || item.sale_price || item.regular_price || item.unit_price || (item.variants && item.variants[0] ? item.variants[0].price : 0);
|
||||
const image = item.image || item.thumbnail || item.featured_image || item.photo || (item.images && item.images[0] ? (item.images[0].src || item.images[0]) : '') || '';
|
||||
|
||||
if (title && typeof title === 'string' && title.length > 3 && (price || image)) {
|
||||
const numPrice = parseFloat(String(price).replace(/[^0-9.]/g, '')) || 1200;
|
||||
let imgUrl = typeof image === 'string' ? image : (image.url || image.src || '');
|
||||
if (imgUrl && !imgUrl.startsWith('http') && !imgUrl.startsWith('data:')) {
|
||||
try { imgUrl = new URL(imgUrl, origin).href; } catch(e) {}
|
||||
}
|
||||
|
||||
results.push({
|
||||
id: item.id ? String(item.id) : (item._id ? String(item._id) : crypto.randomUUID()),
|
||||
title: title.trim(),
|
||||
brand: item.brand?.name || item.brand || item.vendor || storeName,
|
||||
price: toBengaliPrice(numPrice),
|
||||
rawPrice: numPrice,
|
||||
regularPrice: toBengaliPrice(numPrice * 1.25),
|
||||
image: imgUrl,
|
||||
link: item.url || (item.handle ? `${origin}/products/${item.handle}` : origin),
|
||||
category: category,
|
||||
storeName: storeName,
|
||||
storeId: storeId
|
||||
});
|
||||
} else {
|
||||
findProductsInObject(item, results, storeName, origin, category, storeId, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
for (const key of Object.keys(obj)) {
|
||||
findProductsInObject(obj[key], results, storeName, origin, category, storeId, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-Strategy Real Store Scraper & Product Extractor
|
||||
async function scrapeStore(rawUrl, customStoreName = '', customCategory = 'general') {
|
||||
let targetUrl = rawUrl.trim();
|
||||
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
|
||||
targetUrl = 'https://' + targetUrl;
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(targetUrl);
|
||||
const origin = parsedUrl.origin;
|
||||
const hostname = parsedUrl.hostname.replace(/^www\./, '');
|
||||
|
||||
// Clean default store name from domain
|
||||
const rawBrandFromDomain = hostname.split('.')[0];
|
||||
const autoStoreName = customStoreName || (rawBrandFromDomain.charAt(0).toUpperCase() + rawBrandFromDomain.slice(1));
|
||||
const storeId = 'store_' + hostname.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
|
||||
|
||||
let products = [];
|
||||
let detectedTitle = autoStoreName;
|
||||
|
||||
// STRATEGY 1: Check Shopify products.json API (extremely fast and structured)
|
||||
try {
|
||||
const shopifyUrl = `${origin}/products.json?limit=250`;
|
||||
const shopifyRes = await fetchUrl(shopifyUrl);
|
||||
if (shopifyRes.statusCode === 200) {
|
||||
const shopifyData = JSON.parse(shopifyRes.data);
|
||||
if (shopifyData && Array.isArray(shopifyData.products) && shopifyData.products.length > 0) {
|
||||
shopifyData.products.forEach(p => {
|
||||
const variant = p.variants && p.variants[0] ? p.variants[0] : {};
|
||||
const priceVal = parseFloat(variant.price) || 1200;
|
||||
const regPriceVal = parseFloat(variant.compare_at_price) || (priceVal * 1.22);
|
||||
const imgUrl = (p.images && p.images[0] && p.images[0].src) ? p.images[0].src : ((p.image && p.image.src) ? p.image.src : '');
|
||||
|
||||
products.push({
|
||||
id: p.id ? String(p.id) : crypto.randomUUID(),
|
||||
title: p.title || 'Product',
|
||||
brand: p.vendor || autoStoreName,
|
||||
price: toBengaliPrice(priceVal),
|
||||
rawPrice: priceVal,
|
||||
regularPrice: toBengaliPrice(regPriceVal),
|
||||
image: imgUrl,
|
||||
link: `${origin}/products/${p.handle || p.id}`,
|
||||
category: customCategory,
|
||||
storeName: autoStoreName,
|
||||
storeId: storeId
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Non-shopify or shopify API restricted, continue to HTML scraper
|
||||
}
|
||||
|
||||
// STRATEGY 2: If products not found via Shopify API, fetch HTML and parse Schema.org JSON-LD & DOM
|
||||
if (products.length === 0) {
|
||||
const mainRes = await fetchUrl(targetUrl);
|
||||
if (mainRes.statusCode !== 200) {
|
||||
throw new Error(`Failed to fetch webpage (HTTP ${mainRes.statusCode})`);
|
||||
}
|
||||
|
||||
const html = mainRes.data;
|
||||
|
||||
// Detect Page Title
|
||||
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
||||
if (titleMatch && titleMatch[1]) {
|
||||
const rawTitle = titleMatch[1].trim();
|
||||
detectedTitle = customStoreName || rawTitle.split(/[|\-–—]/)[0].trim() || autoStoreName;
|
||||
}
|
||||
|
||||
// 2A: Next.js __NEXT_DATA__ & Nuxt JSON State Extractor
|
||||
const nextDataMatch = html.match(/<script[^>]+id=["']__NEXT_DATA__["'][^>]*>([\s\S]*?)<\/script>/i) ||
|
||||
html.match(/<script[^>]*>window\.__INITIAL_STATE__\s*=\s*(\{[\s\S]*?\});<\/script>/i);
|
||||
if (nextDataMatch && nextDataMatch[1]) {
|
||||
try {
|
||||
const stateJson = JSON.parse(nextDataMatch[1]);
|
||||
findProductsInObject(stateJson, products, autoStoreName, origin, customCategory, storeId);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 2B: Schema.org JSON-LD Parsing
|
||||
const jsonLdMatches = html.matchAll(/<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi);
|
||||
for (const match of jsonLdMatches) {
|
||||
try {
|
||||
const parsed = JSON.parse(match[1]);
|
||||
const items = Array.isArray(parsed) ? parsed : (parsed['@graph'] || [parsed]);
|
||||
|
||||
for (const item of items) {
|
||||
if (item['@type'] === 'Product') {
|
||||
const priceVal = item.offers ? (parseFloat(item.offers.price || item.offers.lowPrice) || 1500) : 1500;
|
||||
const regPriceVal = priceVal * 1.25;
|
||||
const imgUrl = Array.isArray(item.image) ? item.image[0] : (item.image?.url || item.image || '');
|
||||
|
||||
products.push({
|
||||
id: item.sku || crypto.randomUUID(),
|
||||
title: item.name || 'Product',
|
||||
brand: item.brand?.name || item.brand || autoStoreName,
|
||||
price: toBengaliPrice(priceVal),
|
||||
rawPrice: priceVal,
|
||||
regularPrice: toBengaliPrice(regPriceVal),
|
||||
image: typeof imgUrl === 'string' ? imgUrl : '',
|
||||
link: item.url || targetUrl,
|
||||
category: customCategory,
|
||||
storeName: autoStoreName,
|
||||
storeId: storeId
|
||||
});
|
||||
} else if (item['@type'] === 'ItemList' && Array.isArray(item.itemListElement)) {
|
||||
for (const elem of item.itemListElement) {
|
||||
const p = elem.item || elem;
|
||||
if (p.name) {
|
||||
const priceVal = p.offers ? (parseFloat(p.offers.price) || 1200) : 1200;
|
||||
products.push({
|
||||
id: crypto.randomUUID(),
|
||||
title: p.name,
|
||||
brand: p.brand?.name || autoStoreName,
|
||||
price: toBengaliPrice(priceVal),
|
||||
rawPrice: priceVal,
|
||||
regularPrice: toBengaliPrice(priceVal * 1.25),
|
||||
image: p.image || '',
|
||||
link: p.url || targetUrl,
|
||||
category: customCategory,
|
||||
storeName: autoStoreName,
|
||||
storeId: storeId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Skip malformed JSON-LD block
|
||||
}
|
||||
}
|
||||
|
||||
// 2B: HTML Semantic Product Card Regex Parser (if JSON-LD had < 2 products)
|
||||
if (products.length < 2) {
|
||||
// Find product blocks
|
||||
const cardRegex = /<(div|article|li)[^>]*class=["'][^"']*(product|item|grid|card)[^"']*["'][^>]*>([\s\S]*?)<\/\1>/gi;
|
||||
let cardMatch;
|
||||
let count = 0;
|
||||
|
||||
while ((cardMatch = cardRegex.exec(html)) !== null && count < 60) {
|
||||
const block = cardMatch[3];
|
||||
|
||||
// Extract title
|
||||
const titleTag = block.match(/<(h2|h3|h4|h5|a)[^>]*class=["'][^"']*(title|name|heading)[^"']*["'][^>]*>([^<]+)<\/\1>/i) ||
|
||||
block.match(/<(h2|h3|h4|h5)[^>]*>([^<]+)<\/\1>/i) ||
|
||||
block.match(/<a[^>]*title=["']([^"']+)["']/i);
|
||||
const title = titleTag ? (titleTag[3] || titleTag[2] || titleTag[1]).trim() : '';
|
||||
|
||||
// Extract image
|
||||
const imgTag = block.match(/<img[^>]+(?:src|data-src|data-lazy-src)=["']([^"']+)["'][^>]*>/i);
|
||||
let imgUrl = imgTag ? imgTag[1] : '';
|
||||
if (imgUrl && !imgUrl.startsWith('http') && !imgUrl.startsWith('data:')) {
|
||||
imgUrl = new URL(imgUrl, origin).href;
|
||||
}
|
||||
|
||||
// Extract link
|
||||
const linkTag = block.match(/<a[^>]+href=["']([^"']+)["']/i);
|
||||
let linkUrl = linkTag ? linkTag[1] : targetUrl;
|
||||
if (linkUrl && !linkUrl.startsWith('http')) {
|
||||
linkUrl = new URL(linkUrl, origin).href;
|
||||
}
|
||||
|
||||
// Extract price
|
||||
const priceTag = block.match(/(?:৳|Tk\.?|BDT|\$)\s*([\d,]+(?:\.\d+)?)/i) ||
|
||||
block.match(/([\d,]+)\s*(?:৳|Tk\.?|BDT)/i);
|
||||
let priceVal = 1450;
|
||||
if (priceTag) {
|
||||
const rawNum = parseFloat((priceTag[1] || priceTag[2] || '').replace(/,/g, ''));
|
||||
if (rawNum && rawNum > 0) priceVal = rawNum;
|
||||
}
|
||||
|
||||
if (title && title.length > 2 && (imgUrl || block.includes('price'))) {
|
||||
count++;
|
||||
products.push({
|
||||
id: crypto.randomUUID(),
|
||||
title: title.replace(/&/g, '&').replace(/'/g, "'"),
|
||||
brand: autoStoreName,
|
||||
price: toBengaliPrice(priceVal),
|
||||
rawPrice: priceVal,
|
||||
regularPrice: toBengaliPrice(priceVal * 1.25),
|
||||
image: imgUrl,
|
||||
link: linkUrl,
|
||||
category: customCategory,
|
||||
storeName: autoStoreName,
|
||||
storeId: storeId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate products by title
|
||||
const seenTitles = new Set();
|
||||
products = products.filter(p => {
|
||||
const key = p.title.toLowerCase().trim();
|
||||
if (seenTitles.has(key)) return false;
|
||||
seenTitles.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
const finalStore = {
|
||||
id: storeId,
|
||||
name: autoStoreName,
|
||||
detectedTitle: detectedTitle,
|
||||
domain: hostname,
|
||||
url: targetUrl,
|
||||
category: customCategory,
|
||||
syncedAt: new Date().toISOString(),
|
||||
count: products.length,
|
||||
products: products
|
||||
};
|
||||
|
||||
// Save to persistent storage
|
||||
let savedStores = [];
|
||||
try {
|
||||
if (fs.existsSync(SYNCED_STORES_FILE)) {
|
||||
savedStores = JSON.parse(fs.readFileSync(SYNCED_STORES_FILE, 'utf8'));
|
||||
}
|
||||
} catch (e) {
|
||||
savedStores = [];
|
||||
}
|
||||
|
||||
// Upsert store
|
||||
const existingIdx = savedStores.findIndex(s => s.id === storeId || s.domain === hostname);
|
||||
if (existingIdx >= 0) {
|
||||
savedStores[existingIdx] = finalStore;
|
||||
} else {
|
||||
savedStores.push(finalStore);
|
||||
}
|
||||
|
||||
fs.writeFileSync(SYNCED_STORES_FILE, JSON.stringify(savedStores, null, 2), 'utf8');
|
||||
|
||||
return finalStore;
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
let reqPath = req.url.split('?')[0];
|
||||
if (reqPath === '/' || reqPath === '') reqPath = '/index.html';
|
||||
|
||||
// API: Save Asset / Product Image
|
||||
if (req.method === 'POST' && reqPath === '/api/save-asset') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { filename, base64Data } = JSON.parse(body);
|
||||
const buffer = Buffer.from(base64Data.replace(/^data:image\/\w+;base64,/, ''), 'base64');
|
||||
const savePath = path.join(DIRECTORY, 'assets', 'products', filename);
|
||||
fs.writeFileSync(savePath, buffer);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||
res.end(JSON.stringify({ success: true, savedTo: savePath }));
|
||||
} catch (e) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||
res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Universal Store URL Sync Endpoint
|
||||
if (req.method === 'POST' && reqPath === '/api/sync-store') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const { url, storeName, category } = JSON.parse(body || '{}');
|
||||
if (!url) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||
return res.end(JSON.stringify({ error: 'Store URL is required' }));
|
||||
}
|
||||
|
||||
const storeResult = await scrapeStore(url, storeName, category);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||
res.end(JSON.stringify({ success: true, store: storeResult }));
|
||||
} catch (e) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||
res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Get All Synced Stores
|
||||
if (req.method === 'GET' && reqPath === '/api/get-synced-stores') {
|
||||
try {
|
||||
let savedStores = [];
|
||||
if (fs.existsSync(SYNCED_STORES_FILE)) {
|
||||
savedStores = JSON.parse(fs.readFileSync(SYNCED_STORES_FILE, 'utf8'));
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||
res.end(JSON.stringify({ success: true, stores: savedStores }));
|
||||
} catch (e) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||
res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Delete Synced Store
|
||||
if (req.method === 'POST' && reqPath === '/api/delete-synced-store') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { storeId } = JSON.parse(body || '{}');
|
||||
let savedStores = [];
|
||||
if (fs.existsSync(SYNCED_STORES_FILE)) {
|
||||
savedStores = JSON.parse(fs.readFileSync(SYNCED_STORES_FILE, 'utf8'));
|
||||
}
|
||||
savedStores = savedStores.filter(s => s.id !== storeId);
|
||||
fs.writeFileSync(SYNCED_STORES_FILE, JSON.stringify(savedStores, null, 2), 'utf8');
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||
res.end(JSON.stringify({ success: true, stores: savedStores }));
|
||||
} catch (e) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||
res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// CORS Preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type'
|
||||
});
|
||||
return res.end();
|
||||
}
|
||||
|
||||
const filePath = path.join(DIRECTORY, reqPath);
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
|
||||
@@ -513,6 +513,8 @@ body {
|
||||
/* Canvas Wrapper */
|
||||
.canvas-wrapper {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
@@ -522,17 +524,57 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.canvas-wrapper.aspect-reels {
|
||||
aspect-ratio: 9/16;
|
||||
max-height: 520px;
|
||||
max-width: 320px;
|
||||
max-height: 570px;
|
||||
}
|
||||
|
||||
.canvas-wrapper canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Trending Poster Chips */
|
||||
.trending-chips-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
padding: 4px 2px 8px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.trending-chips-bar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.trend-chip {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: var(--radius-full);
|
||||
padding: 6px 12px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.trend-chip:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-primary);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.trend-chip.active {
|
||||
background: linear-gradient(135deg, rgba(139, 92, 246, 0.35), rgba(59, 130, 246, 0.35));
|
||||
border-color: var(--primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* Result Card */
|
||||
@@ -1088,3 +1130,655 @@ body {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* --- 16+ Distinct 3D Stage Theme Selector Grid --- */
|
||||
.theme-selector-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(135px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 14px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.theme-chip {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 10px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.theme-chip:hover {
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.theme-chip.active {
|
||||
background: linear-gradient(145deg, rgba(229, 184, 58, 0.15), rgba(139, 92, 246, 0.18));
|
||||
border-color: #F59E0B;
|
||||
box-shadow: 0 0 16px rgba(245, 158, 11, 0.35);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.theme-chip .icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.theme-chip .name {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #F1F5F9;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.theme-chip .tag {
|
||||
font-size: 8.5px;
|
||||
font-weight: 700;
|
||||
color: #94A3B8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* --- 3D CGI Master Prompt Studio & Library --- */
|
||||
.cgi-library-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.cgi-card {
|
||||
background: linear-gradient(145deg, #111827 0%, #0B0F17 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.cgi-card:hover {
|
||||
border-color: #8B5CF6;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 30px rgba(139, 92, 246, 0.25);
|
||||
}
|
||||
|
||||
.cgi-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cgi-card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.cgi-card-category {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
padding: 2px 8px;
|
||||
border-radius: 99px;
|
||||
background: rgba(139, 92, 246, 0.18);
|
||||
color: #C084FC;
|
||||
border: 1px solid rgba(139, 92, 246, 0.4);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.cgi-prompt-content {
|
||||
background: #06090E;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
font-size: 11.5px;
|
||||
color: #38BDF8;
|
||||
line-height: 1.5;
|
||||
margin: 10px 0;
|
||||
max-height: 140px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cgi-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-copy-prompt {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #3B82F6, #1D4ED8);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-copy-prompt:hover {
|
||||
background: linear-gradient(135deg, #60A5FA, #2563EB);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-apply-poster {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #FBBF24;
|
||||
border: 1px solid rgba(245, 158, 11, 0.35);
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-apply-poster:hover {
|
||||
background: rgba(245, 158, 11, 0.3);
|
||||
border-color: #F59E0B;
|
||||
}
|
||||
|
||||
/* --- Live Autocomplete Dropdown --- */
|
||||
.autocomplete-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #0D131F;
|
||||
border: 1px solid rgba(139, 92, 246, 0.4);
|
||||
border-radius: var(--radius-md);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(16px);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.autocomplete-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.autocomplete-item:hover, .autocomplete-item.highlighted {
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
.autocomplete-thumb {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
background: #1E293B;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.autocomplete-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.autocomplete-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.autocomplete-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.autocomplete-brand {
|
||||
color: #C084FC;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.autocomplete-price {
|
||||
color: #10B981;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.autocomplete-copy-btn {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: #CBD5E1;
|
||||
padding: 5px 9px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.autocomplete-copy-btn:hover {
|
||||
background: var(--primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* --- Catalog Browser Modal Grid --- */
|
||||
.catalog-modal-content {
|
||||
max-width: 900px;
|
||||
width: 94%;
|
||||
max-height: 88vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.catalog-search-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.catalog-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(88vh - 220px);
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.catalog-item-card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.catalog-item-card:hover {
|
||||
border-color: var(--primary);
|
||||
background: rgba(139, 92, 246, 0.06);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.catalog-item-img {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-fit: contain;
|
||||
background: #0B0F17;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.catalog-item-title {
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.catalog-item-price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.catalog-item-price {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.catalog-item-reg-price {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.catalog-item-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-catalog-copy {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: #E2E8F0;
|
||||
padding: 7px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.btn-catalog-copy:hover {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-catalog-use {
|
||||
flex: 1.2;
|
||||
background: linear-gradient(135deg, #8B5CF6, #6366F1);
|
||||
border: none;
|
||||
color: #FFFFFF;
|
||||
padding: 7px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.btn-catalog-use:hover {
|
||||
background: linear-gradient(135deg, #A78BFA, #4F46E5);
|
||||
}
|
||||
|
||||
/* --- Multi-Store Switcher & Client URL Sync Panel --- */
|
||||
.store-switcher-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
padding: 4px 0 10px 0;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.store-chip-btn {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: #CBD5E1;
|
||||
padding: 6px 14px;
|
||||
border-radius: 99px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.store-chip-btn:hover {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
border-color: #8B5CF6;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.store-chip-btn.active {
|
||||
background: linear-gradient(135deg, #8B5CF6, #6366F1);
|
||||
border-color: #A78BFA;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 0 14px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
.store-chip-btn.add-store-btn {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border: 1px dashed rgba(16, 185, 129, 0.4);
|
||||
color: #34D399;
|
||||
}
|
||||
|
||||
.store-chip-btn.add-store-btn:hover {
|
||||
background: rgba(16, 185, 129, 0.25);
|
||||
border-color: #10B981;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.store-sync-panel {
|
||||
background: linear-gradient(145deg, rgba(17, 24, 39, 0.95), rgba(15, 23, 42, 0.95));
|
||||
border: 1px solid rgba(139, 92, 246, 0.35);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 14px;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
|
||||
animation: slideDown 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.store-info-banner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.store-info-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #E2E8F0;
|
||||
}
|
||||
|
||||
.store-badge-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
padding: 2px 7px;
|
||||
border-radius: 99px;
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
color: #C084FC;
|
||||
border: 1px solid rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-delete-store {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
color: #F87171;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.btn-delete-store:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* --- Multi-Product Grid Poster Engine Styles --- */
|
||||
.poster-mode-switcher {
|
||||
display: flex;
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 4px;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.poster-mode-btn {
|
||||
flex: 1;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #94A3B8;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.poster-mode-btn:hover {
|
||||
color: #FFFFFF;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.poster-mode-btn.active {
|
||||
background: linear-gradient(135deg, #8B5CF6, #6366F1);
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 4px 14px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
.multi-product-slots-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.multi-product-slots-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.product-slot-card {
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.product-slot-card:hover {
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
}
|
||||
|
||||
.slot-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.slot-title {
|
||||
font-size: 11.5px;
|
||||
font-weight: 800;
|
||||
color: #C084FC;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.slot-thumb-container {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
background: #020617;
|
||||
border: 1px dashed rgba(255, 255, 255, 0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slot-thumb-container img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.slot-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slot-upload-label {
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
color: #94A3B8;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.slot-upload-label:hover {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.slot-inputs-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 1fr;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.slot-input {
|
||||
width: 100%;
|
||||
background: rgba(2, 6, 23, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.slot-input:focus {
|
||||
border-color: #8B5CF6;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user