Replace display selection dropdowns with visual display picker lightbox
Remove the Displays tab and replace <select> dropdowns in stream and template test modals with a lightbox overlay showing the spatial display layout. Clicking a display selects it. Uses percentage-based positioning so the layout always fits its container. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,11 +56,125 @@ function closeLightbox(event) {
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && document.getElementById('image-lightbox').classList.contains('active')) {
|
||||
closeLightbox();
|
||||
if (e.key === 'Escape') {
|
||||
if (document.getElementById('display-picker-lightbox').classList.contains('active')) {
|
||||
closeDisplayPicker();
|
||||
} else if (document.getElementById('image-lightbox').classList.contains('active')) {
|
||||
closeLightbox();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Display picker lightbox
|
||||
let _displayPickerCallback = null;
|
||||
|
||||
function openDisplayPicker(callback) {
|
||||
_displayPickerCallback = callback;
|
||||
const lightbox = document.getElementById('display-picker-lightbox');
|
||||
const canvas = document.getElementById('display-picker-canvas');
|
||||
|
||||
lightbox.classList.add('active');
|
||||
|
||||
// Defer render to next frame so the lightbox has been laid out and canvas has dimensions
|
||||
requestAnimationFrame(() => {
|
||||
if (_cachedDisplays && _cachedDisplays.length > 0) {
|
||||
renderDisplayPickerLayout(_cachedDisplays);
|
||||
} else {
|
||||
canvas.innerHTML = '<div class="loading-spinner"></div>';
|
||||
loadDisplays().then(() => {
|
||||
if (_cachedDisplays && _cachedDisplays.length > 0) {
|
||||
renderDisplayPickerLayout(_cachedDisplays);
|
||||
} else {
|
||||
canvas.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeDisplayPicker(event) {
|
||||
if (event && event.target && event.target.closest('.display-picker-content')) return;
|
||||
const lightbox = document.getElementById('display-picker-lightbox');
|
||||
lightbox.classList.remove('active');
|
||||
_displayPickerCallback = null;
|
||||
}
|
||||
|
||||
function selectDisplay(displayIndex) {
|
||||
if (_displayPickerCallback) {
|
||||
const display = _cachedDisplays ? _cachedDisplays.find(d => d.index === displayIndex) : null;
|
||||
_displayPickerCallback(displayIndex, display);
|
||||
}
|
||||
closeDisplayPicker();
|
||||
}
|
||||
|
||||
function renderDisplayPickerLayout(displays) {
|
||||
const canvas = document.getElementById('display-picker-canvas');
|
||||
|
||||
if (!displays || displays.length === 0) {
|
||||
canvas.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
displays.forEach(display => {
|
||||
minX = Math.min(minX, display.x);
|
||||
minY = Math.min(minY, display.y);
|
||||
maxX = Math.max(maxX, display.x + display.width);
|
||||
maxY = Math.max(maxY, display.y + display.height);
|
||||
});
|
||||
|
||||
const totalWidth = maxX - minX;
|
||||
const totalHeight = maxY - minY;
|
||||
const aspect = totalHeight / totalWidth;
|
||||
|
||||
// Use percentage-based positioning so layout always fits its container
|
||||
const displayElements = displays.map(display => {
|
||||
const leftPct = ((display.x - minX) / totalWidth) * 100;
|
||||
const topPct = ((display.y - minY) / totalHeight) * 100;
|
||||
const widthPct = (display.width / totalWidth) * 100;
|
||||
const heightPct = (display.height / totalHeight) * 100;
|
||||
|
||||
return `
|
||||
<div class="layout-display layout-display-pickable ${display.is_primary ? 'primary' : 'secondary'}"
|
||||
style="left: ${leftPct}%; top: ${topPct}%; width: ${widthPct}%; height: ${heightPct}%;"
|
||||
onclick="selectDisplay(${display.index})"
|
||||
title="${t('displays.picker.click_to_select')}">
|
||||
<div class="layout-position-label">(${display.x}, ${display.y})</div>
|
||||
<div class="layout-index-label">#${display.index}</div>
|
||||
<div class="layout-display-label">
|
||||
<strong>${display.name}</strong>
|
||||
<small>${display.width}×${display.height}</small>
|
||||
<small>${display.refresh_rate}Hz</small>
|
||||
</div>
|
||||
${display.is_primary ? '<div class="primary-indicator">★</div>' : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
canvas.innerHTML = `
|
||||
<div class="layout-container" style="width: 100%; padding-bottom: ${aspect * 100}%; position: relative;">
|
||||
${displayElements}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function formatDisplayLabel(displayIndex, display) {
|
||||
if (display) {
|
||||
return `#${display.index}: ${display.width}×${display.height}${display.is_primary ? ' (' + t('displays.badge.primary') + ')' : ''}`;
|
||||
}
|
||||
return `Display ${displayIndex}`;
|
||||
}
|
||||
|
||||
function onStreamDisplaySelected(displayIndex, display) {
|
||||
document.getElementById('stream-display-index').value = displayIndex;
|
||||
document.getElementById('stream-display-picker-label').textContent = formatDisplayLabel(displayIndex, display);
|
||||
}
|
||||
|
||||
function onTestDisplaySelected(displayIndex, display) {
|
||||
document.getElementById('test-template-display').value = displayIndex;
|
||||
document.getElementById('test-display-picker-label').textContent = formatDisplayLabel(displayIndex, display);
|
||||
}
|
||||
|
||||
// Locale management
|
||||
let currentLocale = 'en';
|
||||
let translations = {};
|
||||
@@ -346,23 +460,11 @@ async function loadDisplays() {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const container = document.getElementById('displays-list');
|
||||
|
||||
if (!data.displays || data.displays.length === 0) {
|
||||
container.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
||||
document.getElementById('display-layout-canvas').innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
||||
return;
|
||||
if (data.displays && data.displays.length > 0) {
|
||||
_cachedDisplays = data.displays;
|
||||
}
|
||||
|
||||
// Cache and render visual layout
|
||||
_cachedDisplays = data.displays;
|
||||
renderDisplayLayout(data.displays);
|
||||
} catch (error) {
|
||||
console.error('Failed to load displays:', error);
|
||||
document.getElementById('displays-list').innerHTML =
|
||||
`<div class="loading">${t('displays.failed')}</div>`;
|
||||
document.getElementById('display-layout-canvas').innerHTML =
|
||||
`<div class="loading">${t('displays.failed')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,9 +474,6 @@ function switchTab(name) {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === name));
|
||||
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`));
|
||||
localStorage.setItem('activeTab', name);
|
||||
if (name === 'displays' && _cachedDisplays) {
|
||||
requestAnimationFrame(() => renderDisplayLayout(_cachedDisplays));
|
||||
}
|
||||
if (name === 'templates') {
|
||||
loadCaptureTemplates();
|
||||
}
|
||||
@@ -393,66 +492,6 @@ function initTabs() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderDisplayLayout(displays) {
|
||||
const canvas = document.getElementById('display-layout-canvas');
|
||||
|
||||
if (!displays || displays.length === 0) {
|
||||
canvas.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate bounding box for all displays
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
|
||||
displays.forEach(display => {
|
||||
minX = Math.min(minX, display.x);
|
||||
minY = Math.min(minY, display.y);
|
||||
maxX = Math.max(maxX, display.x + display.width);
|
||||
maxY = Math.max(maxY, display.y + display.height);
|
||||
});
|
||||
|
||||
const totalWidth = maxX - minX;
|
||||
const totalHeight = maxY - minY;
|
||||
|
||||
// Scale factor to fit in canvas (respect available width, maintain aspect ratio)
|
||||
const availableWidth = canvas.clientWidth - 60; // account for padding
|
||||
const maxCanvasHeight = 350;
|
||||
const scaleX = availableWidth / totalWidth;
|
||||
const scaleY = maxCanvasHeight / totalHeight;
|
||||
const scale = Math.min(scaleX, scaleY);
|
||||
|
||||
const canvasWidth = totalWidth * scale;
|
||||
const canvasHeight = totalHeight * scale;
|
||||
|
||||
// Create display elements
|
||||
const displayElements = displays.map(display => {
|
||||
const left = (display.x - minX) * scale;
|
||||
const top = (display.y - minY) * scale;
|
||||
const width = display.width * scale;
|
||||
const height = display.height * scale;
|
||||
|
||||
return `
|
||||
<div class="layout-display ${display.is_primary ? 'primary' : 'secondary'}"
|
||||
style="left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px;"
|
||||
title="${display.name}\n${display.width}×${display.height}\nPosition: (${display.x}, ${display.y})">
|
||||
<div class="layout-position-label">(${display.x}, ${display.y})</div>
|
||||
<div class="layout-index-label">#${display.index}</div>
|
||||
<div class="layout-display-label">
|
||||
<strong>${display.name}</strong>
|
||||
<small>${display.width}×${display.height}</small>
|
||||
<small>${display.refresh_rate}Hz</small>
|
||||
</div>
|
||||
${display.is_primary ? '<div class="primary-indicator">★</div>' : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
canvas.innerHTML = `
|
||||
<div class="layout-container" style="width: ${canvasWidth}px; height: ${canvasHeight}px; margin: 0 auto; position: relative;">
|
||||
${displayElements}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Load devices
|
||||
async function loadDevices() {
|
||||
@@ -2815,33 +2854,31 @@ function collectEngineConfig() {
|
||||
// Load displays for test selector
|
||||
async function loadDisplaysForTest() {
|
||||
try {
|
||||
const response = await fetchWithAuth('/config/displays');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load displays: ${response.status}`);
|
||||
if (!_cachedDisplays) {
|
||||
const response = await fetchWithAuth('/config/displays');
|
||||
if (!response.ok) throw new Error(`Failed to load displays: ${response.status}`);
|
||||
const displaysData = await response.json();
|
||||
_cachedDisplays = displaysData.displays || [];
|
||||
}
|
||||
|
||||
const displaysData = await response.json();
|
||||
const select = document.getElementById('test-template-display');
|
||||
select.innerHTML = '';
|
||||
|
||||
let primaryIndex = null;
|
||||
(displaysData.displays || []).forEach(display => {
|
||||
const option = document.createElement('option');
|
||||
option.value = display.index;
|
||||
option.textContent = `Display ${display.index} (${display.width}x${display.height})`;
|
||||
if (display.is_primary) {
|
||||
option.textContent += ' ★';
|
||||
primaryIndex = display.index;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
// Auto-select: last used display, or primary as fallback
|
||||
let selectedIndex = null;
|
||||
const lastDisplay = localStorage.getItem('lastTestDisplayIndex');
|
||||
if (lastDisplay !== null && select.querySelector(`option[value="${lastDisplay}"]`)) {
|
||||
select.value = lastDisplay;
|
||||
} else if (primaryIndex !== null) {
|
||||
select.value = String(primaryIndex);
|
||||
|
||||
if (lastDisplay !== null) {
|
||||
const found = _cachedDisplays.find(d => d.index === parseInt(lastDisplay));
|
||||
if (found) selectedIndex = found.index;
|
||||
}
|
||||
|
||||
if (selectedIndex === null) {
|
||||
const primary = _cachedDisplays.find(d => d.is_primary);
|
||||
if (primary) selectedIndex = primary.index;
|
||||
else if (_cachedDisplays.length > 0) selectedIndex = _cachedDisplays[0].index;
|
||||
}
|
||||
|
||||
if (selectedIndex !== null) {
|
||||
const display = _cachedDisplays.find(d => d.index === selectedIndex);
|
||||
onTestDisplaySelected(selectedIndex, display);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading displays:', error);
|
||||
@@ -3147,6 +3184,8 @@ async function showAddStreamModal(presetType) {
|
||||
document.getElementById('stream-modal-title').textContent = t(titleKey);
|
||||
document.getElementById('stream-form').reset();
|
||||
document.getElementById('stream-id').value = '';
|
||||
document.getElementById('stream-display-index').value = '';
|
||||
document.getElementById('stream-display-picker-label').textContent = t('displays.picker.select');
|
||||
document.getElementById('stream-error').style.display = 'none';
|
||||
document.getElementById('stream-type').value = streamType;
|
||||
onStreamTypeChange();
|
||||
@@ -3181,7 +3220,9 @@ async function editStream(streamId) {
|
||||
await populateStreamModalDropdowns();
|
||||
|
||||
if (stream.stream_type === 'raw') {
|
||||
document.getElementById('stream-display-index').value = String(stream.display_index ?? 0);
|
||||
const displayIdx = stream.display_index ?? 0;
|
||||
const display = _cachedDisplays ? _cachedDisplays.find(d => d.index === displayIdx) : null;
|
||||
onStreamDisplaySelected(displayIdx, display);
|
||||
document.getElementById('stream-capture-template').value = stream.capture_template_id || '';
|
||||
const fps = stream.target_fps ?? 30;
|
||||
document.getElementById('stream-target-fps').value = fps;
|
||||
@@ -3210,23 +3251,16 @@ async function populateStreamModalDropdowns() {
|
||||
fetchWithAuth('/postprocessing-templates'),
|
||||
]);
|
||||
|
||||
// Displays
|
||||
const displaySelect = document.getElementById('stream-display-index');
|
||||
displaySelect.innerHTML = '';
|
||||
// Displays - warm cache for display picker
|
||||
if (displaysRes.ok) {
|
||||
const displaysData = await displaysRes.json();
|
||||
(displaysData.displays || []).forEach(d => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = d.index;
|
||||
opt.textContent = `${d.index}: ${d.width}x${d.height}${d.is_primary ? ` (${t('displays.badge.primary')})` : ''}`;
|
||||
displaySelect.appendChild(opt);
|
||||
});
|
||||
_cachedDisplays = displaysData.displays || [];
|
||||
}
|
||||
if (displaySelect.options.length === 0) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = '0';
|
||||
opt.textContent = '0';
|
||||
displaySelect.appendChild(opt);
|
||||
|
||||
// Auto-select primary display if none selected yet
|
||||
if (!document.getElementById('stream-display-index').value && _cachedDisplays && _cachedDisplays.length > 0) {
|
||||
const primary = _cachedDisplays.find(d => d.is_primary) || _cachedDisplays[0];
|
||||
onStreamDisplaySelected(primary.index, primary);
|
||||
}
|
||||
|
||||
// Capture templates
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<div class="tabs">
|
||||
<div class="tab-bar">
|
||||
<button class="tab-btn active" data-tab="devices" onclick="switchTab('devices')"><span data-i18n="devices.title">💡 Devices</span></button>
|
||||
<button class="tab-btn" data-tab="displays" onclick="switchTab('displays')"><span data-i18n="displays.layout">🖥️ Displays</span></button>
|
||||
|
||||
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')"><span data-i18n="streams.title">📺 Picture Streams</span></button>
|
||||
<button class="tab-btn" data-tab="templates" onclick="switchTab('templates')"><span data-i18n="templates.title">🎯 Capture Templates</span></button>
|
||||
<button class="tab-btn" data-tab="pp-templates" onclick="switchTab('pp-templates')"><span data-i18n="postprocessing.title">🎨 Processing Templates</span></button>
|
||||
@@ -55,14 +55,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-displays">
|
||||
<div class="display-layout-preview">
|
||||
<div id="display-layout-canvas" class="display-layout-canvas">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="displays-list" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-streams">
|
||||
<p class="section-tip">
|
||||
@@ -451,10 +443,11 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="test-template-display" data-i18n="templates.test.display">Display:</label>
|
||||
<select id="test-template-display">
|
||||
<option value="" data-i18n="templates.test.display.select">Select display...</option>
|
||||
</select>
|
||||
<label data-i18n="templates.test.display">Display:</label>
|
||||
<input type="hidden" id="test-template-display" value="">
|
||||
<button type="button" class="btn btn-display-picker" id="test-display-picker-btn" onclick="openDisplayPicker(onTestDisplaySelected)">
|
||||
<span id="test-display-picker-label" data-i18n="displays.picker.select">Select display...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -517,8 +510,11 @@
|
||||
<!-- Raw stream fields -->
|
||||
<div id="stream-raw-fields">
|
||||
<div class="form-group">
|
||||
<label for="stream-display-index" data-i18n="streams.display">Display:</label>
|
||||
<select id="stream-display-index"></select>
|
||||
<label data-i18n="streams.display">Display:</label>
|
||||
<input type="hidden" id="stream-display-index" value="">
|
||||
<button type="button" class="btn btn-display-picker" id="stream-display-picker-btn" onclick="openDisplayPicker(onStreamDisplaySelected)">
|
||||
<span id="stream-display-picker-label" data-i18n="displays.picker.select">Select display...</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stream-capture-template" data-i18n="streams.capture_template">Capture Template:</label>
|
||||
@@ -627,6 +623,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Picker Lightbox -->
|
||||
<div id="display-picker-lightbox" class="lightbox" onclick="closeDisplayPicker(event)">
|
||||
<button class="lightbox-close" onclick="closeDisplayPicker()" title="Close">✕</button>
|
||||
<div class="lightbox-content display-picker-content">
|
||||
<h3 class="display-picker-title" data-i18n="displays.picker.title">Select a Display</h3>
|
||||
<div id="display-picker-canvas" class="display-picker-canvas">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
<script>
|
||||
// Initialize theme
|
||||
@@ -682,7 +689,6 @@
|
||||
|
||||
// Clear the UI
|
||||
document.getElementById('devices-list').innerHTML = `<div class="loading">${t('auth.please_login')} devices</div>`;
|
||||
document.getElementById('displays-list').innerHTML = `<div class="loading">${t('auth.please_login')} displays</div>`;
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
"displays.loading": "Loading displays...",
|
||||
"displays.none": "No displays available",
|
||||
"displays.failed": "Failed to load displays",
|
||||
"displays.picker.title": "Select a Display",
|
||||
"displays.picker.select": "Select display...",
|
||||
"displays.picker.click_to_select": "Click to select this display",
|
||||
"templates.title": "\uD83C\uDFAF Capture Templates",
|
||||
"templates.description": "Capture templates define how the screen is captured. Each template uses a specific capture engine (MSS, DXcam, WGC) with custom settings. Assign templates to devices for optimal performance.",
|
||||
"templates.loading": "Loading templates...",
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
"displays.loading": "Загрузка дисплеев...",
|
||||
"displays.none": "Нет доступных дисплеев",
|
||||
"displays.failed": "Не удалось загрузить дисплеи",
|
||||
"displays.picker.title": "Выберите Дисплей",
|
||||
"displays.picker.select": "Выберите дисплей...",
|
||||
"displays.picker.click_to_select": "Нажмите, чтобы выбрать этот дисплей",
|
||||
"templates.title": "\uD83C\uDFAF Шаблоны Захвата",
|
||||
"templates.description": "Шаблоны захвата определяют, как захватывается экран. Каждый шаблон использует определённый движок захвата (MSS, DXcam, WGC) с настраиваемыми параметрами. Назначайте шаблоны устройствам для оптимальной производительности.",
|
||||
"templates.loading": "Загрузка шаблонов...",
|
||||
|
||||
@@ -487,32 +487,6 @@ section {
|
||||
}
|
||||
|
||||
/* Display Layout Visualization */
|
||||
.display-layout-preview {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.display-layout-preview h3 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.display-layout-canvas {
|
||||
background: var(--bg-color);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
min-height: 280px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout-container {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
@@ -527,12 +501,10 @@ section {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
cursor: help;
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.layout-display:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -597,39 +569,6 @@ section {
|
||||
text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.layout-legend {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.legend-dot.primary {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
.legend-dot.secondary {
|
||||
border-color: var(--border-color);
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
/* Card brightness slider */
|
||||
.brightness-control {
|
||||
@@ -2327,6 +2266,58 @@ input:-webkit-autofill:focus {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Display Picker Lightbox */
|
||||
.display-picker-content {
|
||||
max-width: 900px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.display-picker-title {
|
||||
color: white;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.display-picker-canvas {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 2px dashed rgba(255, 255, 255, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.layout-display-pickable {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.layout-display-pickable:hover {
|
||||
box-shadow: 0 0 20px rgba(76, 175, 80, 0.4);
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* Display picker button in forms */
|
||||
.btn-display-picker {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.btn-display-picker:hover {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.15);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.templates-grid {
|
||||
|
||||
Reference in New Issue
Block a user