GCR deployment testing in progress - content type issue still remaining.

This commit is contained in:
2026-05-05 14:42:03 +05:00
parent 40fe69ed65
commit f2d02a23ec
35 changed files with 2316 additions and 2431 deletions
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -0,0 +1,13 @@
{
"ConnectionStrings": {
"DefaultConnection": "mongodb://localhost:27017"
},
"MongoDbName": "HtmxAppDb",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
{
"dependencies": {
"@tailwindcss/cli": "^4.2.4",
"tailwindcss": "^4.2.4"
}
}
@@ -0,0 +1,190 @@
@import "tailwindcss";
@source "../../**/*.{html,htmx,cs}";
@source "../../src/**/!(*.g).cs";
@theme {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/* ── Calendar component ───────────────────────────────────────────────── */
@layer components {
.cal-dow {
@apply text-xs font-medium text-muted-foreground py-1;
}
.cal-day {
@apply h-9 w-full rounded-md text-sm text-center
text-foreground bg-transparent
hover:bg-accent hover:text-accent-foreground
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
transition-colors cursor-pointer;
}
.cal-day-selected {
@apply bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground;
}
.cal-day-today {
@apply font-semibold underline underline-offset-2;
}
.cal-nav {
@apply text-lg leading-none;
}
/* ── Month / year quick-pick grid ── */
.cal-view-btn {
@apply h-9 w-full rounded-md text-sm text-center
text-foreground bg-transparent
hover:bg-accent hover:text-accent-foreground
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
transition-colors cursor-pointer;
}
.cal-view-btn-selected {
@apply bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground;
}
/* ── CalendarRange day states ── */
.calr-day {
@apply h-9 w-full text-sm text-center text-foreground
transition-colors cursor-pointer focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-ring;
}
/* Plain days (no range involvement) */
.calr-day-plain {
@apply rounded-md hover:bg-accent hover:text-accent-foreground;
}
/* Start cap — primary, rounded left only */
.calr-day-start {
@apply rounded-l-md bg-primary text-primary-foreground
hover:bg-primary;
}
/* End cap — primary, rounded right only */
.calr-day-end {
@apply rounded-r-md bg-primary text-primary-foreground
hover:bg-primary;
}
/* Days strictly between start and end */
.calr-day-mid {
@apply rounded-none bg-accent text-accent-foreground
hover:bg-accent;
}
}
/* ── Select custom caret via background SVG ─────────────────────────── */
@layer components {
select.appearance-none {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
padding-right: 2.25rem;
}
}
/* ── TimePicker hide number spinner arrows ──────────────────────────── */
@layer utilities {
input[type=number].timepicker-hour,
input[type=number].timepicker-minute {
-moz-appearance: textfield;
}
input[type=number].timepicker-hour::-webkit-outer-spin-button,
input[type=number].timepicker-hour::-webkit-inner-spin-button,
input[type=number].timepicker-minute::-webkit-outer-spin-button,
input[type=number].timepicker-minute::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,717 @@
/* ─────────────────────────────────────────────────────────────────────────
* components.js client-side logic for htmx server-rendered components
* ───────────────────────────────────────────────────────────────────────── */
// ── Calendar ──────────────────────────────────────────────────────────────
(function () {
var MONTHS = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
function renderCalendar(root) {
var view = root.dataset.view || 'days';
var year = parseInt(root.dataset.year, 10);
var month = parseInt(root.dataset.month, 10);
var selD = parseInt(root.dataset.selDay, 10);
var selM = parseInt(root.dataset.selMonth, 10);
var selY = parseInt(root.dataset.selYear, 10);
var labelBtn = root.querySelector('.cal-month-label');
var grid = root.querySelector('.cal-grid');
var dowRow = root.querySelector('.cal-dow-row');
// ── Update header label based on view ──
if (view === 'days') {
labelBtn.textContent = MONTHS[month] + ' ' + year;
} else if (view === 'months') {
labelBtn.textContent = year;
} else { // years
var ds = Math.floor(year / 12) * 12;
labelBtn.textContent = ds + ' ' + (ds + 11);
}
// Show DOW row only in day view
if (dowRow) dowRow.style.display = view === 'days' ? '' : 'none';
grid.innerHTML = '';
if (view === 'days') {
grid.style.gridTemplateColumns = ''; // let CSS class (grid-cols-7) take over
var firstDay = new Date(year, month, 1).getDay();
var daysInMonth = new Date(year, month + 1, 0).getDate();
for (var i = 0; i < firstDay; i++) {
grid.appendChild(document.createElement('div'));
}
for (var d = 1; d <= daysInMonth; d++) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = d;
btn.className = 'cal-day';
if (d === selD && month === selM && year === selY) {
btn.classList.add('cal-day-selected');
}
btn.dataset.date = year + '-'
+ String(month + 1).padStart(2, '0') + '-'
+ String(d).padStart(2, '0');
btn.addEventListener('click', (function (b, r) {
return function () {
var parts = b.dataset.date.split('-');
r.dataset.selYear = parts[0];
r.dataset.selMonth = parseInt(parts[1], 10) - 1;
r.dataset.selDay = parseInt(parts[2], 10);
r.querySelectorAll('.cal-day').forEach(function (el) {
el.classList.remove('cal-day-selected');
});
b.classList.add('cal-day-selected');
r.querySelector('.cal-hidden-input').value = b.dataset.date;
r.dispatchEvent(new CustomEvent('calendarChange', {
detail: { date: b.dataset.date },
bubbles: true
}));
};
})(btn, root));
grid.appendChild(btn);
}
} else if (view === 'months') {
grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
MONTHS.forEach(function (name, i) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = name.slice(0, 3);
btn.className = 'cal-view-btn' + (i === month ? ' cal-view-btn-selected' : '');
btn.addEventListener('click', function () {
root.dataset.month = i;
root.dataset.view = 'days';
renderCalendar(root);
});
grid.appendChild(btn);
});
} else { // years
var decadeStart = Math.floor(year / 12) * 12;
grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
for (var yi = 0; yi < 12; yi++) {
(function (y) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = y;
btn.className = 'cal-view-btn' + (y === year ? ' cal-view-btn-selected' : '');
btn.addEventListener('click', function () {
root.dataset.year = y;
root.dataset.view = 'months';
renderCalendar(root);
});
grid.appendChild(btn);
})(decadeStart + yi);
}
}
}
function initCalendar(root) {
root.querySelector('.cal-prev').addEventListener('click', function () {
var view = root.dataset.view || 'days';
var m = parseInt(root.dataset.month, 10);
var y = parseInt(root.dataset.year, 10);
if (view === 'days') {
if (m === 0) { m = 11; y--; } else { m--; }
root.dataset.month = m;
root.dataset.year = y;
} else if (view === 'months') {
root.dataset.year = y - 1;
} else { // years
root.dataset.year = Math.floor(y / 12) * 12 - 12;
}
renderCalendar(root);
});
root.querySelector('.cal-next').addEventListener('click', function () {
var view = root.dataset.view || 'days';
var m = parseInt(root.dataset.month, 10);
var y = parseInt(root.dataset.year, 10);
if (view === 'days') {
if (m === 11) { m = 0; y++; } else { m++; }
root.dataset.month = m;
root.dataset.year = y;
} else if (view === 'months') {
root.dataset.year = y + 1;
} else { // years
root.dataset.year = Math.floor(y / 12) * 12 + 12;
}
renderCalendar(root);
});
root.querySelector('.cal-month-label').addEventListener('click', function () {
var view = root.dataset.view || 'days';
if (view === 'days') root.dataset.view = 'months';
else if (view === 'months') root.dataset.view = 'years';
// already at years — nothing deeper
renderCalendar(root);
});
renderCalendar(root);
}
// Initialise all calendars on page load, and again after any htmx swap
function initAll() {
document.querySelectorAll('.calendar-root').forEach(initCalendar);
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── CalendarRange ─────────────────────────────────────────────────────────
(function () {
var MONTHS = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
function cmpDate(a, b) { return a < b ? -1 : a > b ? 1 : 0; }
function isBetween(d, start, end) {
return cmpDate(d, start) > 0 && cmpDate(d, end) < 0;
}
function toDateStr(year, month, day) {
return year + '-'
+ String(month + 1).padStart(2, '0') + '-'
+ String(day).padStart(2, '0');
}
function updateLabel(root) {
var lbl = root.querySelector('.calr-label');
if (!lbl) return;
var start = root.dataset.start;
var end = root.dataset.end;
if (start && end) { lbl.textContent = start + ' → ' + end; return; }
if (start) { lbl.textContent = start + ' → pick end date'; return; }
lbl.textContent = '';
}
// Only updates CSS classes on already-rendered buttons — no DOM destruction.
function updateHoverClasses(root, hoverDate) {
var start = root.dataset.start;
var end = root.dataset.end;
var rangeEnd = (start && !end && hoverDate && cmpDate(hoverDate, start) >= 0)
? hoverDate : end;
root.querySelectorAll('.calr-day').forEach(function (btn) {
var ds = btn.dataset.date;
var isStart = !!(start && ds === start);
var isEnd = !!(end && ds === end);
var isHoverEnd = !!(!end && start && hoverDate && ds === hoverDate
&& cmpDate(hoverDate, start) > 0);
var isMid = !!(start && rangeEnd && isBetween(ds, start, rangeEnd));
btn.classList.remove('calr-day-start', 'calr-day-end', 'calr-day-mid', 'calr-day-plain');
if (isStart) btn.classList.add('calr-day-start');
if (isEnd || isHoverEnd) btn.classList.add('calr-day-end');
if (isMid) btn.classList.add('calr-day-mid');
if (!isStart && !isEnd && !isHoverEnd && !isMid) btn.classList.add('calr-day-plain');
});
}
// Full re-render of the grid. Called on mount, click, and view changes.
function renderRange(root) {
var view = root.dataset.view || 'days';
var year = parseInt(root.dataset.year, 10);
var month = parseInt(root.dataset.month, 10);
var start = root.dataset.start || '';
var end = root.dataset.end || '';
var labelBtn = root.querySelector('.calr-month-label');
var grid = root.querySelector('.calr-grid');
var dowRow = root.querySelector('.cal-dow-row');
// ── Update header label ──
if (view === 'days') {
labelBtn.textContent = MONTHS[month] + ' ' + year;
} else if (view === 'months') {
labelBtn.textContent = year;
} else { // years
var ds = Math.floor(year / 12) * 12;
labelBtn.textContent = ds + ' ' + (ds + 11);
}
if (dowRow) dowRow.style.display = view === 'days' ? '' : 'none';
grid.innerHTML = '';
// ── Clear event handlers (will be reassigned per view below) ──
grid.onmouseover = null;
grid.onmouseleave = null;
if (view === 'days') {
grid.style.gridTemplateColumns = '';
var firstDay = new Date(year, month, 1).getDay();
var daysInMonth = new Date(year, month + 1, 0).getDate();
for (var i = 0; i < firstDay; i++) {
grid.appendChild(document.createElement('div'));
}
for (var d = 1; d <= daysInMonth; d++) {
var dateStr = toDateStr(year, month, d);
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = d;
btn.dataset.date = dateStr;
var isStart = start && dateStr === start;
var isEnd = end && dateStr === end;
var isMid = start && end && isBetween(dateStr, start, end);
var cls = 'calr-day';
if (isStart) cls += ' calr-day-start';
else if (isEnd) cls += ' calr-day-end';
else if (isMid) cls += ' calr-day-mid';
else cls += ' calr-day-plain';
btn.className = cls;
grid.appendChild(btn);
}
// Click: update state → full re-render
grid.onclick = function (e) {
var btn = e.target.closest('.calr-day');
if (!btn) return;
var ds = btn.dataset.date;
var s = root.dataset.start;
var en = root.dataset.end;
if (!s || (s && en)) {
root.dataset.start = ds;
root.dataset.end = '';
} else {
if (cmpDate(ds, s) > 0) {
root.dataset.end = ds;
} else if (cmpDate(ds, s) < 0) {
root.dataset.start = ds;
root.dataset.end = '';
} else {
root.dataset.start = '';
root.dataset.end = '';
}
}
root.querySelector('.calr-hidden-start').value = root.dataset.start;
root.querySelector('.calr-hidden-end').value = root.dataset.end;
root.dispatchEvent(new CustomEvent('rangeChange', {
detail: { start: root.dataset.start, end: root.dataset.end },
bubbles: true
}));
renderRange(root);
updateLabel(root);
};
grid.onmouseover = function (e) {
var btn = e.target.closest('.calr-day');
if (!btn) return;
updateHoverClasses(root, btn.dataset.date);
};
grid.onmouseleave = function () {
updateHoverClasses(root, null);
};
} else if (view === 'months') {
grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
MONTHS.forEach(function (name, i) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = name.slice(0, 3);
btn.className = 'cal-view-btn' + (i === month ? ' cal-view-btn-selected' : '');
btn.addEventListener('click', function () {
root.dataset.month = i;
root.dataset.view = 'days';
renderRange(root);
});
grid.appendChild(btn);
});
} else { // years
var decadeStart = Math.floor(year / 12) * 12;
grid.style.gridTemplateColumns = 'repeat(3, minmax(0, 1fr))';
for (var yi = 0; yi < 12; yi++) {
(function (y) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = y;
btn.className = 'cal-view-btn' + (y === year ? ' cal-view-btn-selected' : '');
btn.addEventListener('click', function () {
root.dataset.year = y;
root.dataset.view = 'months';
renderRange(root);
});
grid.appendChild(btn);
})(decadeStart + yi);
}
}
}
function initCalendarRange(root) {
root.querySelector('.calr-prev').addEventListener('click', function () {
var view = root.dataset.view || 'days';
var m = parseInt(root.dataset.month, 10);
var y = parseInt(root.dataset.year, 10);
if (view === 'days') {
if (m === 0) { m = 11; y--; } else { m--; }
root.dataset.month = m;
root.dataset.year = y;
} else if (view === 'months') {
root.dataset.year = y - 1;
} else { // years
root.dataset.year = Math.floor(y / 12) * 12 - 12;
}
renderRange(root);
});
root.querySelector('.calr-next').addEventListener('click', function () {
var view = root.dataset.view || 'days';
var m = parseInt(root.dataset.month, 10);
var y = parseInt(root.dataset.year, 10);
if (view === 'days') {
if (m === 11) { m = 0; y++; } else { m++; }
root.dataset.month = m;
root.dataset.year = y;
} else if (view === 'months') {
root.dataset.year = y + 1;
} else { // years
root.dataset.year = Math.floor(y / 12) * 12 + 12;
}
renderRange(root);
});
root.querySelector('.calr-month-label').addEventListener('click', function () {
var view = root.dataset.view || 'days';
if (view === 'days') root.dataset.view = 'months';
else if (view === 'months') root.dataset.view = 'years';
renderRange(root);
});
renderRange(root);
updateLabel(root);
}
function initAll() {
document.querySelectorAll('.calr-root').forEach(initCalendarRange);
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── TimePicker ────────────────────────────────────────────────────────────
(function () {
function syncTime(root) {
var h = parseInt(root.querySelector('.timepicker-hour').value, 10) || 0;
var m = parseInt(root.querySelector('.timepicker-minute').value, 10) || 0;
var use12h = root.dataset.use12h === 'true';
var h24 = h;
if (use12h) {
var ampmEl = root.querySelector('.timepicker-ampm');
var ampm = ampmEl ? ampmEl.value : 'AM';
if (ampm === 'PM') { h24 = h === 12 ? 12 : h + 12; }
else { h24 = h === 12 ? 0 : h; }
}
root.querySelector('.timepicker-hidden').value =
String(h24).padStart(2, '0') + ':' + String(m).padStart(2, '0');
}
function initTimePicker(root) {
var sync = syncTime.bind(null, root);
root.querySelector('.timepicker-hour').addEventListener('input', sync);
root.querySelector('.timepicker-minute').addEventListener('input', sync);
var ampmEl = root.querySelector('.timepicker-ampm');
if (ampmEl) ampmEl.addEventListener('change', sync);
sync();
}
function initAll() {
document.querySelectorAll('.timepicker-root').forEach(initTimePicker);
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── Switch ────────────────────────────────────────────────────────────────
(function () {
function updateSwitch(input) {
var track = input.parentElement && input.parentElement.querySelector('.switch-track');
if (!track) return;
var thumb = track.querySelector('.switch-thumb');
if (input.checked) {
track.classList.add('bg-primary');
track.classList.remove('bg-input');
if (thumb) thumb.style.transform = 'translateX(1.375rem)';
} else {
track.classList.remove('bg-primary');
track.classList.add('bg-input');
if (thumb) thumb.style.transform = '';
}
}
function initAll() {
document.querySelectorAll('.switch-checkbox').forEach(function (input) {
updateSwitch(input);
if (!input._switchBound) {
input._switchBound = true;
input.addEventListener('change', function () { updateSwitch(input); });
}
});
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── Tabs ──────────────────────────────────────────────────────────────────
(function () {
var ACTIVE = 'bg-background text-foreground shadow-sm';
var INACTIVE = 'text-muted-foreground';
function initTabs(root) {
if (root._tabsInitialised) return;
root._tabsInitialised = true;
var triggers = Array.from(root.querySelectorAll('.tabs-trigger'));
var panels = Array.from(root.querySelectorAll('.tabs-panel'));
function activate(idx) {
triggers.forEach(function (t, i) {
var active = i === idx;
t.setAttribute('aria-selected', String(active));
ACTIVE.split(' ').forEach(function (c) { t.classList.toggle(c, active); });
INACTIVE.split(' ').forEach(function (c) { t.classList.toggle(c, !active); });
});
panels.forEach(function (p, i) { p.hidden = i !== idx; });
}
triggers.forEach(function (trigger, idx) {
trigger.addEventListener('click', function () { activate(idx); });
});
activate(0);
}
function initAll() {
document.querySelectorAll('.tabs-root').forEach(initTabs);
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── Accordion ─────────────────────────────────────────────────────────────
(function () {
function initAccordion(root) {
if (root._accInitialised) return;
root._accInitialised = true;
root.querySelectorAll('.accordion-trigger').forEach(function (trigger) {
trigger.addEventListener('click', function () {
var expanded = trigger.getAttribute('aria-expanded') === 'true';
var panel = trigger.closest('.accordion-item').querySelector('.accordion-panel');
if (expanded) {
trigger.setAttribute('aria-expanded', 'false');
panel.style.height = '0';
panel.style.opacity = '0';
} else {
trigger.setAttribute('aria-expanded', 'true');
panel.style.height = panel.scrollHeight + 'px';
panel.style.opacity = '1';
}
var chevron = trigger.querySelector('.accordion-chevron');
if (chevron) chevron.style.transform = expanded ? '' : 'rotate(180deg)';
});
});
}
function initAll() {
document.querySelectorAll('.accordion-root').forEach(initAccordion);
}
document.addEventListener('DOMContentLoaded', initAll);
document.addEventListener('htmx:afterSwap', initAll);
})();
// ── Toast ─────────────────────────────────────────────────────────────────
(function () {
function dismissToast(toast) {
toast.style.opacity = '0';
toast.style.transform = 'translateY(0.5rem)';
setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
}
window.showToast = function (options) {
var viewport = document.querySelector('.toast-viewport');
if (!viewport) return;
var title = options.title || '';
var description = options.description || '';
var variant = options.variant || 'default';
var duration = typeof options.duration === 'number' ? options.duration : 5000;
var variantCls = variant === 'destructive' ? ' border-destructive text-destructive' : '';
var toast = document.createElement('div');
toast.setAttribute('role', 'alert');
toast.className = 'toast-item pointer-events-auto relative flex w-full items-center justify-between' +
' space-x-4 overflow-hidden rounded-md border border-border bg-background p-4' +
' shadow-lg transition-all duration-300 opacity-0 translate-y-2' + variantCls;
toast.innerHTML =
'<div class="grid gap-1">' +
(title ? '<div class="text-sm font-semibold">' + title + '</div>' : '') +
(description ? '<div class="text-sm opacity-90">' + description + '</div>' : '') +
'</div>' +
'<button type="button" aria-label="Dismiss"' +
' class="toast-close inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md' +
' text-muted-foreground hover:text-foreground focus:outline-none">' +
'<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"' +
' stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>' +
'</button>';
viewport.appendChild(toast);
requestAnimationFrame(function () {
toast.classList.remove('opacity-0', 'translate-y-2');
});
var timer = duration > 0 ? setTimeout(function () { dismissToast(toast); }, duration) : null;
toast.querySelector('.toast-close').addEventListener('click', function () {
if (timer) clearTimeout(timer);
dismissToast(toast);
});
};
// Delegate clicks on server-rendered toast close buttons
document.addEventListener('click', function (e) {
var btn = e.target.closest('.toast-close');
if (btn) {
var item = btn.closest('.toast-item');
if (item) dismissToast(item);
}
});
document.addEventListener('keydown', function (e) {
var trigger = e.target.closest('.dropdown-trigger');
if (!trigger) return;
if (e.key !== 'Enter' && e.key !== ' ') return;
e.preventDefault();
var root = trigger.closest('.dropdown-root');
var content = root && root.querySelector('.dropdown-content');
var isOpen = content && !content.classList.contains('hidden');
document.querySelectorAll('.dropdown-root').forEach(closeDropdown);
if (!isOpen && root) openDropdown(root);
});
})();
// ── Dialog ────────────────────────────────────────────────────────────────
(function () {
document.addEventListener('click', function (e) {
// Open
var openBtn = e.target.closest('[data-dialog-open]');
if (openBtn) {
var dlg = document.getElementById('dlg-' + openBtn.dataset.dialogOpen);
if (dlg && dlg.showModal) dlg.showModal();
}
// Close via button
var closeBtn = e.target.closest('[data-dialog-close], .dialog-close');
if (closeBtn) {
var dlg = closeBtn.closest('dialog');
if (dlg) dlg.close();
}
});
// Close on backdrop click
document.addEventListener('click', function (e) {
if (e.target && e.target.tagName === 'DIALOG') {
e.target.close();
}
});
})();
// ── DropdownMenu ──────────────────────────────────────────────────────────
(function () {
function closeDropdown(root) {
var trigger = root.querySelector('.dropdown-trigger');
var content = root.querySelector('.dropdown-content');
if (!trigger || !content) return;
content.classList.add('hidden');
trigger.setAttribute('aria-expanded', 'false');
}
function openDropdown(root) {
var trigger = root.querySelector('.dropdown-trigger');
var content = root.querySelector('.dropdown-content');
if (!trigger || !content) return;
content.classList.remove('hidden');
trigger.setAttribute('aria-expanded', 'true');
}
document.addEventListener('click', function (e) {
var trigger = e.target.closest('.dropdown-trigger');
if (trigger) {
var root = trigger.closest('.dropdown-root');
var content = root && root.querySelector('.dropdown-content');
var isOpen = content && !content.classList.contains('hidden');
document.querySelectorAll('.dropdown-root').forEach(closeDropdown);
if (!isOpen && root) openDropdown(root);
return;
}
var insideMenu = e.target.closest('.dropdown-content');
if (insideMenu) {
var rootInMenu = insideMenu.closest('.dropdown-root');
if (e.target.closest('a, button') && rootInMenu) {
closeDropdown(rootInMenu);
}
return;
}
document.querySelectorAll('.dropdown-root').forEach(function (root) {
if (!root.contains(e.target)) closeDropdown(root);
});
});
})();