Add domain preview modal and manager
Introduces a domain preview modal to the UI, allowing users to preview domain rules for each service. Adds DomainPreviewManager.js to handle fetching and displaying domain data from YAML files. Updates ServicesManager.js to support preview actions and UI, and enhances main.css with modal and preview button styles.
This commit is contained in:
1 parent
6193723a7f
commit
45101cc497
5 files changed
+949
-1
No files matched your search
+44
@@ -223,6 +223,50 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Domain Preview Modal -->
|
||||
<div id="domain-preview-modal" class="modal-overlay" aria-hidden="true">
|
||||
<div
|
||||
class="modal-content"
|
||||
role="dialog"
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-description"
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h3 id="modal-title">Domain Preview</h3>
|
||||
<button class="modal-close" aria-label="Close domain preview">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="service-info-header">
|
||||
<div class="service-icon">
|
||||
<i class="fas fa-globe"></i>
|
||||
</div>
|
||||
<div class="service-details">
|
||||
<h4 id="modal-service-name">Service Name</h4>
|
||||
<p id="modal-description">Domain rules for this service</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="domains-container">
|
||||
<div class="loading-spinner" id="modal-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<p>Loading domain list...</p>
|
||||
</div>
|
||||
<div
|
||||
class="domains-list"
|
||||
id="domains-list"
|
||||
style="display: none"
|
||||
></div>
|
||||
<div class="modal-error" id="modal-error" style="display: none">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<h4>Failed to Load Domains</h4>
|
||||
<p id="error-message">Unable to fetch domain information.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Information Section -->
|
||||
<aside class="info-banner card" aria-labelledby="info-heading">
|
||||
<div class="card-header">
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* @license GPL-3.0
|
||||
*/
|
||||
|
||||
import { DomainPreviewManager } from "./components/DomainPreviewManager.js";
|
||||
import { OutputManager } from "./components/OutputManager.js";
|
||||
import { ServiceConfigGenerator } from "./components/ServiceConfigGenerator.js";
|
||||
import { ServicesManager } from "./components/ServicesManager.js";
|
||||
@@ -64,6 +65,9 @@ export class MetaJSCustomizer {
|
||||
// Initialize service config generator (no dependencies)
|
||||
this.components.serviceConfigGenerator = new ServiceConfigGenerator();
|
||||
|
||||
// Initialize domain preview manager (no dependencies)
|
||||
this.components.domainPreviewManager = new DomainPreviewManager();
|
||||
|
||||
// Initialize services manager (depends on API)
|
||||
this.components.servicesManager = new ServicesManager();
|
||||
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* Domain Preview Manager Com this.elements = {
|
||||
modal: document.getElementById("domain-preview-modal"),
|
||||
modalTitle: document.getElementById("modal-title"),
|
||||
modalServiceName: document.getElementById("modal-service-name"),
|
||||
modalDescription: document.getElementById("modal-description"),
|
||||
modalLoading: document.getElementById("modal-loading"),
|
||||
domainsList: document.getElementById("domains-list"),
|
||||
modalError: document.getElementById("modal-error"),
|
||||
errorMessage: document.getElementById("error-message"),
|
||||
modalClose: document.querySelector(".modal-close")
|
||||
};Handles domain preview functionality including:
|
||||
* - Fetching YAML files from GitHub repository
|
||||
* - Parsing payload arrays from YAML content
|
||||
* - Displaying domain lists in modal popup
|
||||
* - Organizing domains by type (DOMAIN, DOMAIN-SUFFIX, DOMAIN-KEYWORD)
|
||||
*
|
||||
* @author xixu-me
|
||||
* @license GPL-3.0
|
||||
*/
|
||||
|
||||
export class DomainPreviewManager {
|
||||
constructor() {
|
||||
this.owner = "xixu-me";
|
||||
this.repo = "RFM";
|
||||
this.branch = "universal";
|
||||
this.baseUrl = `https://raw.githubusercontent.com/${this.owner}/${this.repo}/${this.branch}/yaml`;
|
||||
|
||||
// Wait for DOM to be ready before initializing elements
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () =>
|
||||
this.initializeElements()
|
||||
);
|
||||
} else {
|
||||
this.initializeElements();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize DOM elements after DOM is ready
|
||||
*/
|
||||
initializeElements() {
|
||||
this.elements = {
|
||||
modal: document.getElementById("domain-preview-modal"),
|
||||
modalTitle: document.getElementById("modal-title"),
|
||||
modalServiceName: document.getElementById("modal-service-name"),
|
||||
modalDescription: document.getElementById("modal-description"),
|
||||
modalLoading: document.getElementById("modal-loading"),
|
||||
domainStats: document.getElementById("domain-stats"),
|
||||
domainsList: document.getElementById("domains-list"),
|
||||
modalError: document.getElementById("modal-error"),
|
||||
errorMessage: document.getElementById("error-message"),
|
||||
totalDomains: document.getElementById("total-domains"),
|
||||
domainSuffixes: document.getElementById("domain-suffixes"),
|
||||
keywords: document.getElementById("keywords"),
|
||||
modalClose: document.querySelector(".modal-close"),
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the domain preview manager
|
||||
*/
|
||||
init() {
|
||||
if (!this.elements.modal) {
|
||||
console.error("Domain preview modal not found in DOM");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Close modal events
|
||||
this.elements.modalClose.addEventListener("click", () => this.closeModal());
|
||||
this.elements.modal.addEventListener("click", (e) => {
|
||||
if (e.target === this.elements.modal) {
|
||||
this.closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard events
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && this.isModalOpen()) {
|
||||
this.closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for preview requests
|
||||
document.addEventListener("domainPreviewRequest", (event) => {
|
||||
this.showPreview(event.detail.serviceName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show domain preview for a service
|
||||
* @param {string} serviceName - Name of the service
|
||||
*/
|
||||
async showPreview(serviceName) {
|
||||
this.openModal();
|
||||
this.showLoading(serviceName);
|
||||
|
||||
try {
|
||||
const domainData = await this.fetchDomainData(serviceName);
|
||||
this.displayDomainData(serviceName, domainData);
|
||||
} catch (error) {
|
||||
console.error("Failed to load domain data:", error);
|
||||
this.showError("Failed to load domain information for this service.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch domain data from YAML file
|
||||
* @param {string} serviceName - Name of the service
|
||||
* @returns {Promise<Object>} Parsed domain data
|
||||
*/
|
||||
async fetchDomainData(serviceName) {
|
||||
const yamlUrl = `${this.baseUrl}/${serviceName}.yaml`;
|
||||
|
||||
try {
|
||||
const response = await fetch(yamlUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const yamlText = await response.text();
|
||||
return this.parseYamlDomains(yamlText);
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to fetch domain data: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse YAML content to extract domain information
|
||||
* @param {string} yamlText - Raw YAML content
|
||||
* @returns {Object} Parsed domain data organized by type
|
||||
*/
|
||||
parseYamlDomains(yamlText) {
|
||||
const domainData = {
|
||||
domains: [],
|
||||
domainSuffixes: [],
|
||||
keywords: [],
|
||||
total: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
// Simple YAML parsing focused on payload section
|
||||
const lines = yamlText.split("\n");
|
||||
let inPayloadSection = false;
|
||||
let currentIndentation = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Skip comments and empty lines
|
||||
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we're entering the payload section
|
||||
if (trimmedLine === "payload:" || trimmedLine.startsWith("payload:")) {
|
||||
inPayloadSection = true;
|
||||
currentIndentation = line.indexOf(trimmedLine.split(":")[0]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're in payload section, look for list items
|
||||
if (inPayloadSection) {
|
||||
// Check for array items (lines starting with - after spaces)
|
||||
const match = line.match(/^(\s*)-\s*(.+)$/);
|
||||
if (match) {
|
||||
const domain = match[2].trim();
|
||||
|
||||
// Remove quotes if present
|
||||
const cleanDomain = domain.replace(/^["']|["']$/g, "");
|
||||
|
||||
if (cleanDomain) {
|
||||
// Determine domain type based on prefix or content
|
||||
if (cleanDomain.startsWith("+.")) {
|
||||
// Domain suffix (e.g., "+.example.com")
|
||||
domainData.domainSuffixes.push(cleanDomain);
|
||||
} else if (
|
||||
cleanDomain.includes("KEYWORD") ||
|
||||
cleanDomain.toLowerCase().includes("keyword") ||
|
||||
cleanDomain.includes("*")
|
||||
) {
|
||||
// Keywords or wildcard patterns
|
||||
domainData.keywords.push(cleanDomain);
|
||||
} else {
|
||||
// Regular domain
|
||||
domainData.domains.push(cleanDomain);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if we've exited the payload section
|
||||
else if (
|
||||
trimmedLine &&
|
||||
!trimmedLine.startsWith("-") &&
|
||||
line.search(/\S/) <= currentIndentation &&
|
||||
trimmedLine !== "payload:"
|
||||
) {
|
||||
inPayloadSection = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
domainData.total =
|
||||
domainData.domains.length +
|
||||
domainData.domainSuffixes.length +
|
||||
domainData.keywords.length;
|
||||
|
||||
// If no domains found, try a more aggressive approach
|
||||
if (domainData.total === 0) {
|
||||
this.parseYamlFallback(yamlText, domainData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing YAML:", error);
|
||||
// Try fallback parsing method
|
||||
this.parseYamlFallback(yamlText, domainData);
|
||||
}
|
||||
|
||||
return domainData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback YAML parsing method for different structures
|
||||
* @param {string} yamlText - Raw YAML content
|
||||
* @param {Object} domainData - Domain data object to populate
|
||||
*/
|
||||
parseYamlFallback(yamlText, domainData) {
|
||||
try {
|
||||
// Look for any line that looks like a domain
|
||||
const lines = yamlText.split("\n");
|
||||
const domainRegex =
|
||||
/^[\s\-]*(?:["'])?([a-zA-Z0-9\-\+\.]+\.[a-zA-Z]{2,}|[\w\-\+\.]+\*[\w\-\.]*|\+\.[\w\-\.]+)(?:["'])?[\s]*$/;
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(domainRegex);
|
||||
if (match) {
|
||||
const domain = match[1].trim();
|
||||
|
||||
if (domain.startsWith("+.")) {
|
||||
domainData.domainSuffixes.push(domain);
|
||||
} else if (domain.includes("*") || domain.includes("KEYWORD")) {
|
||||
domainData.keywords.push(domain);
|
||||
} else {
|
||||
domainData.domains.push(domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
domainData.total =
|
||||
domainData.domains.length +
|
||||
domainData.domainSuffixes.length +
|
||||
domainData.keywords.length;
|
||||
} catch (error) {
|
||||
console.error("Fallback YAML parsing failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display domain data in the modal
|
||||
* @param {string} serviceName - Name of the service
|
||||
* @param {Object} domainData - Parsed domain data
|
||||
*/
|
||||
displayDomainData(serviceName, domainData) {
|
||||
// Update service info
|
||||
this.elements.modalServiceName.textContent = serviceName;
|
||||
this.elements.modalDescription.textContent = `Domain rules and patterns for ${serviceName} service`;
|
||||
|
||||
// Build domains list HTML
|
||||
let domainsHTML = "";
|
||||
|
||||
// Regular domains
|
||||
if (domainData.domains.length > 0) {
|
||||
domainsHTML += this.createDomainCategory(
|
||||
"Exact Domains",
|
||||
"fas fa-globe",
|
||||
domainData.domains,
|
||||
"Exact domain matches"
|
||||
);
|
||||
}
|
||||
|
||||
// Domain suffixes
|
||||
if (domainData.domainSuffixes.length > 0) {
|
||||
domainsHTML += this.createDomainCategory(
|
||||
"Domain Suffixes",
|
||||
"fas fa-sitemap",
|
||||
domainData.domainSuffixes.map((domain) => `+.${domain}`),
|
||||
"Matches domain and all subdomains"
|
||||
);
|
||||
}
|
||||
|
||||
// Keywords
|
||||
if (domainData.keywords.length > 0) {
|
||||
domainsHTML += this.createDomainCategory(
|
||||
"Pattern Rules",
|
||||
"fas fa-key",
|
||||
domainData.keywords,
|
||||
"Keyword-based and wildcard matching rules"
|
||||
);
|
||||
}
|
||||
|
||||
if (domainsHTML === "") {
|
||||
domainsHTML =
|
||||
'<div class="no-domains"><i class="fas fa-inbox"></i><p>No domain rules found</p></div>';
|
||||
}
|
||||
|
||||
this.elements.domainsList.innerHTML = domainsHTML;
|
||||
|
||||
// Show content and hide loading
|
||||
this.hideLoading();
|
||||
this.elements.domainsList.style.display = "block";
|
||||
|
||||
// Add click handlers for category toggles
|
||||
this.setupCategoryToggles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTML for a domain category
|
||||
* @param {string} title - Category title
|
||||
* @param {string} icon - Font Awesome icon class
|
||||
* @param {Array} domains - Array of domains
|
||||
* @param {string} description - Category description
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
createDomainCategory(title, icon, domains, description) {
|
||||
const domainItems = domains
|
||||
.map(
|
||||
(domain) =>
|
||||
`<div class="domain-item" title="${description}">${domain}</div>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="domain-category">
|
||||
<div class="category-header" data-category="${title.toLowerCase()}">
|
||||
<div class="category-title">
|
||||
<i class="${icon}"></i>
|
||||
<span>${title}</span>
|
||||
</div>
|
||||
<div class="category-count">${domains.length}</div>
|
||||
<i class="fas fa-chevron-down category-toggle"></i>
|
||||
</div>
|
||||
<div class="domain-items">
|
||||
${domainItems}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up category toggle functionality
|
||||
*/
|
||||
setupCategoryToggles() {
|
||||
const categoryHeaders =
|
||||
this.elements.domainsList.querySelectorAll(".category-header");
|
||||
|
||||
categoryHeaders.forEach((header) => {
|
||||
header.addEventListener("click", () => {
|
||||
header.classList.toggle("collapsed");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading state
|
||||
* @param {string} serviceName - Name of the service
|
||||
*/
|
||||
showLoading(serviceName) {
|
||||
this.elements.modalServiceName.textContent = serviceName;
|
||||
this.elements.modalDescription.textContent =
|
||||
"Loading domain information...";
|
||||
|
||||
this.elements.modalLoading.style.display = "block";
|
||||
this.elements.domainsList.style.display = "none";
|
||||
this.elements.modalError.style.display = "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading state
|
||||
*/
|
||||
hideLoading() {
|
||||
this.elements.modalLoading.style.display = "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error state
|
||||
* @param {string} message - Error message
|
||||
*/
|
||||
showError(message) {
|
||||
this.hideLoading();
|
||||
this.elements.domainsList.style.display = "none";
|
||||
|
||||
this.elements.errorMessage.textContent = message;
|
||||
this.elements.modalError.style.display = "block";
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the modal
|
||||
*/
|
||||
openModal() {
|
||||
this.elements.modal.classList.add("show");
|
||||
this.elements.modal.setAttribute("aria-hidden", "false");
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
// Focus the close button for accessibility
|
||||
setTimeout(() => {
|
||||
this.elements.modalClose.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal
|
||||
*/
|
||||
closeModal() {
|
||||
this.elements.modal.classList.remove("show");
|
||||
this.elements.modal.setAttribute("aria-hidden", "true");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if modal is currently open
|
||||
* @returns {boolean} True if modal is open
|
||||
*/
|
||||
isModalOpen() {
|
||||
return this.elements.modal.classList.contains("show");
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger domain preview for a service
|
||||
* @param {string} serviceName - Name of the service
|
||||
*/
|
||||
static requestPreview(serviceName) {
|
||||
const event = new CustomEvent("domainPreviewRequest", {
|
||||
detail: { serviceName },
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -154,9 +154,17 @@ export class ServicesManager {
|
||||
.map(
|
||||
(service) => `
|
||||
<div class="search-result-item" data-service-name="${service.name}">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex: 1;">
|
||||
<i class="fas fa-plus-circle"></i>
|
||||
<span>${service.name}</span>
|
||||
</div>
|
||||
<button class="preview-btn"
|
||||
data-service-name="${service.name}"
|
||||
title="Preview domains for ${service.name}">
|
||||
<i class="fas fa-eye"></i>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
@@ -177,6 +185,24 @@ export class ServicesManager {
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleSearchResultClick(event) {
|
||||
// Handle preview button clicks
|
||||
if (
|
||||
event.target.classList.contains("preview-btn") ||
|
||||
event.target.closest(".preview-btn")
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const previewBtn = event.target.classList.contains("preview-btn")
|
||||
? event.target
|
||||
: event.target.closest(".preview-btn");
|
||||
const serviceName = previewBtn.dataset.serviceName;
|
||||
if (serviceName) {
|
||||
this.requestDomainPreview(serviceName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const resultItem = event.target.closest(".search-result-item");
|
||||
if (!resultItem || resultItem.classList.contains("search-no-results"))
|
||||
return;
|
||||
@@ -188,10 +214,29 @@ export class ServicesManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clicks on selected services (removal)
|
||||
* Handle clicks on selected services (removal and preview)
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleSelectedServiceClick(event) {
|
||||
// Handle preview button clicks
|
||||
if (
|
||||
event.target.classList.contains("preview-btn") ||
|
||||
event.target.closest(".preview-btn")
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const previewBtn = event.target.classList.contains("preview-btn")
|
||||
? event.target
|
||||
: event.target.closest(".preview-btn");
|
||||
const serviceName = previewBtn.dataset.serviceName;
|
||||
if (serviceName) {
|
||||
this.requestDomainPreview(serviceName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle remove button clicks
|
||||
if (event.target.classList.contains("service-remove-btn")) {
|
||||
const serviceName = event.target.dataset.serviceName;
|
||||
if (serviceName) {
|
||||
@@ -238,6 +283,18 @@ export class ServicesManager {
|
||||
this.hideSearchResults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request domain preview for a service
|
||||
* @param {string} serviceName - Service name to preview
|
||||
*/
|
||||
requestDomainPreview(serviceName) {
|
||||
// Dispatch custom event for domain preview
|
||||
const event = new CustomEvent("domainPreviewRequest", {
|
||||
detail: { serviceName },
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI components
|
||||
*/
|
||||
@@ -281,6 +338,12 @@ export class ServicesManager {
|
||||
</div>
|
||||
<div class="service-info">
|
||||
<span class="service-name">${serviceName}</span>
|
||||
<button class="preview-btn"
|
||||
data-service-name="${serviceName}"
|
||||
title="Preview domains for ${serviceName}">
|
||||
<i class="fas fa-eye"></i>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
<button class="service-remove-btn"
|
||||
data-service-name="${serviceName}"
|
||||
|
||||
@@ -1471,6 +1471,7 @@ body::before {
|
||||
.service-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1643,3 +1644,393 @@ body::before {
|
||||
.skip-link:focus {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
/* Domain Preview Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 16px;
|
||||
width: 90vw;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
transform: scale(0.9) translateY(20px);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.show .modal-content {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border: 1px solid rgba(220, 38, 38, 0.2);
|
||||
color: var(--danger-color);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: rgba(220, 38, 38, 0.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
max-height: calc(80vh - 120px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.service-info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--primary-color);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.service-details h4 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.service-details p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.domains-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.loading-spinner i {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.domain-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.domains-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.domain-category {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.domain-category:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.category-header:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.category-count {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.category-toggle {
|
||||
color: var(--text-muted);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.category-header.collapsed .category-toggle {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.domain-items {
|
||||
padding: 0.5rem;
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.category-header.collapsed + .domain-items {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.domain-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
font-family: "Fira Code", monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s ease;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.domain-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-error {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.modal-error i {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-error h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.modal-error p {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Preview button styles */
|
||||
.preview-btn {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
color: var(--primary-color);
|
||||
border-radius: 6px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.preview-btn:hover {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border-color: rgba(99, 102, 241, 0.3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.preview-btn i {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* Scrollbar styling for modal */
|
||||
.modal-body::-webkit-scrollbar,
|
||||
.domains-list::-webkit-scrollbar,
|
||||
.domain-items::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-track,
|
||||
.domains-list::-webkit-scrollbar-track,
|
||||
.domain-items::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-thumb,
|
||||
.domains-list::-webkit-scrollbar-thumb,
|
||||
.domain-items::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-thumb:hover,
|
||||
.domains-list::-webkit-scrollbar-thumb:hover,
|
||||
.domain-items::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-hover);
|
||||
}
|
||||
|
||||
/* Responsive modal styles */
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
width: 95vw;
|
||||
max-height: 90vh;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.service-info-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.domain-stats {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.domain-items {
|
||||
grid-template-columns: 1fr;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.modal-content {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user