GWM : improved some ui

This commit is contained in:
Gowtham M 2026-04-02 16:46:32 +05:30
parent 948553aac7
commit 460ebf3e30
11 changed files with 389 additions and 27 deletions

View File

@ -554,7 +554,7 @@ class ChartController extends BaseController
$clean[] = $h;
}
}
$out['palette_colors'] = array_slice($clean, 0, 8);
$out['palette_colors'] = array_slice($clean, 0, 12);
}
return $out;

View File

@ -91,6 +91,7 @@ class DashboardController extends BaseController
'chartsList' => $charts,
'dashBoot' => [
'dashboardId' => $id,
'dashboardName' => (string) ($dashboard['name'] ?? 'dashboard'),
'workspaceTheme'=> (string) ($dashboard['theme'] ?? 'system'),
'refreshSec' => max(0, (int) ($dashboard['refresh_interval'] ?? 0)),
'widgets' => $this->widgetsBootPayload($widgets),
@ -102,6 +103,7 @@ class DashboardController extends BaseController
'urls' => [
'chartData' => rtrim(base_url(), '/') . '/chart/',
'chartEdit' => rtrim(base_url(), '/') . '/chart/edit/',
'chartExportBase' => rtrim(base_url(), '/') . '/chart/',
'saveLayout' => rtrim(base_url(), '/') . '/dashboard/' . $id . '/layout',
'addWidget' => rtrim(base_url(), '/') . '/dashboard/' . $id . '/widget',
'delWidget' => rtrim(base_url(), '/') . '/dashboard/widget/',

View File

@ -227,6 +227,7 @@ class PublicController extends BaseController
$dashBoot = [
'dashboardId' => $dashId,
'dashboardName' => (string) ($dashboard['name'] ?? 'dashboard'),
'workspaceTheme'=> (string) ($dashboard['theme'] ?? 'system'),
'refreshSec' => max(0, (int) ($dashboard['refresh_interval'] ?? 0)),
'widgets' => $this->widgetsBootPayload($widgets),

View File

@ -99,6 +99,10 @@ class ChartRenderer
$y = trim((string) ($chart['y_field'] ?? ''));
$group = trim((string) ($chart['group_field'] ?? ''));
$valueF = trim((string) ($chart['value_field'] ?? ''));
// Combo charts are designed as 2-metric single-series overlays; ignore grouping.
if ($type === 'combo') {
$group = '';
}
$colors = $this->resolveColors($display);
$baseFont = 'Inter, system-ui, sans-serif';
@ -152,7 +156,8 @@ class ChartRenderer
default => 'pie',
};
$plotOptions = [];
// Keep plotOptions as an object for Apex (not [] array) when no pie config is needed.
$plotOptions = new \stdClass();
if ($type === 'pie' || $type === 'donut') {
$plotOptions['pie'] = [
'donut' => [
@ -294,6 +299,7 @@ class ChartRenderer
'plotOptions' => [
'bar' => [
'horizontal' => true,
'isFunnel' => true,
'barHeight' => '75%',
'distributed' => true,
'borderRadius' => 6,
@ -347,7 +353,7 @@ class ChartRenderer
}
if ($type === 'heatmap') {
if ($x === '' || $y === '') {
if ($x === '' || $y === '' || $group === '') {
return null;
}
$seriesNameCol = $group !== '' ? $group : '__single__';
@ -436,7 +442,7 @@ class ChartRenderer
if ($type === 'combo') {
$y2 = trim((string) ($display['secondary_y_field'] ?? ''));
if ($y2 === '' || $group !== '') {
if ($y2 === '') {
return null;
}
$lineData = $this->columnValues($rows, $categories, $x, $y2, '');
@ -512,7 +518,7 @@ class ChartRenderer
'legend' => $legend,
'plotOptions' => ['bar' => $plotBar],
'fill' => $fill,
'tooltip' => $this->sharedTooltip(),
'tooltip' => $apexType === 'bar' ? $this->pointTooltip() : $this->sharedTooltip(),
];
}
@ -530,6 +536,20 @@ class ChartRenderer
];
}
/**
* Show only hovered datapoint in tooltip (used for bar charts).
*
* @return array<string, mixed>
*/
private function pointTooltip(): array
{
return [
'shared' => false,
'intersect' => true,
'theme' => 'light',
];
}
/**
* @param array<string, mixed> $display
* @return array<string, mixed>

View File

@ -101,7 +101,7 @@ $bootstrapJson = json_encode($bootstrap, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX
</div>
<div class="row g-3 align-items-start cb-cb-main-row">
<div class="col-lg-5">
<div class="col-lg-4">
<div class="cb-cb-card cb-cb-config-card">
<div class="cb-cb-config-tabs">
<button type="button" class="cb-cb-tab-btn" :class="{ active: configTab === 'data' }" @click="configTab = 'data'">Data</button>
@ -266,8 +266,8 @@ $bootstrapJson = json_encode($bootstrap, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX
</div>
<div class="mt-2" x-show="display.color_mode === 'custom'">
<label class="form-label small">Swatches</label>
<div class="d-flex flex-wrap gap-2 align-items-center">
<template x-for="idx in [0,1,2,3,4]" :key="idx">
<div class="cb-cb-swatch-grid">
<template x-for="idx in [0,1,2,3,4,5,6,7,8,9,10,11]" :key="idx">
<input type="color" class="form-control form-control-color cb-cb-color-swatch" :title="'Color ' + (idx + 1)" x-model="display.palette_colors[idx]" @input="queueRenderPreview()">
</template>
</div>
@ -320,7 +320,7 @@ $bootstrapJson = json_encode($bootstrap, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX
</div>
</div>
<div class="col-lg-7">
<div class="col-lg-8">
<div class="cb-cb-card cb-cb-preview-card">
<div class="cb-cb-card-h">
<span class="fw-bold">Live preview</span>

View File

@ -232,11 +232,11 @@
ensurePaletteColors() {
const defs = ['#36a2eb', '#4bc0c0', '#ffcd56', '#ff9f40', '#9966ff', '#2f7ed8', '#26c6da', '#f87171'];
if (!Array.isArray(this.display.palette_colors)) {
this.display.palette_colors = defs.slice(0, 5);
this.display.palette_colors = defs.slice(0);
} else {
for (let i = 0; i < 5; i++) {
for (let i = 0; i < 12; i++) {
if (this.display.palette_colors[i] == null || this.display.palette_colors[i] === '') {
this.display.palette_colors[i] = defs[i] || '#111827';
this.display.palette_colors[i] = defs[i % defs.length] || '#111827';
}
}
}
@ -395,7 +395,8 @@
this.valueField = setIf(this.valueField, defaultY);
}
if (this.chartType === 'combo') {
this.display.secondary_y_field = setIf(this.display.secondary_y_field, nums[1] || '');
const alt = nums.find((n) => n !== (this.yField || defaultY)) || '';
this.display.secondary_y_field = setIf(this.display.secondary_y_field, alt || this.yField || defaultY);
}
if (this.chartType === 'table') {
this.xField = '';
@ -408,9 +409,10 @@
canRenderPreview() {
if (!this.savedQueryId || !this.columns.length) return false;
if (this.chartType === 'table') return true;
if (this.chartType === 'heatmap') return !!(this.xField && this.yField && this.groupField);
if (this.chartType === 'gauge' || this.chartType === 'kpi_card') return !!(this.valueField || this.yField);
if (this.chartType === 'bubble') return !!(this.xField && this.yField && this.valueField);
if (this.chartType === 'combo') return !!(this.xField && this.yField && this.display.secondary_y_field);
if (this.chartType === 'combo') return !!(this.xField && this.yField);
if (this.needsXY()) return !!(this.xField && this.yField);
return true;
},
@ -528,6 +530,17 @@
try { el._apexChart.destroy(); } catch (e) { /* */ }
el._apexChart = null;
}
if (this.chartType === 'heatmap' && !this.groupField) {
this.renderError = 'Heatmap needs Group / series field (row dimension).';
this.renderMeta = '';
if (el) {
el.innerHTML = '<div class="d-flex align-items-center justify-content-center h-100 text-muted small p-4">Select Group / series to render heatmap.</div>';
}
return;
}
if (this.chartType === 'combo' && !this.display.secondary_y_field) {
this.display.secondary_y_field = this.yField || '';
}
this.renderLoading = true;
this.renderError = '';
const body = new URLSearchParams();
@ -537,7 +550,7 @@
body.append('chart_type', this.chartType);
body.append('x_field', this.xField);
body.append('y_field', this.yField);
body.append('group_field', this.groupField);
body.append('group_field', this.chartType === 'combo' ? '' : this.groupField);
body.append('value_field', this.valueField);
body.append('display_config', this.displayJson());
try {

View File

@ -260,6 +260,257 @@
return '<label class="form-label small mb-0">' + escapeHtml(lab) + '</label><input type="text" class="form-control form-control-sm" data-cb-var-key="' + escapeHtml(k) + '" value="' + escapeHtml(globalVars[k] ?? '') + '"' + req + '>';
}
function sanitizeFileName(raw) {
const s = String(raw ?? '').trim();
const cleaned = s.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
return cleaned || 'download';
}
function getDashBackgroundColor() {
const shell = document.getElementById('cbDashShell');
return shell && shell.classList.contains('cb-theme-dark') ? '#0f172a' : '#ffffff';
}
function downloadDataUri(dataUri, filename) {
const a = document.createElement('a');
a.href = dataUri;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForDashChartsLoaded(timeoutMs) {
const deadline = Date.now() + Math.max(0, timeoutMs || 0);
const chartWidgets = Array.isArray(boot.widgets) ? boot.widgets.filter((w) => w.widget_type === 'chart' && w.id) : [];
if (!chartWidgets.length) return true;
while (Date.now() <= deadline) {
let pending = false;
for (const w of chartWidgets) {
const loadEl = document.getElementById('cb-load-' + w.id);
if (loadEl && !loadEl.hidden) { pending = true; break; }
}
if (!pending) return true;
await sleep(250);
}
// Best-effort: export anyway.
return false;
}
async function elementToPngDataUri(el, scale) {
if (typeof html2canvas === 'undefined') {
throw new Error('html2canvas not available');
}
const canvas = await html2canvas(el, {
scale: scale || 2,
useCORS: true,
allowTaint: true,
backgroundColor: getDashBackgroundColor(),
});
return canvas.toDataURL('image/png');
}
async function exportChartPng(w) {
const chartEl = document.getElementById('cb-chart-' + w.id);
const loadEl = document.getElementById('cb-load-' + w.id);
if (loadEl && !loadEl.hidden) {
alert('Chart is still loading. Please wait a moment.');
return;
}
if (!chartEl) { alert('Chart not found.'); return; }
const name = sanitizeFileName(w.title || w.chart_name || 'chart') + '.png';
const apex = chartEl._apexChart;
try {
if (apex && typeof apex.dataURI === 'function') {
const res = await apex.dataURI({ scale: 2, width: chartEl.offsetWidth || undefined });
const imgURI = res && res.imgURI ? res.imgURI : null;
if (!imgURI) throw new Error('No imgURI from Apex.');
downloadDataUri(imgURI, name);
return;
}
const dataUri = await elementToPngDataUri(chartEl, 2);
downloadDataUri(dataUri, name);
} catch (e) {
console.error(e);
alert('Could not export PNG.');
}
}
async function exportChartPdf(w) {
const chartEl = document.getElementById('cb-chart-' + w.id);
const loadEl = document.getElementById('cb-load-' + w.id);
if (loadEl && !loadEl.hidden) {
alert('Chart is still loading. Please wait a moment.');
return;
}
if (!chartEl) { alert('Chart not found.'); return; }
const jsPDFCtor = window.jspdf && window.jspdf.jsPDF ? window.jspdf.jsPDF : null;
if (!jsPDFCtor) { alert('PDF export is not available.'); return; }
const name = sanitizeFileName(w.title || w.chart_name || 'chart') + '.pdf';
let imgDataUri = null;
const apex = chartEl._apexChart;
try {
if (apex && typeof apex.dataURI === 'function') {
const res = await apex.dataURI({ scale: 2, width: chartEl.offsetWidth || undefined });
imgDataUri = res && res.imgURI ? res.imgURI : null;
} else {
imgDataUri = await elementToPngDataUri(chartEl, 2);
}
} catch (e) {
console.error(e);
alert('Could not export PDF.');
return;
}
if (!imgDataUri) { alert('Could not export PDF.'); return; }
const rect = chartEl.getBoundingClientRect();
const orientation = rect.width >= rect.height ? 'landscape' : 'portrait';
const doc = new jsPDFCtor({ orientation, unit: 'pt', format: 'a4' });
const pageW = doc.internal.pageSize.getWidth();
const pageH = doc.internal.pageSize.getHeight();
const margin = 18;
let img;
try {
img = await new Promise((resolve, reject) => {
const i = new Image();
i.onload = () => resolve(i);
i.onerror = (err) => reject(err);
i.src = imgDataUri;
});
} catch (e) {
console.error(e);
alert('Could not export PDF.');
return;
}
const ratio = img.height / Math.max(1, img.width);
let imgW = pageW - margin * 2;
let imgH = imgW * ratio;
if (imgH > pageH - margin * 2) {
imgH = pageH - margin * 2;
imgW = imgH / ratio;
}
const x = (pageW - imgW) / 2;
const y = (pageH - imgH) / 2;
doc.addImage(imgDataUri, 'PNG', x, y, imgW, imgH);
doc.save(name);
}
async function exportDashboardPdf() {
const btn = document.getElementById('cbBtnExportDashPdf');
const btnLabel = btn ? btn.querySelector('span:not(.material-symbols-outlined)') : null;
const oldLabel = btnLabel ? btnLabel.textContent : '';
if (btn) btn.disabled = true;
if (btnLabel) btnLabel.textContent = 'Exporting...';
try {
if (typeof html2canvas === 'undefined') throw new Error('html2canvas missing');
const jsPDFCtor = window.jspdf && window.jspdf.jsPDF ? window.jspdf.jsPDF : null;
if (!jsPDFCtor) throw new Error('jsPDF missing');
await waitForDashChartsLoaded(10000);
const pageBreaks = document.getElementById('cbChkDashPdfPageBreaks')?.checked !== false;
const shell = document.getElementById('cbDashShell');
const dashName = sanitizeFileName(boot.dashboardName || (shell && shell.querySelector('h1') ? shell.querySelector('h1').textContent : 'dashboard'));
const bg = getDashBackgroundColor();
// Hide widget export buttons so they don't pollute the PDF.
const exportBtns = Array.from(document.querySelectorAll('.cb-dash-widget-export'));
const prevDisplay = exportBtns.map((el) => el.style.display);
exportBtns.forEach((el) => { el.style.display = 'none'; });
try {
if (!pageBreaks) {
const root = document.getElementById('cbGridStack') || shell;
const doc = new jsPDFCtor({ orientation: 'portrait', unit: 'pt', format: 'a4' });
const canvas = await html2canvas(root, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: bg,
});
const imgDataUri = canvas.toDataURL('image/png');
const pageW = doc.internal.pageSize.getWidth();
const pageH = doc.internal.pageSize.getHeight();
const margin = 18;
const ratio = canvas.height / Math.max(1, canvas.width);
let imgW = pageW - margin * 2;
let imgH = imgW * ratio;
if (imgH > pageH - margin * 2) {
imgH = pageH - margin * 2;
imgW = imgH / ratio;
}
const x = (pageW - imgW) / 2;
const y = (pageH - imgH) / 2;
doc.addImage(imgDataUri, 'PNG', x, y, imgW, imgH);
doc.save(dashName + '.pdf');
return;
}
// Capture the current dashboard layout as-is, then slice into A4 pages.
// This matches the on-screen view (instead of forcing "one widget per page").
const root = document.getElementById('cbGridStack') || shell;
const doc = new jsPDFCtor({ orientation: 'portrait', unit: 'pt', format: 'a4' });
const pageW = doc.internal.pageSize.getWidth();
const pageH = doc.internal.pageSize.getHeight();
const margin = 14;
const canvas = await html2canvas(root, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: bg,
});
const imgW = pageW - margin * 2;
const scaleFactor = imgW / Math.max(1, canvas.width); // pt per px
const availableHpt = pageH - margin * 2;
const sliceHpx = Math.max(1, Math.floor(availableHpt / Math.max(0.0001, scaleFactor)));
let pageIndex = 0;
for (let y = 0; y < canvas.height; y += sliceHpx) {
const pageCanvas = document.createElement('canvas');
const pageHeightPx = Math.min(sliceHpx, canvas.height - y);
pageCanvas.width = canvas.width;
pageCanvas.height = pageHeightPx;
const ctx = pageCanvas.getContext('2d');
ctx.drawImage(canvas, 0, y, canvas.width, pageHeightPx, 0, 0, canvas.width, pageHeightPx);
const imgDataUri = pageCanvas.toDataURL('image/png');
const imgH = pageHeightPx * scaleFactor;
if (pageIndex > 0) doc.addPage();
doc.addImage(imgDataUri, 'PNG', margin, margin, imgW, imgH);
pageIndex++;
}
doc.save(dashName + '.pdf');
} finally {
// Restore export buttons no matter what.
exportBtns.forEach((el, idx) => { el.style.display = prevDisplay[idx]; });
}
} catch (e) {
console.error(e);
alert('Dashboard PDF export failed.');
} finally {
if (btn) {
btn.disabled = false;
if (btnLabel) btnLabel.textContent = oldLabel || 'Export PDF';
}
}
}
async function mountWidget(w) {
const mount = document.getElementById('cb-w-' + w.id);
if (!mount) return;
@ -277,6 +528,14 @@
const actions = editLink + rm;
if (w.widget_type === 'chart') {
const exportPng = '<button type="button" class="cb-dash-widget-export cb-dash-export-png" data-widget-id="' + w.id + '" title="Export PNG" aria-label="Export PNG"><span class="material-symbols-outlined" style="font-size:18px;">image</span></button>';
const exportPdf = '<button type="button" class="cb-dash-widget-export cb-dash-export-pdf" data-widget-id="' + w.id + '" title="Export PDF" aria-label="Export PDF"><span class="material-symbols-outlined" style="font-size:18px;">picture_as_pdf</span></button>';
const canExcel = !READ_ONLY && boot.urls && boot.urls.chartExportBase && w.chart_id;
const excelHref = canExcel ? (boot.urls.chartExportBase + w.chart_id + '/export?format=excel') : '';
const exportExcel = canExcel
? '<a class="cb-dash-widget-export" href="' + escapeHtml(excelHref) + '" title="Export Excel" aria-label="Export Excel" target="_blank" rel="noopener noreferrer"><span class="material-symbols-outlined" style="font-size:18px;">table_view</span></a>'
: '';
const chartActions = exportPng + exportPdf + exportExcel + actions;
const vars = await loadVariableDefs(w.saved_query_id);
let varsHtml = '';
if (vars.length) {
@ -284,7 +543,7 @@
}
mount.innerHTML =
'<div class="cb-dash-widget-card h-100">' +
'<div class="cb-dash-widget-head"><span class="text-truncate">' + escapeHtml(title) + '</span><div class="cb-dash-widget-head-actions">' + actions + '</div></div>' +
'<div class="cb-dash-widget-head"><span class="text-truncate">' + escapeHtml(title) + '</span><div class="cb-dash-widget-head-actions">' + chartActions + '</div></div>' +
'<div class="cb-dash-widget-body">' +
varsHtml +
'<div class="cb-dash-chart-inner position-relative">' +
@ -316,6 +575,8 @@
});
await loadChartData(w);
mount.querySelector('.cb-dash-export-png')?.addEventListener('click', () => exportChartPng(w));
mount.querySelector('.cb-dash-export-pdf')?.addEventListener('click', () => exportChartPdf(w));
return;
}
@ -465,6 +726,8 @@
document.getElementById('cbBtnToggleEdit')?.addEventListener('click', () => { if (!READ_ONLY) toggleEdit(); });
document.getElementById('cbBtnExportDashPdf')?.addEventListener('click', () => { exportDashboardPdf(); });
document.getElementById('cbBtnSaveLayout')?.addEventListener('click', async () => {
if (READ_ONLY || !boot.urls || !boot.urls.saveLayout) return;
if (!grid) return;

View File

@ -38,6 +38,14 @@ $themeClass = match ($theme) {
<button type="button" class="btn btn-sm btn-outline-secondary" id="cbBtnFs" title="Fullscreen">
<span class="material-symbols-outlined cb-btn-icon">fullscreen</span>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="cbBtnExportDashPdf" title="Export dashboard PDF (A4)">
<span class="material-symbols-outlined cb-btn-icon">picture_as_pdf</span>
<span>Export PDF</span>
</button>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" role="switch" id="cbChkDashPdfPageBreaks" checked>
<label class="form-check-label small" for="cbChkDashPdfPageBreaks">Page breaks</label>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#modalShare" data-share-type="dashboard" data-resource-id="<?= $did ?>">Share</button>
<a href="<?= base_url('dashboard/settings/' . $did) ?>" class="btn btn-sm btn-outline-secondary">Settings</a>
<a href="<?= base_url('dashboard') ?>" class="btn btn-sm btn-light border">All boards</a>
@ -150,5 +158,7 @@ $themeClass = match ($theme) {
<script src="https://cdn.jsdelivr.net/npm/gridstack@10.3.1/dist/gridstack-all.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
<?= view('dashboard/_view_scripts') ?>
<?= $this->endSection() ?>

View File

@ -20,9 +20,19 @@ $themeClass = match ($theme) {
<h1><?= esc($dashboard['name']) ?></h1>
<div class="cb-dash-meta"><?= esc($dashboard['description'] ?? '') ?></div>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary d-none" id="cbBtnFs" title="Fullscreen">
<span class="material-symbols-outlined" style="font-size:18px;">fullscreen</span>
</button>
<div class="d-flex flex-wrap align-items-center gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="cbBtnExportDashPdf" title="Export dashboard PDF (A4)">
<span class="material-symbols-outlined cb-btn-icon">picture_as_pdf</span>
<span>Export PDF</span>
</button>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" role="switch" id="cbChkDashPdfPageBreaks" checked>
<label class="form-check-label small" for="cbChkDashPdfPageBreaks">Page breaks</label>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary d-none" id="cbBtnFs" title="Fullscreen">
<span class="material-symbols-outlined" style="font-size:18px;">fullscreen</span>
</button>
</div>
</div>
<div class="cb-dash-grid-wrap">
@ -60,5 +70,7 @@ $themeClass = match ($theme) {
<script src="https://cdn.jsdelivr.net/npm/gridstack@10.3.1/dist/gridstack-all.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
<?= view('dashboard/_view_scripts') ?>
<?= $this->endSection() ?>

View File

@ -176,8 +176,8 @@
flex-direction: column;
}
.cb-cb-main-row > .col-lg-5,
.cb-cb-main-row > .col-lg-7 {
.cb-cb-main-row > .col-lg-4,
.cb-cb-main-row > .col-lg-8 {
display: flex;
}
@ -360,12 +360,34 @@
}
.cb-cb-color-swatch {
width: 2.25rem;
height: 2.25rem;
padding: 0.15rem;
border-radius: 0.35rem;
border: 1px solid var(--cb-cb-border);
width: 100%;
height: 2rem;
padding: 0.12rem;
border-radius: 999px;
border: 1px solid #d6deea;
cursor: pointer;
background: #fff;
}
.cb-cb-color-swatch::-webkit-color-swatch-wrapper {
padding: 0;
border-radius: 999px;
}
.cb-cb-color-swatch::-webkit-color-swatch {
border: none;
border-radius: 999px;
}
.cb-cb-color-swatch::-moz-color-swatch {
border: none;
border-radius: 999px;
}
.cb-cb-swatch-grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 8px;
}
@media (max-width: 768px) {
@ -388,7 +410,8 @@
}
.cb-cb-main-row > .col-lg-5,
.cb-cb-main-row > .col-lg-7 {
.cb-cb-main-row > .col-lg-4,
.cb-cb-main-row > .col-lg-8 {
display: block;
}

View File

@ -374,6 +374,24 @@
color: #dc2626;
}
.cb-dash-widget-export {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: #94a3b8;
padding: 2px;
line-height: 1;
border-radius: 6px;
text-decoration: none;
}
.cb-dash-widget-export:hover {
background: #eef2ff;
color: #4f46e5;
}
.cb-dash-prose {
font-size: 14px;
line-height: 1.65;