(function() {
function csrfHeaders() {
var m = document.cookie.match(/(?:^|;\s*)umbral_csrf_token=([^;]*)/);
return m ? { 'X-CSRF-Token': decodeURIComponent(m[1]) } : {};
}
function applyTheme(dark) {
var root = document.documentElement;
var themeName = dark ? 'dark' : 'light';
if (dark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
root.setAttribute('data-theme', themeName);
var light = document.getElementById('theme-icon-light');
var moon = document.getElementById('theme-icon-dark');
if (dark) {
if (light) light.classList.remove('hidden');
if (moon) moon.classList.add('hidden');
} else {
if (light) light.classList.add('hidden');
if (moon) moon.classList.remove('hidden');
}
}
var serverTheme = document.documentElement.getAttribute('data-theme');
var storedTheme = localStorage.getItem('umbral-admin-theme');
var resolvedTheme = storedTheme || serverTheme || 'dark';
var isDark = resolvedTheme !== 'light';
applyTheme(isDark);
function toggleTheme() {
isDark = !isDark;
var themeName = isDark ? 'dark' : 'light';
localStorage.setItem('umbral-admin-theme', themeName);
applyTheme(isDark);
queueChartRefresh();
fetch(umbralAdminBase + '/api/prefs', {
method: 'PUT',
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()),
body: JSON.stringify({ theme: themeName })
}).catch(function() { });
}
var sidebarCollapsed = localStorage.getItem('umbral-admin-sidebar') === 'collapsed';
var sidebarMobileOpen = false;
function isMobileSidebar() {
return window.matchMedia && window.matchMedia('(max-width: 767px)').matches;
}
function applySidebarState() {
var body = document.body;
if (!body) return;
var mobile = isMobileSidebar();
body.classList.toggle('sidebar-collapsed', !mobile && sidebarCollapsed);
body.classList.toggle('sidebar-open', mobile && sidebarMobileOpen);
var toggle = document.getElementById('sidebar-toggle');
if (toggle) {
toggle.setAttribute('aria-expanded', mobile ? String(sidebarMobileOpen) : String(!sidebarCollapsed));
toggle.setAttribute('aria-label', mobile
? (sidebarMobileOpen ? 'Close navigation' : 'Open navigation')
: (sidebarCollapsed ? 'Expand navigation' : 'Collapse navigation'));
}
if (mobile || !sidebarCollapsed) hideSidebarTooltip();
}
function persistSidebarCollapsed() {
fetch(umbralAdminBase + '/api/prefs', {
method: 'PUT',
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()),
body: JSON.stringify({ sidebar_collapsed: sidebarCollapsed })
}).catch(function() { });
}
function toggleSidebar() {
if (isMobileSidebar()) {
sidebarMobileOpen = !sidebarMobileOpen;
} else {
sidebarCollapsed = !sidebarCollapsed;
localStorage.setItem('umbral-admin-sidebar', sidebarCollapsed ? 'collapsed' : 'expanded');
persistSidebarCollapsed();
}
applySidebarState();
queueChartRefresh();
}
function closeSidebar() {
if (isMobileSidebar()) {
sidebarMobileOpen = false;
} else {
sidebarCollapsed = true;
localStorage.setItem('umbral-admin-sidebar', 'collapsed');
persistSidebarCollapsed();
}
applySidebarState();
queueChartRefresh();
}
applySidebarState();
window.addEventListener('resize', applySidebarState);
function hideSidebarTooltip() {
var tooltip = document.getElementById('umbral-sidebar-tooltip');
if (!tooltip) return;
tooltip.removeAttribute('data-open');
tooltip.setAttribute('aria-hidden', 'true');
tooltip.textContent = '';
}
function showSidebarTooltip(trigger) {
if (isMobileSidebar() || !document.body.classList.contains('sidebar-collapsed')) return;
var text = trigger.getAttribute('data-sidebar-tooltip') || '';
var tooltip = document.getElementById('umbral-sidebar-tooltip');
if (!text || !tooltip) return;
tooltip.textContent = text;
tooltip.setAttribute('data-open', 'true');
tooltip.setAttribute('aria-hidden', 'false');
var rect = trigger.getBoundingClientRect();
var y = rect.top + rect.height / 2;
tooltip.style.transform = 'translateY(-50%)';
tooltip.style.top = Math.max(18, Math.min(window.innerHeight - 18, y)) + 'px';
}
function initSidebarTooltips(root) {
root = root || document;
root.querySelectorAll('[data-sidebar-tooltip]:not([data-sidebar-tooltip-init])').forEach(function(el) {
el.setAttribute('data-sidebar-tooltip-init', '1');
el.addEventListener('mouseenter', function() { showSidebarTooltip(el); });
el.addEventListener('focus', function() { showSidebarTooltip(el); });
el.addEventListener('mouseleave', hideSidebarTooltip);
el.addEventListener('blur', hideSidebarTooltip);
});
}
initSidebarTooltips();
function sidebarFilter(query) {
var q = query.trim().toLowerCase();
var links = document.querySelectorAll('.sidebar-model-link');
links.forEach(function(link) {
var name = (link.getAttribute('data-model-name') || '').toLowerCase();
var visible = q === '' || name.indexOf(q) !== -1;
link.style.display = visible ? '' : 'none';
});
document.querySelectorAll('.sidebar-plugin-group').forEach(function(group) {
var anyVisible = Array.from(group.querySelectorAll('.sidebar-model-link'))
.some(function(l) { return l.style.display !== 'none'; });
group.style.display = anyVisible ? '' : 'none';
});
}
function token(name, fallback) {
var styles = getComputedStyle(document.documentElement);
var value = styles.getPropertyValue(name);
return value ? value.trim() : fallback;
}
function chartIsDark() {
return document.documentElement.getAttribute('data-theme') !== 'light';
}
function readBarChart(el) {
var labels = [];
var values = [];
el.querySelectorAll('[data-chart-point]').forEach(function(point) {
var label = point.getAttribute('data-label') || '';
var value = Number(point.getAttribute('data-value') || 0);
labels.push(label);
values.push(Number.isFinite(value) ? value : 0);
});
return {
labels: labels,
values: values,
seriesName: el.getAttribute('data-chart-series') || 'entries',
};
}
function barChartOptions(el) {
var data = readBarChart(el);
if (data.labels.length === 0) return null;
var maxValue = Math.max.apply(null, data.values.concat([1]));
var foreground = token('--on-surface', 'rgb(229 231 235)');
var onPrimary = token('--on-primary', 'rgb(17 24 39)');
var muted = token('--outline', 'rgb(148 163 184)');
var grid = token('--outline-variant', 'rgb(51 65 85)');
return {
chart: {
type: 'bar',
height: '100%',
width: '100%',
background: 'transparent',
fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif',
foreColor: muted,
parentHeightOffset: 0,
toolbar: { show: false },
zoom: { enabled: false },
animations: {
enabled: true,
speed: 320,
animateGradually: { enabled: false },
dynamicAnimation: { enabled: true, speed: 220 },
},
},
series: [{
name: data.seriesName,
data: data.values,
}],
colors: [token('--primary', 'rgb(79 70 229)')],
plotOptions: {
bar: {
horizontal: true,
borderRadius: 6,
borderRadiusApplication: 'end',
barHeight: '58%',
},
},
dataLabels: {
enabled: true,
formatter: function(value) { return Math.round(value); },
style: {
colors: [onPrimary],
fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif',
fontSize: '11px',
fontWeight: 600,
},
background: {
enabled: false,
},
},
grid: {
borderColor: grid,
strokeDashArray: 3,
padding: { top: 0, right: 8, bottom: 0, left: 4 },
},
xaxis: {
categories: data.labels,
min: 0,
max: maxValue,
tickAmount: Math.min(4, Math.max(1, maxValue)),
labels: {
formatter: function(value) { return Math.round(value); },
style: {
colors: muted,
fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif',
fontSize: '11px',
},
},
axisBorder: { show: false },
axisTicks: { show: false },
},
yaxis: {
labels: {
align: 'left',
minWidth: 0,
maxWidth: 124,
style: {
colors: foreground,
fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif',
fontSize: '12px',
fontWeight: 500,
},
},
},
tooltip: {
theme: chartIsDark() ? 'dark' : 'light',
y: {
formatter: function(value) {
var count = Math.round(value);
return count + (count === 1 ? ' model' : ' models');
},
},
},
states: {
hover: { filter: { type: 'lighten', value: 0.04 } },
active: { filter: { type: 'none' } },
},
};
}
function sparklineChartOptions(el) {
var values = [];
el.querySelectorAll('[data-chart-point]').forEach(function(point) {
var v = Number(point.getAttribute('data-value') || 0);
values.push(Number.isFinite(v) ? v : 0);
});
if (values.length === 0) return null;
var color = el.getAttribute('data-spark-color') || token('--primary', 'rgb(99 102 241)');
return {
chart: {
type: 'area',
height: 48,
width: '100%',
background: 'transparent',
sparkline: { enabled: true },
animations: {
enabled: true,
speed: 380,
animateGradually: { enabled: false },
dynamicAnimation: { enabled: true, speed: 220 },
},
},
series: [{ name: el.getAttribute('data-chart-series') || 'series', data: values }],
colors: [color],
stroke: { curve: 'smooth', width: 2 },
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.35,
opacityTo: 0,
stops: [0, 100],
},
},
tooltip: {
enabled: true,
theme: chartIsDark() ? 'dark' : 'light',
fixed: { enabled: false },
x: { show: false },
y: { formatter: function(v) { return Math.round(v); } },
marker: { show: false },
},
};
}
function lineChartOptions(el) {
var labels = [];
var labelsSeen = Object.create(null);
var seriesOrder = [];
var seriesMap = Object.create(null);
el.querySelectorAll('[data-chart-point]').forEach(function(point) {
var name = point.getAttribute('data-series') || 'series';
var x = point.getAttribute('data-x') || '';
var y = Number(point.getAttribute('data-y') || 0);
var yClean = Number.isFinite(y) ? y : 0;
if (!seriesMap[name]) {
seriesMap[name] = [];
seriesOrder.push(name);
}
seriesMap[name].push(yClean);
if (!labelsSeen[x]) {
labelsSeen[x] = true;
labels.push(x);
}
});
if (seriesOrder.length === 0) return null;
var series = seriesOrder.map(function(name) {
return { name: name, data: seriesMap[name] };
});
var primary = token('--primary', 'rgb(99 102 241)');
var muted = token('--outline', 'rgb(148 163 184)');
var grid = token('--outline-variant', 'rgb(51 65 85)');
var palette = [primary, '#34d399', '#60a5fa', '#fbbf24', '#f472b6'];
var isSingleSeries = series.length === 1;
return {
chart: {
type: 'area',
height: '100%',
width: '100%',
background: 'transparent',
toolbar: { show: false },
zoom: { enabled: false },
parentHeightOffset: 0,
fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif',
foreColor: muted,
animations: {
enabled: true,
speed: 380,
animateGradually: { enabled: false },
dynamicAnimation: { enabled: true, speed: 220 },
},
},
series: series,
colors: palette,
stroke: { curve: 'smooth', width: 2 },
fill: isSingleSeries
? {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.30,
opacityTo: 0,
stops: [0, 100],
},
}
: { type: 'solid', opacity: 0.06 },
dataLabels: { enabled: false },
legend: {
show: !isSingleSeries,
position: 'top',
horizontalAlign: 'right',
labels: { colors: muted },
markers: { width: 8, height: 8, radius: 4 },
itemMargin: { horizontal: 8 },
},
grid: {
borderColor: grid,
strokeDashArray: 3,
padding: { top: 0, right: 8, bottom: 0, left: 4 },
},
markers: { size: 0, hover: { size: 4 } },
xaxis: {
categories: labels,
labels: {
style: { colors: muted, fontSize: '11px' },
rotate: 0,
hideOverlappingLabels: true,
},
axisBorder: { show: false },
axisTicks: { show: false },
},
yaxis: {
labels: {
style: { colors: muted, fontSize: '11px' },
formatter: function(v) { return Math.round(v); },
},
},
tooltip: {
theme: chartIsDark() ? 'dark' : 'light',
y: { formatter: function(v) { return Math.round(v); } },
},
};
}
function donutChartOptions(el) {
var labels = [];
var values = [];
var explicitColors = [];
var hasAnyColor = false;
el.querySelectorAll('[data-chart-slice]').forEach(function(slice) {
labels.push(slice.getAttribute('data-label') || '');
var v = Number(slice.getAttribute('data-value') || 0);
values.push(Number.isFinite(v) ? v : 0);
var c = slice.getAttribute('data-color');
explicitColors.push(c || null);
if (c) hasAnyColor = true;
});
if (values.length === 0) return null;
var muted = token('--outline', 'rgb(148 163 184)');
var onSurface = token('--on-surface', 'rgb(229 231 235)');
var defaultPalette = ['#34d399', '#60a5fa', '#fbbf24', '#f472b6', '#a78bfa', '#22d3ee'];
var colors = hasAnyColor
? explicitColors.map(function(c, i) { return c || defaultPalette[i % defaultPalette.length]; })
: defaultPalette;
return {
chart: {
type: 'donut',
height: '100%',
width: '100%',
background: 'transparent',
fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif',
foreColor: muted,
parentHeightOffset: 0,
animations: {
enabled: true,
speed: 380,
animateGradually: { enabled: false },
dynamicAnimation: { enabled: true, speed: 220 },
},
},
series: values,
labels: labels,
colors: colors,
stroke: { width: 0 },
dataLabels: { enabled: false },
legend: {
position: 'right',
labels: { colors: muted },
markers: { width: 8, height: 8, radius: 4 },
itemMargin: { vertical: 4 },
},
plotOptions: {
pie: {
donut: {
size: '70%',
labels: {
show: true,
name: {
show: true,
fontSize: '11px',
color: muted,
offsetY: -4,
},
value: {
show: true,
fontSize: '20px',
fontWeight: 600,
color: onSurface,
offsetY: 6,
formatter: function(v) { return Math.round(Number(v)); },
},
total: {
show: true,
label: 'Total',
color: muted,
fontSize: '11px',
formatter: function(w) {
return Math.round(
w.globals.seriesTotals.reduce(function(a, b) { return a + b; }, 0)
);
},
},
},
},
},
},
tooltip: {
theme: chartIsDark() ? 'dark' : 'light',
y: { formatter: function(v) { return Math.round(v); } },
},
responsive: [{
breakpoint: 480,
options: {
legend: { position: 'bottom' },
},
}],
};
}
function radialChartOptions(el) {
var labels = [];
var values = [];
var explicitColors = [];
var hasAnyColor = false;
el.querySelectorAll('[data-chart-track]').forEach(function(track) {
labels.push(track.getAttribute('data-label') || '');
var v = Number(track.getAttribute('data-value') || 0);
values.push(Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 0);
var c = track.getAttribute('data-color');
explicitColors.push(c || null);
if (c) hasAnyColor = true;
});
if (values.length === 0) return null;
var muted = token('--outline', 'rgb(148 163 184)');
var onSurface = token('--on-surface', 'rgb(229 231 235)');
var defaultPalette = ['#34d399', '#60a5fa', '#fbbf24', '#f472b6', '#a78bfa', '#22d3ee'];
var colors = hasAnyColor
? explicitColors.map(function(c, i) { return c || defaultPalette[i % defaultPalette.length]; })
: defaultPalette;
var single = values.length === 1;
return {
chart: {
type: 'radialBar',
height: '100%',
width: '100%',
background: 'transparent',
fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif',
foreColor: muted,
parentHeightOffset: 0,
animations: {
enabled: true,
speed: 380,
animateGradually: { enabled: false },
dynamicAnimation: { enabled: true, speed: 220 },
},
},
series: values,
labels: labels,
colors: colors,
stroke: { lineCap: 'round' },
plotOptions: {
radialBar: {
hollow: { size: single ? '60%' : '38%' },
track: {
background: token('--surface-container-high', 'rgba(148,163,184,0.15)'),
strokeWidth: '100%',
margin: 4,
},
dataLabels: {
name: {
show: true,
fontSize: '11px',
color: muted,
offsetY: single ? -8 : 0,
},
value: {
show: true,
fontSize: single ? '22px' : '14px',
fontWeight: 600,
color: onSurface,
offsetY: single ? 4 : 0,
formatter: function(v) { return Math.round(Number(v)) + '%'; },
},
total: single ? undefined : {
show: true,
label: 'Avg',
color: muted,
fontSize: '11px',
formatter: function(w) {
var s = w.globals.series;
if (!s.length) return '0%';
var sum = s.reduce(function(a, b) { return a + b; }, 0);
return Math.round(sum / s.length) + '%';
},
},
},
},
},
legend: single ? { show: false } : {
show: true,
position: 'bottom',
labels: { colors: muted },
markers: { width: 8, height: 8, radius: 4 },
itemMargin: { vertical: 2 },
},
tooltip: { enabled: false },
};
}
function heatmapChartOptions(el) {
var order = [];
var byRow = {};
el.querySelectorAll('[data-chart-cell]').forEach(function(cell) {
var name = cell.getAttribute('data-row') || '';
if (!byRow[name]) { byRow[name] = []; order.push(name); }
var y = Number(cell.getAttribute('data-y') || 0);
byRow[name].push({
x: cell.getAttribute('data-x') || '',
y: Number.isFinite(y) ? y : 0,
});
});
if (order.length === 0) return null;
var series = order.slice().reverse().map(function(name) {
return { name: name, data: byRow[name] };
});
var muted = token('--outline', 'rgb(148 163 184)');
return {
chart: {
type: 'heatmap',
height: '100%',
width: '100%',
background: 'transparent',
fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif',
foreColor: muted,
parentHeightOffset: 0,
toolbar: { show: false },
animations: { enabled: true, speed: 320 },
},
series: series,
dataLabels: { enabled: false },
stroke: { width: 2, colors: ['transparent'] },
colors: ['#60a5fa'],
plotOptions: {
heatmap: { radius: 4, enableShades: true, shadeIntensity: 0.6 },
},
xaxis: {
type: 'category',
labels: { style: { colors: muted, fontSize: '10px' } },
axisBorder: { show: false },
axisTicks: { show: false },
},
yaxis: { labels: { style: { colors: muted, fontSize: '10px' } } },
grid: { borderColor: 'transparent', padding: { top: 0, right: 0 } },
tooltip: {
theme: chartIsDark() ? 'dark' : 'light',
y: { formatter: function(v) { return Math.round(v); } },
},
legend: { show: false },
};
}
function initCharts(root) {
root = root || document;
root.querySelectorAll('[data-umbral-chart="bar"]').forEach(function(el) {
var canvas = el.querySelector('[data-chart-canvas]');
var fallback = el.querySelector('[data-chart-unavailable]');
if (!canvas) return;
if (!window.ApexCharts) {
if (fallback) fallback.classList.remove('hidden');
return;
}
if (fallback) fallback.classList.add('hidden');
var options = barChartOptions(el);
if (!options) return;
if (canvas._umbralApexChart) {
canvas._umbralApexChart.updateOptions(options, false, true);
return;
}
canvas._umbralApexChart = new ApexCharts(canvas, options);
canvas._umbralApexChart.render();
});
root.querySelectorAll('[data-umbral-chart="sparkline"]').forEach(function(el) {
var canvas = el.querySelector('[data-chart-canvas]');
var fallback = el.querySelector('[data-chart-unavailable]');
if (!canvas) return;
if (!window.ApexCharts) {
if (fallback) fallback.classList.remove('hidden');
return;
}
if (fallback) fallback.classList.add('hidden');
var options = sparklineChartOptions(el);
if (!options) return;
if (canvas._umbralApexChart) {
canvas._umbralApexChart.updateOptions(options, false, true);
return;
}
canvas._umbralApexChart = new ApexCharts(canvas, options);
canvas._umbralApexChart.render();
});
root.querySelectorAll('[data-umbral-chart="line"]').forEach(function(el) {
var canvas = el.querySelector('[data-chart-canvas]');
var fallback = el.querySelector('[data-chart-unavailable]');
if (!canvas) return;
if (!window.ApexCharts) {
if (fallback) fallback.classList.remove('hidden');
return;
}
if (fallback) fallback.classList.add('hidden');
var options = lineChartOptions(el);
if (!options) return;
if (canvas._umbralApexChart) {
canvas._umbralApexChart.updateOptions(options, false, true);
return;
}
canvas._umbralApexChart = new ApexCharts(canvas, options);
canvas._umbralApexChart.render();
});
root.querySelectorAll('[data-umbral-chart="donut"]').forEach(function(el) {
var canvas = el.querySelector('[data-chart-canvas]');
var fallback = el.querySelector('[data-chart-unavailable]');
if (!canvas) return;
if (!window.ApexCharts) {
if (fallback) fallback.classList.remove('hidden');
return;
}
if (fallback) fallback.classList.add('hidden');
var options = donutChartOptions(el);
if (!options) return;
if (canvas._umbralApexChart) {
canvas._umbralApexChart.updateOptions(options, false, true);
return;
}
canvas._umbralApexChart = new ApexCharts(canvas, options);
canvas._umbralApexChart.render();
});
root.querySelectorAll('[data-umbral-chart="radial"]').forEach(function(el) {
var canvas = el.querySelector('[data-chart-canvas]');
var fallback = el.querySelector('[data-chart-unavailable]');
if (!canvas) return;
if (!window.ApexCharts) {
if (fallback) fallback.classList.remove('hidden');
return;
}
if (fallback) fallback.classList.add('hidden');
var options = radialChartOptions(el);
if (!options) return;
if (canvas._umbralApexChart) {
canvas._umbralApexChart.updateOptions(options, false, true);
return;
}
canvas._umbralApexChart = new ApexCharts(canvas, options);
canvas._umbralApexChart.render();
});
root.querySelectorAll('[data-umbral-chart="heatmap"]').forEach(function(el) {
var canvas = el.querySelector('[data-chart-canvas]');
var fallback = el.querySelector('[data-chart-unavailable]');
if (!canvas) return;
if (!window.ApexCharts) {
if (fallback) fallback.classList.remove('hidden');
return;
}
if (fallback) fallback.classList.add('hidden');
var options = heatmapChartOptions(el);
if (!options) return;
if (canvas._umbralApexChart) {
canvas._umbralApexChart.updateOptions(options, false, true);
return;
}
canvas._umbralApexChart = new ApexCharts(canvas, options);
canvas._umbralApexChart.render();
});
}
function queueChartRefresh() {
window.setTimeout(function() {
initCharts(document);
}, 220);
}
function toggleUserMenu() {
}
Object.assign(window.umbral, {
csrfHeaders: csrfHeaders,
toggleTheme: toggleTheme,
toggleSidebar: toggleSidebar,
closeSidebar: closeSidebar,
sidebarFilter: sidebarFilter,
initCharts: initCharts,
refreshCharts: queueChartRefresh,
toggleUserMenu: toggleUserMenu,
});
})();
document.addEventListener('DOMContentLoaded', function() {
if (window.lucide) lucide.createIcons();
if (window.umbral && umbral.initCharts) umbral.initCharts(document);
});
document.body.addEventListener('htmx:afterSwap', function(e) {
if (window.lucide) lucide.createIcons({ el: e.target });
if (window.umbral && umbral.initCharts) umbral.initCharts(e.target);
});
(function() {
window.umbral = window.umbral || {};
umbral.showToast = function(message, level) {
level = level || 'info';
var container = document.getElementById('umbral-toast-container');
if (!container) return;
var colors = {
info: 'bg-surface-container border-outline-variant text-on-surface',
success: 'bg-primary-container/20 border-primary/30 text-primary',
warning: 'bg-surface-container border-outline text-on-surface-variant',
error: 'bg-error-container/20 border-error/30 text-error'
};
var icons = { info: 'info', success: 'check-circle', warning: 'alert-triangle', error: 'alert-circle' };
var toast = document.createElement('div');
toast.className = 'pointer-events-auto flex items-center gap-sm px-lg py-sm rounded-xl border shadow-lg font-label-md text-label-md transition-all duration-300 ' + (colors[level] || colors.info);
toast.innerHTML = '<i data-lucide="' + (icons[level]||'info') + '" class="w-4 h-4 flex-shrink-0"></i><span>' + message + '</span>';
container.appendChild(toast);
if (window.lucide) lucide.createIcons({ el: toast });
setTimeout(function() {
toast.style.opacity = '0';
toast.style.transform = 'translateX(20px)';
setTimeout(function() { toast.remove(); }, 300);
}, 4000);
};
document.body.addEventListener('htmx:responseError', function(e) {
var status = e.detail && e.detail.xhr ? e.detail.xhr.status : 0;
if (status === 400 || status === 409 || status === 422) {
return;
}
umbral.showToast('Server error', 'error');
});
document.body.addEventListener('showToast', function(e) {
if (e.detail) umbral.showToast(e.detail.message, e.detail.level);
});
function initMultiChoicePickers(root) {
root = root || document;
root.querySelectorAll('.multichoice-picker:not([data-mc-init])').forEach(function(picker) {
picker.setAttribute('data-mc-init', '1');
var hidden = picker.querySelector('input[type=hidden]');
if (!hidden) return;
function sync() {
var values = [];
picker.querySelectorAll('input[type=checkbox][data-mc-value]').forEach(function(cb) {
if (cb.checked) values.push(cb.getAttribute('data-mc-value'));
});
hidden.value = values.join(',');
picker.querySelectorAll('input[type=checkbox][data-mc-value]').forEach(function(cb) {
var label = cb.closest('label');
if (!label) return;
if (cb.checked) {
label.classList.add('border-primary', 'text-primary', 'bg-primary/5');
label.classList.remove('border-outline-variant', 'text-on-surface-variant');
} else {
label.classList.remove('border-primary', 'text-primary', 'bg-primary/5');
label.classList.add('border-outline-variant', 'text-on-surface-variant');
}
});
}
picker.querySelectorAll('input[type=checkbox][data-mc-value]').forEach(function(cb) {
cb.addEventListener('change', sync);
});
});
}
umbral.initMultiChoicePickers = initMultiChoicePickers;
initMultiChoicePickers();
document.body.addEventListener('htmx:afterSwap', function() { initMultiChoicePickers(); });
function initFkPickers(root) {
root = root || document;
root.querySelectorAll('.fk-picker:not([data-fk-init])').forEach(function(picker) {
picker.setAttribute('data-fk-init', '1');
var textInput = picker.querySelector('input[type=text]');
var hiddenInput = picker.querySelector('input[type=hidden]');
var dropdown = picker.querySelector('.fk-options');
if (!textInput || !hiddenInput || !dropdown) return;
function setSelection(value, label) {
hiddenInput.value = value || '';
var active = picker.querySelector('[data-fk-active]');
var activeLabel = picker.querySelector('[data-fk-active-label]');
var activeValue = picker.querySelector('[data-fk-active-value]');
if (active) active.classList.toggle('hidden', !value);
if (activeLabel) activeLabel.textContent = label || 'Selected option';
if (activeValue) activeValue.textContent = value ? '#' + value : '';
}
textInput.addEventListener('focus', function() { dropdown.classList.remove('hidden'); });
textInput.addEventListener('input', function() {
if (textInput.value.trim()) dropdown.classList.remove('hidden');
});
document.addEventListener('click', function(e) {
if (!picker.contains(e.target)) dropdown.classList.add('hidden');
});
picker.querySelectorAll('[data-fk-clear]').forEach(function(btn) {
btn.addEventListener('click', function() {
setSelection('', '');
textInput.value = '';
textInput.focus();
});
});
picker.addEventListener('htmx:afterSwap', function() {
dropdown.classList.remove('hidden');
dropdown.querySelectorAll('[data-fk-value]').forEach(function(opt) {
opt.addEventListener('mousedown', function(e) {
e.preventDefault();
var value = opt.getAttribute('data-fk-value') || '';
var label = opt.getAttribute('data-fk-label') || opt.textContent.trim();
setSelection(value, label);
textInput.value = '';
dropdown.classList.add('hidden');
});
});
if (window.lucide) lucide.createIcons({ el: dropdown });
});
});
}
umbral.fkResolve = function(field, event) {
try {
var data = JSON.parse(event.detail.xhr.responseText);
if (data.items && data.items[0]) {
var source = event.target || (event.detail && event.detail.elt);
var picker = source && source.closest ? source.closest('.fk-picker') : null;
if (!picker) {
var el = document.getElementById('fk_text_' + field);
picker = el && el.closest ? el.closest('.fk-picker') : null;
}
if (picker) {
var item = data.items[0];
var hidden = picker.querySelector('input[type=hidden]');
var active = picker.querySelector('[data-fk-active]');
var activeLabel = picker.querySelector('[data-fk-active-label]');
var activeValue = picker.querySelector('[data-fk-active-value]');
if (hidden) hidden.value = String(item.value);
if (active) active.classList.remove('hidden');
if (activeLabel) activeLabel.textContent = item.label;
if (activeValue) activeValue.textContent = '#' + item.value;
}
}
} catch(e) {}
};
initFkPickers();
document.body.addEventListener('htmx:afterSwap', function() { initFkPickers(); });
function initM2MPickers(root) {
root = root || document;
root.querySelectorAll('.m2m-field-picker:not([data-m2m-init])').forEach(function(picker) {
picker.setAttribute('data-m2m-init', '1');
var search = picker.querySelector('[data-m2m-search]');
var selected = picker.querySelector('[data-m2m-selected]');
var selectedEmpty = picker.querySelector('[data-m2m-selected-empty]');
var count = picker.querySelector('[data-m2m-count]');
var pageLabel = picker.querySelector('[data-m2m-page]');
var prev = picker.querySelector('[data-m2m-prev]');
var next = picker.querySelector('[data-m2m-next]');
var empty = picker.querySelector('[data-m2m-empty]');
var options = Array.from(picker.querySelectorAll('[data-m2m-option]'));
var page = 1;
var pageSize = parseInt(picker.getAttribute('data-page-size') || '12', 10);
function labelFor(option) {
return (option.getAttribute('data-label') || option.textContent || '').trim();
}
function checkedOptions() {
return options.filter(function(option) {
var cb = option.querySelector('input[type=checkbox]');
return cb && cb.checked;
});
}
function renderSelected() {
if (!selected) return;
selected.innerHTML = '';
var checked = checkedOptions();
if (count) count.textContent = checked.length + ' selected';
if (selectedEmpty) selectedEmpty.classList.toggle('hidden', checked.length > 0);
checked.slice(0, 8).forEach(function(option) {
var cb = option.querySelector('input[type=checkbox]');
var chip = document.createElement('button');
chip.type = 'button';
chip.className = 'inline-flex items-center gap-xs rounded-full border border-primary/25 bg-primary-container px-sm py-xs text-label-sm text-on-primary-container';
var label = document.createElement('span');
label.className = 'max-w-[160px] truncate';
label.textContent = labelFor(option);
var remove = document.createElement('span');
remove.setAttribute('aria-hidden', 'true');
remove.textContent = '×';
chip.appendChild(label);
chip.appendChild(remove);
chip.addEventListener('click', function() {
cb.checked = false;
render();
});
selected.appendChild(chip);
});
if (checked.length > 8) {
var more = document.createElement('span');
more.className = 'text-label-sm text-outline px-xs py-xs';
more.textContent = '+' + (checked.length - 8) + ' more';
selected.appendChild(more);
}
}
function render() {
var q = search ? search.value.trim().toLowerCase() : '';
var matches = options.filter(function(option) {
return !q || labelFor(option).toLowerCase().indexOf(q) !== -1;
});
var pages = Math.max(1, Math.ceil(matches.length / pageSize));
if (page > pages) page = pages;
var start = (page - 1) * pageSize;
var visible = new Set(matches.slice(start, start + pageSize));
options.forEach(function(option) {
option.classList.toggle('hidden', !visible.has(option));
});
if (empty) empty.classList.toggle('hidden', matches.length > 0);
if (pageLabel) pageLabel.textContent = 'Page ' + page + ' of ' + pages;
if (prev) prev.disabled = page <= 1;
if (next) next.disabled = page >= pages;
renderSelected();
}
if (search) {
search.addEventListener('input', function() {
page = 1;
render();
});
}
if (prev) prev.addEventListener('click', function() { page = Math.max(1, page - 1); render(); });
if (next) next.addEventListener('click', function() { page = page + 1; render(); });
options.forEach(function(option) {
var cb = option.querySelector('input[type=checkbox]');
if (cb) cb.addEventListener('change', render);
});
render();
});
}
umbral.initM2MPickers = initM2MPickers;
initM2MPickers();
document.body.addEventListener('htmx:afterSwap', function() { initM2MPickers(); });
})();
(function() {
window.umbral = window.umbral || {};
var MD_CSS = 'https://unpkg.com/easymde@2.18.0/dist/easymde.min.css';
var MD_JS = 'https://unpkg.com/easymde@2.18.0/dist/easymde.min.js';
var RTE_CSS = 'https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css';
var RTE_JS = 'https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js';
var PURIFY_JS = 'https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js';
var CM_CSS = 'https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.css';
var CM_JS = 'https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js';
var CM_MODE = 'https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/javascript/javascript.min.js';
function previewClean(html) {
if (window.DOMPurify) return window.DOMPurify.sanitize(html);
var d = document.createElement('div');
d.textContent = html;
return d.innerHTML;
}
var assets = {};
function loadCss(url) {
if (assets[url]) return;
assets[url] = true;
var l = document.createElement('link');
l.rel = 'stylesheet';
l.href = url;
document.head.appendChild(l);
}
function loadScript(url) {
if (assets[url]) return assets[url];
assets[url] = new Promise(function(resolve, reject) {
var s = document.createElement('script');
s.src = url;
s.async = true;
s.onload = function() { resolve(); };
s.onerror = function() { reject(new Error('umbral: failed to load ' + url)); };
document.head.appendChild(s);
});
return assets[url];
}
function claim(root, selector) {
var out = [];
var nodes = root.querySelectorAll('textarea[data-widget="' + selector + '"]:not([data-widget-mounted])');
for (var i = 0; i < nodes.length; i++) {
nodes[i].setAttribute('data-widget-mounted', '1');
out.push(nodes[i]);
}
return out;
}
var _mountedEditors = [];
var _flushRegisteredForms = typeof WeakSet !== 'undefined' ? new WeakSet() : null;
function registerSubmitFlush(form) {
if (!form) return;
var alreadyRegistered = _flushRegisteredForms
? _flushRegisteredForms.has(form)
: form.hasAttribute('data-umbral-flush-registered');
if (alreadyRegistered) return;
if (_flushRegisteredForms) _flushRegisteredForms.add(form);
else form.setAttribute('data-umbral-flush-registered', '1');
form.addEventListener('htmx:beforeRequest', flushEditorsOnForm);
form.addEventListener('submit', flushEditorsOnForm);
}
function flushEditorsOnForm(event) {
var form = event.currentTarget || event.target;
_mountedEditors.forEach(function(entry) {
try {
if (form.contains(entry.ta)) entry.flush();
} catch (e) {
if (window.console) console.error('umbral: editor flush failed', e);
}
});
}
function mountMarkdown(ta) {
var mde = new EasyMDE({
element: ta,
spellChecker: false,
status: false,
minHeight: '220px',
autoDownloadFontAwesome: true,
renderingConfig: { sanitizerFunction: previewClean },
uploadImage: true,
imageUploadFunction: function(file, onSuccess, onError) {
var fd = new FormData();
fd.append('image', file, file.name || 'upload.png');
var m = document.cookie.match(/(?:^|;\s*)umbral_csrf_token=([^;]*)/);
var headers = m ? { 'X-CSRF-Token': decodeURIComponent(m[1]) } : {};
var base = (typeof umbralAdminBase !== 'undefined') ? umbralAdminBase : '/admin';
fetch(base + '/upload-image', {
method: 'POST',
headers: headers,
body: fd,
credentials: 'same-origin'
}).then(function(resp) {
return resp.json().then(function(data) {
if (resp.ok && data && data.url) {
onSuccess(data.url);
} else {
onError((data && data.error) || ('upload failed (' + resp.status + ')'));
}
});
}).catch(function(err) {
onError('upload failed: ' + err);
});
},
toolbar: ['bold', 'italic', 'heading', '|', 'quote', 'unordered-list',
'ordered-list', '|', 'link', 'image', 'code', 'table', '|',
'preview', 'side-by-side', 'guide']
});
mde.codemirror.on('change', function() { mde.codemirror.save(); });
var flush = function() { mde.codemirror.save(); };
_mountedEditors.push({ ta: ta, flush: flush });
registerSubmitFlush(ta.closest('form'));
}
function mountRte(ta) {
ta.style.display = 'none';
var wrap = document.createElement('div');
wrap.className = 'umbral-rte';
var host = document.createElement('div');
wrap.appendChild(host);
ta.parentNode.insertBefore(wrap, ta.nextSibling);
var quill = new Quill(host, {
theme: 'snow',
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ header: [1, 2, 3, false] }],
[{ list: 'ordered' }, { list: 'bullet' }],
['link'],
['clean']
]
}
});
if (ta.value) quill.clipboard.dangerouslyPasteHTML(previewClean(ta.value));
function sync() {
var html = quill.root.innerHTML;
ta.value = (html === '<p><br></p>') ? '' : html;
}
quill.on('text-change', sync);
_mountedEditors.push({ ta: ta, flush: sync });
registerSubmitFlush(ta.closest('form'));
}
function mountCode(ta) {
var cm = CodeMirror.fromTextArea(ta, {
mode: { name: 'javascript', json: true },
lineNumbers: true,
tabSize: 2,
lineWrapping: true,
viewportMargin: Infinity
});
cm.getWrapperElement().classList.add('umbral-code');
cm.on('change', function() { cm.save(); });
var flush = function() { cm.save(); };
_mountedEditors.push({ ta: ta, flush: flush });
registerSubmitFlush(ta.closest('form'));
}
function mountAll(list, fn, label) {
list.forEach(function(ta) {
try {
fn(ta);
} catch (e) {
ta.style.display = '';
if (window.console) console.error('umbral: ' + label + ' editor mount failed', e);
}
});
}
function unhide(list) {
list.forEach(function(t) { t.style.display = ''; });
}
function initWidgetEditors(root) {
root = root || document;
var mds = claim(root, 'markdown');
var rtes = claim(root, 'rte');
var codes = claim(root, 'code');
if (mds.length) {
loadCss(MD_CSS);
Promise.all([loadScript(PURIFY_JS), loadScript(MD_JS)])
.then(function() { mountAll(mds, mountMarkdown, 'markdown'); })
.catch(function(e) { unhide(mds); if (window.console) console.error(e); });
}
if (rtes.length) {
loadCss(RTE_CSS);
Promise.all([loadScript(PURIFY_JS), loadScript(RTE_JS)])
.then(function() { mountAll(rtes, mountRte, 'rte'); })
.catch(function(e) { unhide(rtes); if (window.console) console.error(e); });
}
if (codes.length) {
loadCss(CM_CSS);
loadScript(CM_JS)
.then(function() { return loadScript(CM_MODE); })
.then(function() { mountAll(codes, mountCode, 'code'); })
.catch(function(e) { unhide(codes); if (window.console) console.error(e); });
}
}
umbral.initWidgetEditors = initWidgetEditors;
document.addEventListener('DOMContentLoaded', function() { initWidgetEditors(document); });
document.body.addEventListener('htmx:afterSwap', function(e) { initWidgetEditors(e.target); });
})();
(function() {
window.umbral = window.umbral || {};
var stack = [];
umbral.openSheet = function(html) {
var slot = document.getElementById('umbral-sheet-slot');
if (!slot) return;
stack.push(slot.innerHTML);
slot.innerHTML = html;
document.body.classList.add('overflow-hidden');
if (window.lucide) lucide.createIcons({ el: slot });
if (umbral.initWidgetEditors) umbral.initWidgetEditors(slot);
umbral._applyStackOffsets();
};
umbral.popSheet = function() {
var slot = document.getElementById('umbral-sheet-slot');
if (!slot) return;
if (stack.length > 0) {
slot.innerHTML = stack.pop();
if (window.lucide) lucide.createIcons({ el: slot });
if (umbral.initWidgetEditors) umbral.initWidgetEditors(slot);
umbral._applyStackOffsets();
} else {
umbral.closeSheet();
}
};
umbral.closeSheet = function() {
var slot = document.getElementById('umbral-sheet-slot');
if (slot) slot.innerHTML = '';
stack = [];
document.body.classList.remove('overflow-hidden');
};
document.body.addEventListener('closeSheet', function() {
umbral.closeSheet();
});
document.body.addEventListener('refreshTable', function() {
var tbody = document.getElementById('table-body');
if (!tbody) {
window.location.reload();
return;
}
var base = tbody.getAttribute('data-rows-url');
if (!base) {
base = window.location.pathname.replace(/\/+$/, '') + '/rows';
}
var url = base + window.location.search;
htmx.ajax('GET', url, { target: '#table-body', swap: 'innerHTML' });
});
umbral._applyStackOffsets = function() {
var slot = document.getElementById('umbral-sheet-slot');
if (!slot) return;
var panels = slot.querySelectorAll('#umbral-sheet-panel');
panels.forEach(function(panel, i) {
panel.style.transform = 'translateX(-' + (i * 40) + 'px)';
});
};
umbral.openNestedSheet = function(table) {
htmx.ajax('GET', umbralAdminBase + '/' + table + '/new-sheet', {
handler: function(elt, info) {
umbral.openSheet(info.xhr.responseText);
}
});
};
umbral.closeDialog = function() {
var slot = document.getElementById('umbral-dialog-slot');
if (slot) slot.innerHTML = '';
};
umbral._openChangePasswordDialog = function(table, id) {
var slot = document.getElementById('umbral-dialog-slot');
var tpl = document.getElementById('umbral-change-password-dialog-template');
if (!slot || !tpl || !tpl.content) return;
var node = tpl.content.cloneNode(true);
var form = node.querySelector('[data-change-pw-form]');
if (form) {
form.setAttribute('hx-post', umbralAdminBase + '/' + table + '/' + id + '/change-password');
}
slot.innerHTML = '';
slot.appendChild(node);
if (window.lucide) lucide.createIcons({ el: slot });
htmx.process(slot);
};
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
var palSlot = document.getElementById('umbral-palette-slot');
if (palSlot && palSlot.innerHTML.trim()) { umbral.closePalette && umbral.closePalette(); e.preventDefault(); return; }
var slot = document.getElementById('umbral-sheet-slot');
if (slot && slot.innerHTML.trim()) { umbral.popSheet(); e.preventDefault(); }
}
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
var palSlot2 = document.getElementById('umbral-palette-slot');
if (palSlot2 && palSlot2.innerHTML.trim()) {
umbral.closePalette && umbral.closePalette();
} else {
htmx.ajax('GET', umbralAdminBase + '/api/palette', {
target: '#umbral-palette-slot',
swap: 'innerHTML'
});
}
}
});
})();
(function () {
'use strict';
var dragging = null;
function cells(grid) {
return Array.prototype.slice.call(
grid.querySelectorAll('[data-widget-key]')
);
}
function currentLayout() {
var out = [];
var grids = document.querySelectorAll('[data-widget-grid]');
Array.prototype.forEach.call(grids, function (grid) {
cells(grid).forEach(function (cell) {
out.push({
key: cell.getAttribute('data-widget-key'),
span: {
cols: parseInt(cell.getAttribute('data-span-cols'), 10) || 3,
rows: parseInt(cell.getAttribute('data-span-rows'), 10) || 1
}
});
});
});
return out;
}
function saveLayout() {
var csrf = (window.umbral && window.umbral.csrfHeaders)
? window.umbral.csrfHeaders()
: {};
fetch(umbralAdminBase + '/api/dashboard/layout', {
method: 'PUT',
credentials: 'same-origin',
headers: Object.assign({ 'Content-Type': 'application/json' }, csrf),
body: JSON.stringify(currentLayout())
}).catch(function (err) {
console.error('umbral-admin: could not save dashboard layout', err);
window.location.reload();
});
}
function bindGrid(grid) {
if (grid.getAttribute('data-dnd-bound') === '1') { return; }
grid.setAttribute('data-dnd-bound', '1');
cells(grid).forEach(function (cell) {
cell.setAttribute('draggable', 'true');
cell.style.cursor = 'grab';
cell.addEventListener('dragstart', function (e) {
dragging = cell;
cell.style.opacity = '0.4';
e.dataTransfer.effectAllowed = 'move';
try { e.dataTransfer.setData('text/plain', cell.getAttribute('data-widget-key')); } catch (_) {}
});
cell.addEventListener('dragend', function () {
cell.style.opacity = '';
dragging = null;
saveLayout();
});
cell.addEventListener('dragover', function (e) {
if (!dragging || dragging === cell || dragging.parentNode !== cell.parentNode) { return; }
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
var box = cell.getBoundingClientRect();
var after = (e.clientX - box.left) > box.width / 2;
cell.parentNode.insertBefore(dragging, after ? cell.nextSibling : cell);
});
cell.addEventListener('drop', function (e) { e.preventDefault(); });
});
}
function bindAll() {
var grids = document.querySelectorAll('[data-widget-grid]');
Array.prototype.forEach.call(grids, bindGrid);
}
document.addEventListener('DOMContentLoaded', bindAll);
document.body && document.body.addEventListener('htmx:afterSwap', bindAll);
})();