chore: update templ and templui

This commit is contained in:
juancwu 2026-04-12 16:07:06 +00:00
commit 61eaa268ab
89 changed files with 25776 additions and 8231 deletions

View file

@ -38,8 +38,10 @@ tailwindcss -i ./assets/css/input.css -o ./assets/css/output.css --watch # watc
- `internal/repository/` — data access with sqlx, interface-based
- `internal/model/` — data structs with `db:` tags
- `internal/middleware/` — ordered chain: Config → Logging → NoCache → CSRF → Auth → URLPath
- `internal/router/` - custom router to group routes together and chain middleware.
- `internal/routes/routes.go` — all route definitions with middleware wrapping
- `internal/ui/` — templ templates organized as pages/, components/, layouts/, blocks/
- `internal/misc/` - miscellanous packages such as timezones
- `assets/` — static files (CSS, JS, fonts) embedded in binary via `go:embed`
## Key Patterns
@ -67,3 +69,78 @@ App reads from `.env` file via `godotenv`. Key vars: `APP_ENV`, `APP_URL`, `DB_D
## Database
PostgreSQL (pgx driver) or SQLite. Migrations auto-run on startup from `internal/db/migrations/` (Goose SQL format, embedded via `go:embed`). 8 migration files covering users, tokens, profiles, spaces, shopping lists, tags, expenses, invitations.
# templui Components
> templ-based UI components for Go. Open source. Customizable. Accessible.
## Overview
templui is a collection of beautifully designed, accessible UI components built with templ and Go.
Components are designed to be composable, customizable, and easy to integrate into your Go projects.
- [Introduction](https://templui.io/docs/introduction): Core principles and getting started guide
- [How to Use](https://templui.io/docs/how-to-use): CLI installation and usage guide
- [Components](https://templui.io/docs/components): Component overview and catalog
- [Themes](https://templui.io/docs/themes): Theme customization and styling
- [GitHub](https://github.com/templui/templui): Source code and issue tracker
## Form & Input
- [Button](https://templui.io/docs/components/button): Button component with multiple variants.
- [Calendar](https://templui.io/docs/components/calendar): Calendar component for date selection.
- [Checkbox](https://templui.io/docs/components/checkbox): Checkbox input component.
- [Date Picker](https://templui.io/docs/components/date-picker): Date picker component combining input and calendar.
- [Form](https://templui.io/docs/components/form): Form container with validation support.
- [Input](https://templui.io/docs/components/input): Text input component.
- [Input OTP](https://templui.io/docs/components/input-otp): One-time password input component.
- [Label](https://templui.io/docs/components/label): Form label component.
- [Radio](https://templui.io/docs/components/radio): Radio button group component.
- [Rating](https://templui.io/docs/components/rating): Star rating input component.
- [Select Box](https://templui.io/docs/components/select-box): Searchable select component.
- [Slider](https://templui.io/docs/components/slider): Slider input component.
- [Switch](https://templui.io/docs/components/switch): Toggle switch component.
- [Tags Input](https://templui.io/docs/components/tags-input): Tags input component.
- [Textarea](https://templui.io/docs/components/textarea): Multi-line text input component.
- [Time Picker](https://templui.io/docs/components/time-picker): Time picker component.
## Layout & Navigation
- [Accordion](https://templui.io/docs/components/accordion): Collapsible accordion component.
- [Breadcrumb](https://templui.io/docs/components/breadcrumb): Breadcrumb navigation component.
- [Pagination](https://templui.io/docs/components/pagination): Pagination component for lists and tables.
- [Separator](https://templui.io/docs/components/separator): Visual divider between content sections.
- [Sidebar](https://templui.io/docs/components/sidebar): Collapsible sidebar component for app layouts.
- [Tabs](https://templui.io/docs/components/tabs): Tabbed interface component.
## Overlays & Dialogs
- [Dialog](https://templui.io/docs/components/dialog): Modal dialog component.
- [Dropdown](https://templui.io/docs/components/dropdown): Dropdown menu component.
- [Popover](https://templui.io/docs/components/popover): Floating popover component.
- [Sheet](https://templui.io/docs/components/sheet): Slide-out panel component (drawer).
- [Tooltip](https://templui.io/docs/components/tooltip): Tooltip component for additional context.
## Feedback & Status
- [Alert](https://templui.io/docs/components/alert): Alert component for messages and notifications.
- [Badge](https://templui.io/docs/components/badge): Badge component for labels and status indicators.
- [Progress](https://templui.io/docs/components/progress): Progress bar component.
- [Skeleton](https://templui.io/docs/components/skeleton): Skeleton loading placeholder.
- [Toast](https://templui.io/docs/components/toast): Toast notification component.
## Display & Media
- [Aspect Ratio](https://templui.io/docs/components/aspect-ratio): Container that maintains aspect ratio.
- [Avatar](https://templui.io/docs/components/avatar): Avatar component for user profiles.
- [Card](https://templui.io/docs/components/card): Card container component.
- [Carousel](https://templui.io/docs/components/carousel): Carousel component with navigation controls.
- [Charts](https://templui.io/docs/components/charts): Chart components for data visualization.
- [Table](https://templui.io/docs/components/table): Table component for displaying data.
## Misc
- [Collapsible](https://templui.io/docs/components/collapsible): Collapsible container component.
- [Copy Button](https://templui.io/docs/components/copy-button): Copy to clipboard button component.
- [Icon](https://templui.io/docs/components/icon): SVG icon component library.

63
assets/js/avatar.js Normal file
View file

@ -0,0 +1,63 @@
(function () {
"use strict";
// Handle image load events
document.addEventListener(
"load",
function (e) {
if (e.target.matches("[data-tui-avatar-image]")) {
const fallback = e.target.parentElement.querySelector(
"[data-tui-avatar-fallback]",
);
if (fallback) {
fallback.style.display = "none";
}
}
},
true,
);
// Handle image error events
document.addEventListener(
"error",
function (e) {
if (e.target.matches("[data-tui-avatar-image]")) {
e.target.style.display = "none";
const fallback = e.target.parentElement.querySelector(
"[data-tui-avatar-fallback]",
);
if (fallback) {
fallback.style.display = "flex";
}
}
},
true,
);
// Check already loaded/broken images on DOM ready
function checkImages() {
document
.querySelectorAll("[data-tui-avatar-image]")
.forEach(function (img) {
const fallback = img.parentElement.querySelector(
"[data-tui-avatar-fallback]",
);
// Image already successfully loaded
if (img.complete && img.naturalWidth > 0) {
if (fallback) fallback.style.display = "none";
}
// Image already failed
else if (img.complete && img.naturalWidth === 0) {
img.style.display = "none";
if (fallback) fallback.style.display = "flex";
}
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", checkImages);
} else {
checkImages();
}
})();

414
assets/js/calendar.js Normal file
View file

@ -0,0 +1,414 @@
(function () {
"use strict";
// Utility functions
function parseISODate(isoStr) {
if (!isoStr) return null;
const parts = isoStr.split("-");
if (parts.length !== 3) return null;
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1;
const day = parseInt(parts[2], 10);
const date = new Date(Date.UTC(year, month, day));
if (
date.getUTCFullYear() === year &&
date.getUTCMonth() === month &&
date.getUTCDate() === day
) {
return date;
}
return null;
}
function getMonthNames(locale) {
try {
return Array.from({ length: 12 }, (_, i) =>
new Intl.DateTimeFormat(locale, {
month: "short",
timeZone: "UTC",
}).format(new Date(Date.UTC(2000, i, 1))),
);
} catch {
return [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
}
}
function getDayNames(locale, startOfWeek) {
try {
return Array.from({ length: 7 }, (_, i) =>
new Intl.DateTimeFormat(locale, {
weekday: "short",
timeZone: "UTC",
}).format(new Date(Date.UTC(2000, 0, i + 2 + startOfWeek))),
);
} catch {
return ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
}
}
function findHiddenInput(container) {
const wrapper = container.closest("[data-tui-calendar-wrapper]");
return wrapper?.querySelector("[data-tui-calendar-hidden-input]") || null;
}
function renderCalendar(container) {
const weekdaysContainer = container.querySelector(
"[data-tui-calendar-weekdays]",
);
const daysContainer = container.querySelector("[data-tui-calendar-days]");
if (!weekdaysContainer || !daysContainer) return;
// Get current viewing month/year (or use initial/defaults)
let currentMonth = parseInt(container.dataset.tuiCalendarCurrentMonth);
let currentYear = parseInt(container.dataset.tuiCalendarCurrentYear);
// If not set, use initial values or current date
if (isNaN(currentMonth) || isNaN(currentYear)) {
const initialMonth = parseInt(
container.getAttribute("data-tui-calendar-initial-month"),
);
const initialYear = parseInt(
container.getAttribute("data-tui-calendar-initial-year"),
);
const selectedDate = container.getAttribute(
"data-tui-calendar-selected-date",
);
if (selectedDate) {
const parsed = parseISODate(selectedDate);
if (parsed) {
currentMonth = parsed.getUTCMonth();
currentYear = parsed.getUTCFullYear();
}
}
if (isNaN(currentMonth)) {
currentMonth = !isNaN(initialMonth)
? initialMonth
: new Date().getMonth();
}
if (isNaN(currentYear)) {
currentYear =
!isNaN(initialYear) && initialYear > 0
? initialYear
: new Date().getFullYear();
}
// Store for navigation
container.dataset.tuiCalendarCurrentMonth = currentMonth;
container.dataset.tuiCalendarCurrentYear = currentYear;
}
// Get other settings
const locale =
container.getAttribute("data-tui-calendar-locale-tag") || "en-US";
let startOfWeek = parseInt(
container.getAttribute("data-tui-calendar-start-of-week"),
10,
);
if (isNaN(startOfWeek)) startOfWeek = 1;
const selectedDateStr = container.getAttribute(
"data-tui-calendar-selected-date",
);
const selectedDate = selectedDateStr ? parseISODate(selectedDateStr) : null;
// Update SelectBox values
const monthNames = getMonthNames(locale);
const monthValue = container.querySelector(`#${container.id}-month-value`);
const yearValue = container.querySelector(`#${container.id}-year-value`);
if (monthValue) monthValue.textContent = monthNames[currentMonth];
if (yearValue) yearValue.textContent = currentYear;
// Render weekdays if empty
if (!weekdaysContainer.children.length) {
const dayNames = getDayNames(locale, startOfWeek);
weekdaysContainer.innerHTML = dayNames
.map(
(day) =>
`<div class="text-center text-xs text-muted-foreground font-medium">${day}</div>`,
)
.join("");
}
// Render days
daysContainer.innerHTML = "";
const firstDay = new Date(Date.UTC(currentYear, currentMonth, 1));
const startOffset = (((firstDay.getUTCDay() - startOfWeek) % 7) + 7) % 7;
const daysInMonth = new Date(
Date.UTC(currentYear, currentMonth + 1, 0),
).getUTCDate();
const today = new Date();
const todayUTC = new Date(
Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()),
);
// Add empty cells for offset
for (let i = 0; i < startOffset; i++) {
daysContainer.innerHTML +=
'<div class="h-[var(--cell-size)] w-[var(--cell-size)]"></div>';
}
// Add day buttons
for (let day = 1; day <= daysInMonth; day++) {
const currentDate = new Date(Date.UTC(currentYear, currentMonth, day));
const isSelected =
selectedDate && currentDate.getTime() === selectedDate.getTime();
const isToday = currentDate.getTime() === todayUTC.getTime();
let classes =
"inline-flex h-[var(--cell-size)] w-[var(--cell-size)] items-center justify-center rounded-md text-sm font-medium focus:outline-none focus:ring-1 focus:ring-ring";
if (isSelected) {
classes += " bg-primary text-primary-foreground hover:bg-primary/90";
} else if (isToday) {
classes += " bg-accent text-accent-foreground";
} else {
classes += " hover:bg-accent hover:text-accent-foreground";
}
const attrs = [
`data-tui-calendar-day="${day}"`,
isToday ? 'data-tui-calendar-today="true"' : "",
isSelected ? 'data-tui-calendar-selected="true"' : "",
]
.filter(Boolean)
.join(" ");
daysContainer.innerHTML += `<button type="button" class="${classes}" ${attrs}>${day}</button>`;
}
}
// Handle month/year selection from native selects
document.addEventListener("change", (e) => {
// Month select
if (e.target.matches("[data-tui-calendar-month-select]")) {
const container = e.target.closest("[data-tui-calendar-container]");
if (!container) return;
const newMonth = parseInt(e.target.value, 10);
if (isNaN(newMonth)) return;
container.dataset.tuiCalendarCurrentMonth = newMonth;
renderCalendar(container);
return;
}
// Year select
if (e.target.matches("[data-tui-calendar-year-select]")) {
const container = e.target.closest("[data-tui-calendar-container]");
if (!container) return;
const newYear = parseInt(e.target.value, 10);
if (isNaN(newYear)) return;
container.dataset.tuiCalendarCurrentYear = newYear;
renderCalendar(container);
return;
}
});
// Event delegation for calendar navigation and selection
document.addEventListener("click", (e) => {
// Previous month
const prevBtn = e.target.closest("[data-tui-calendar-prev]");
if (prevBtn) {
const container = prevBtn.closest("[data-tui-calendar-container]");
if (!container) return;
let month = parseInt(container.dataset.tuiCalendarCurrentMonth, 10);
let year = parseInt(container.dataset.tuiCalendarCurrentYear, 10);
// Only use fallback if truly not initialized (should not happen after init)
if (isNaN(month)) month = new Date().getMonth();
if (isNaN(year)) year = new Date().getFullYear();
month--;
if (month < 0) {
month = 11;
year--;
}
container.dataset.tuiCalendarCurrentMonth = month;
container.dataset.tuiCalendarCurrentYear = year;
renderCalendar(container);
updateNativeSelects(container);
return;
}
// Next month
const nextBtn = e.target.closest("[data-tui-calendar-next]");
if (nextBtn) {
const container = nextBtn.closest("[data-tui-calendar-container]");
if (!container) return;
let month = parseInt(container.dataset.tuiCalendarCurrentMonth, 10);
let year = parseInt(container.dataset.tuiCalendarCurrentYear, 10);
// Only use fallback if truly not initialized (should not happen after init)
if (isNaN(month)) month = new Date().getMonth();
if (isNaN(year)) year = new Date().getFullYear();
month++;
if (month > 11) {
month = 0;
year++;
}
container.dataset.tuiCalendarCurrentMonth = month;
container.dataset.tuiCalendarCurrentYear = year;
renderCalendar(container);
updateNativeSelects(container);
return;
}
// Day selection
if (e.target.matches("[data-tui-calendar-day]")) {
const container = e.target.closest("[data-tui-calendar-container]");
if (!container) return;
const day = parseInt(e.target.dataset.tuiCalendarDay);
let month = parseInt(container.dataset.tuiCalendarCurrentMonth, 10);
let year = parseInt(container.dataset.tuiCalendarCurrentYear, 10);
// Only use fallback if truly not initialized (should not happen after init)
if (isNaN(month)) month = new Date().getMonth();
if (isNaN(year)) year = new Date().getFullYear();
const selectedDate = new Date(Date.UTC(year, month, day));
// Update selected date attribute
container.setAttribute(
"data-tui-calendar-selected-date",
selectedDate.toISOString().split("T")[0],
);
// Update hidden input
const hiddenInput = findHiddenInput(container);
if (hiddenInput) {
hiddenInput.value = selectedDate.toISOString().split("T")[0];
}
// Dispatch custom event
container.dispatchEvent(
new CustomEvent("calendar-date-selected", {
bubbles: true,
detail: { date: selectedDate },
}),
);
renderCalendar(container);
}
});
// Update native selects when month/year changes via arrows
function updateNativeSelects(container) {
const month = parseInt(container.dataset.tuiCalendarCurrentMonth, 10);
const year = parseInt(container.dataset.tuiCalendarCurrentYear, 10);
if (isNaN(month) || isNaN(year)) return;
// Update month select
const monthSelect = container.querySelector(
"[data-tui-calendar-month-select]",
);
if (monthSelect) {
monthSelect.value = month.toString();
}
// Update year select
const yearSelect = container.querySelector(
"[data-tui-calendar-year-select]",
);
if (yearSelect) {
yearSelect.value = year.toString();
}
}
// Form reset handling
document.addEventListener("reset", (e) => {
if (!e.target.matches("form")) return;
e.target
.querySelectorAll("[data-tui-calendar-container]")
.forEach((container) => {
const hiddenInput = findHiddenInput(container);
if (hiddenInput) {
hiddenInput.value = "";
}
// Clear selected date and reset to current month
container.removeAttribute("data-tui-calendar-selected-date");
const today = new Date();
container.dataset.tuiCalendarCurrentMonth = today.getMonth();
container.dataset.tuiCalendarCurrentYear = today.getFullYear();
renderCalendar(container);
});
});
// MutationObserver for dynamic content (framework-agnostic)
const observer = new MutationObserver(() => {
document
.querySelectorAll("[data-tui-calendar-container]")
.forEach((container) => {
const daysContainer = container.querySelector(
"[data-tui-calendar-days]",
);
// Only render if not already rendered
if (daysContainer && !daysContainer.children.length) {
renderCalendar(container);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
// Initialize calendars on page load
function initCalendars() {
document
.querySelectorAll("[data-tui-calendar-container]")
.forEach((container) => {
// Localize month names in native select options
const locale =
container.getAttribute("data-tui-calendar-locale-tag") || "en-US";
const monthNames = getMonthNames(locale);
const monthSelect = container.querySelector(
"[data-tui-calendar-month-select]",
);
if (monthSelect) {
const options = monthSelect.querySelectorAll("option");
options.forEach((option, index) => {
if (monthNames[index]) {
option.textContent = monthNames[index];
}
});
}
renderCalendar(container);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initCalendars);
} else {
initCalendars();
}
})();

File diff suppressed because one or more lines are too long

236
assets/js/carousel.js Normal file
View file

@ -0,0 +1,236 @@
(function() {
'use strict';
const autoplays = new Map();
let dragState = null;
// Click handling for navigation
document.addEventListener('click', (e) => {
const prevBtn = e.target.closest('[data-tui-carousel-prev]');
if (prevBtn) {
const carousel = prevBtn.closest('[data-tui-carousel]');
if (carousel) navigate(carousel, -1);
return;
}
const nextBtn = e.target.closest('[data-tui-carousel-next]');
if (nextBtn) {
const carousel = nextBtn.closest('[data-tui-carousel]');
if (carousel) navigate(carousel, 1);
return;
}
const indicator = e.target.closest('[data-tui-carousel-indicator]');
if (indicator) {
const carousel = indicator.closest('[data-tui-carousel]');
const index = parseInt(indicator.dataset.tuiCarouselIndicator);
if (carousel && !isNaN(index)) {
updateCarousel(carousel, index);
}
}
});
// Drag/swipe handling
function startDrag(e) {
const track = e.target.closest('[data-tui-carousel-track]');
if (!track) return;
const carousel = track.closest('[data-tui-carousel]');
if (!carousel) return;
e.preventDefault();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
dragState = {
carousel,
track,
startX: clientX,
currentX: clientX,
startTime: Date.now()
};
track.style.cursor = 'grabbing';
track.style.transition = 'none';
stopAutoplay(carousel);
}
function doDrag(e) {
if (!dragState) return;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
dragState.currentX = clientX;
const diff = clientX - dragState.startX;
const currentIndex = parseInt(dragState.carousel.dataset.tuiCarouselCurrent || '0');
const offset = -currentIndex * 100 + (diff / dragState.track.offsetWidth) * 100;
dragState.track.style.transform = `translateX(${offset}%)`;
}
function endDrag(e) {
if (!dragState) return;
const { carousel, track, startX, startTime } = dragState;
const clientX = e.changedTouches ? e.changedTouches[0].clientX : (e.clientX || dragState.currentX);
track.style.cursor = '';
track.style.transition = '';
const diff = startX - clientX;
const velocity = Math.abs(diff) / (Date.now() - startTime);
if (Math.abs(diff) > 50 || velocity > 0.5) {
navigate(carousel, diff > 0 ? 1 : -1);
} else {
const currentIndex = parseInt(carousel.dataset.tuiCarouselCurrent || '0');
updateCarousel(carousel, currentIndex);
}
dragState = null;
if (carousel.dataset.tuiCarouselAutoplay === 'true' && !carousel.matches(':hover')) {
startAutoplay(carousel);
}
}
document.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', doDrag);
document.addEventListener('mouseup', endDrag);
document.addEventListener('mouseleave', (e) => {
if (e.target === document.documentElement) endDrag(e);
});
document.addEventListener('touchstart', startDrag, { passive: false });
document.addEventListener('touchmove', doDrag, { passive: false });
document.addEventListener('touchend', endDrag, { passive: false });
// Navigation logic
function navigate(carousel, direction) {
const current = parseInt(carousel.dataset.tuiCarouselCurrent || '0');
const items = carousel.querySelectorAll('[data-tui-carousel-item]');
const count = items.length;
if (count === 0) return;
let next = current + direction;
if (carousel.dataset.tuiCarouselLoop === 'true') {
next = ((next % count) + count) % count;
} else {
next = Math.max(0, Math.min(next, count - 1));
}
updateCarousel(carousel, next);
}
function updateCarousel(carousel, index) {
const track = carousel.querySelector('[data-tui-carousel-track]');
const indicators = carousel.querySelectorAll('[data-tui-carousel-indicator]');
const prevBtn = carousel.querySelector('[data-tui-carousel-prev]');
const nextBtn = carousel.querySelector('[data-tui-carousel-next]');
const items = carousel.querySelectorAll('[data-tui-carousel-item]');
const count = items.length;
carousel.dataset.tuiCarouselCurrent = index;
if (track) {
track.style.transform = `translateX(-${index * 100}%)`;
}
indicators.forEach((ind, i) => {
ind.dataset.tuiCarouselActive = (i === index) ? 'true' : 'false';
ind.classList.toggle('bg-primary', i === index);
ind.classList.toggle('bg-foreground/30', i !== index);
});
const isLoop = carousel.dataset.tuiCarouselLoop === 'true';
if (prevBtn) {
prevBtn.disabled = !isLoop && index === 0;
prevBtn.classList.toggle('opacity-50', prevBtn.disabled);
}
if (nextBtn) {
nextBtn.disabled = !isLoop && index === count - 1;
nextBtn.classList.toggle('opacity-50', nextBtn.disabled);
}
}
// Autoplay functionality
function startAutoplay(carousel) {
if (carousel.dataset.tuiCarouselAutoplay !== 'true') return;
stopAutoplay(carousel);
const interval = parseInt(carousel.dataset.tuiCarouselInterval || '5000');
const id = setInterval(() => {
if (!document.contains(carousel)) {
stopAutoplay(carousel);
return;
}
if (carousel.matches(':hover') || dragState?.carousel === carousel) {
return;
}
navigate(carousel, 1);
}, interval);
autoplays.set(carousel, id);
}
function stopAutoplay(carousel) {
const id = autoplays.get(carousel);
if (id) {
clearInterval(id);
autoplays.delete(carousel);
}
}
// Intersection Observer for visibility management
const observedCarousels = new WeakSet();
const carouselObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const carousel = entry.target;
// Initialize display on first observation
if (!carousel.hasAttribute('data-tui-carousel-initialized')) {
carousel.setAttribute('data-tui-carousel-initialized', 'true');
const index = parseInt(carousel.dataset.tuiCarouselCurrent || '0');
updateCarousel(carousel, index);
}
// Handle autoplay if enabled
if (carousel.dataset.tuiCarouselAutoplay === 'true') {
if (entry.isIntersecting) {
startAutoplay(carousel);
} else {
stopAutoplay(carousel);
}
}
});
});
// Observe all carousels for visibility and initialization
function observeCarousels() {
document.querySelectorAll('[data-tui-carousel]').forEach(carousel => {
if (!observedCarousels.has(carousel)) {
observedCarousels.add(carousel);
carouselObserver.observe(carousel);
}
});
}
// Start observing
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', observeCarousels);
} else {
observeCarousels();
}
// Watch for dynamically added carousels
new MutationObserver(observeCarousels).observe(document.body, {
childList: true,
subtree: true
});
})();

214
assets/js/chart.js Normal file
View file

@ -0,0 +1,214 @@
import "./chartjs.js";
(function () {
"use strict";
const chartInstances = new Map();
function getThemeColors() {
const style = getComputedStyle(document.documentElement);
return {
foreground: style.getPropertyValue("--foreground").trim() || "#000",
background: style.getPropertyValue("--background").trim() || "#fff",
mutedForeground:
style.getPropertyValue("--muted-foreground").trim() || "#666",
border: style.getPropertyValue("--border").trim() || "#ccc",
};
}
function buildGeneratedChartConfig(chartConfig, colors) {
const isComplexChart = ["pie", "doughnut", "bar", "radar"].includes(
chartConfig.type,
);
const legendOptions = {
display: chartConfig.showLegend || false,
labels: { color: colors.foreground },
};
const tooltipOptions = {
backgroundColor: colors.background,
bodyColor: colors.mutedForeground,
titleColor: colors.foreground,
borderColor: colors.border,
borderWidth: 1,
};
const scalesOptions =
chartConfig.type === "radar"
? {
r: {
grid: {
color: colors.border,
display: chartConfig.showYGrid !== false,
},
ticks: {
color: colors.mutedForeground,
backdropColor: "transparent",
display: chartConfig.showYLabels !== false,
},
angleLines: {
color: colors.border,
display: chartConfig.showXGrid !== false,
},
pointLabels: {
color: colors.foreground,
font: { size: 12 },
},
border: {
display: chartConfig.showYAxis !== false,
color: colors.border,
},
beginAtZero: true,
},
}
: {
x: {
beginAtZero: true,
display:
chartConfig.showXLabels !== false ||
chartConfig.showXGrid !== false ||
chartConfig.showXAxis !== false,
border: {
display: chartConfig.showXAxis !== false,
color: colors.border,
},
ticks: {
display: chartConfig.showXLabels !== false,
color: colors.mutedForeground,
},
grid: {
display: chartConfig.showXGrid !== false,
color: colors.border,
},
stacked: chartConfig.stacked || false,
},
y: {
offset: true,
beginAtZero: chartConfig.beginAtZero !== false,
min: chartConfig.yMin,
max: chartConfig.yMax,
display:
chartConfig.showYLabels !== false ||
chartConfig.showYGrid !== false ||
chartConfig.showYAxis !== false,
border: {
display: chartConfig.showYAxis !== false,
color: colors.border,
},
ticks: {
display: chartConfig.showYLabels !== false,
color: colors.mutedForeground,
},
grid: {
display: chartConfig.showYGrid !== false,
color: colors.border,
},
stacked: chartConfig.stacked || false,
},
};
return {
...chartConfig,
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: isComplexChart,
axis: "xy",
mode: isComplexChart ? "nearest" : "index",
},
indexAxis: chartConfig.horizontal ? "y" : "x",
plugins: {
legend: legendOptions,
tooltip: tooltipOptions,
},
scales: scalesOptions,
},
};
}
function initChart(canvas) {
if (!canvas || !canvas.id || !canvas.hasAttribute("data-tui-chart-id"))
return;
if (chartInstances.has(canvas.id)) {
cleanupChart(canvas);
}
const dataId = canvas.getAttribute("data-tui-chart-id");
const dataElement = document.getElementById(dataId);
if (!dataElement) return;
try {
const chartPayload = JSON.parse(dataElement.textContent);
const colors = getThemeColors();
// eslint-disable-next-line no-undef
Chart.defaults.elements.point.radius = 0;
// eslint-disable-next-line no-undef
Chart.defaults.elements.point.hoverRadius = 5;
const finalChartConfig = chartPayload.rawConfig
? chartPayload.rawConfig
: buildGeneratedChartConfig(
chartPayload.generatedConfig || chartPayload,
colors,
);
// eslint-disable-next-line no-undef
chartInstances.set(canvas.id, new Chart(canvas, finalChartConfig));
} catch {}
}
function cleanupChart(canvas) {
if (!canvas || !canvas.id || !chartInstances.has(canvas.id)) return;
try {
chartInstances.get(canvas.id).destroy();
} finally {
chartInstances.delete(canvas.id);
}
}
function waitForChartAndInit() {
if (typeof Chart !== "undefined") {
document.querySelectorAll("canvas[data-tui-chart-id]").forEach(initChart);
setupObservers();
} else {
setTimeout(waitForChartAndInit, 100);
}
}
document.addEventListener("DOMContentLoaded", waitForChartAndInit);
function setupObservers() {
// Observe theme changes
let themeTimeout;
new MutationObserver(() => {
clearTimeout(themeTimeout);
themeTimeout = setTimeout(() => {
document
.querySelectorAll("canvas[data-tui-chart-id]")
.forEach((canvas) => {
if (chartInstances.has(canvas.id)) {
cleanupChart(canvas);
initChart(canvas);
}
});
}, 50);
}).observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "style"],
});
// Observe for new charts
new MutationObserver(() => {
document
.querySelectorAll("canvas[data-tui-chart-id]")
.forEach((canvas) => {
if (!chartInstances.has(canvas.id)) {
initChart(canvas);
}
});
}).observe(document.body, { childList: true, subtree: true });
}
})();

File diff suppressed because one or more lines are too long

11735
assets/js/chartjs.js Normal file

File diff suppressed because it is too large Load diff

82
assets/js/checkbox.js Normal file
View file

@ -0,0 +1,82 @@
(function () {
"use strict";
// Reactive Binding für checked property (wie bei Selectbox)
function enableReactiveBinding(input) {
if (input._tuiCheckbox) return;
input._tuiCheckbox = true;
var desc = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"checked"
);
if (!desc || !desc.set) return;
Object.defineProperty(input, "checked", {
get: desc.get,
set: function (v) {
var old = desc.get.call(this);
desc.set.call(this, v);
if (old !== v) {
this.dispatchEvent(new Event("change", { bubbles: true }));
}
},
configurable: true,
});
}
function updateParent(group) {
var parent = document.querySelector(
'[data-tui-checkbox-group="' + group + '"][data-tui-checkbox-parent]'
);
var children = document.querySelectorAll(
'[data-tui-checkbox-group="' + group + '"]:not([data-tui-checkbox-parent])'
);
if (!parent || !children.length) return;
var checked = 0;
children.forEach(function (c) {
if (c.checked) checked++;
});
parent.checked = checked === children.length;
parent.indeterminate = checked > 0 && checked < children.length;
}
function toggleChildren(group, checked) {
document
.querySelectorAll(
'[data-tui-checkbox-group="' + group + '"]:not([data-tui-checkbox-parent])'
)
.forEach(function (c) {
c.checked = checked;
});
}
document.addEventListener("change", function (e) {
var el = e.target;
if (!el.matches("[data-tui-checkbox-group]")) return;
var group = el.getAttribute("data-tui-checkbox-group");
if (el.hasAttribute("data-tui-checkbox-parent")) {
toggleChildren(group, el.checked);
} else {
updateParent(group);
}
});
function init() {
var groups = new Set();
document.querySelectorAll("[data-tui-checkbox-group]").forEach(function (el) {
groups.add(el.getAttribute("data-tui-checkbox-group"));
enableReactiveBinding(el);
});
groups.forEach(updateParent);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

70
assets/js/collapsible.js Normal file
View file

@ -0,0 +1,70 @@
(function () {
"use strict";
// Find the direct content element for a root (not nested ones)
function findDirectContent(root) {
const allContents = root.querySelectorAll('[data-tui-collapsible="content"]');
for (const content of allContents) {
if (content.closest('[data-tui-collapsible="root"]') === root) {
return content;
}
}
return null;
}
function toggle(trigger) {
const root = trigger.closest('[data-tui-collapsible="root"]');
if (!root) return;
const content = findDirectContent(root);
const isOpen = root.getAttribute("data-tui-collapsible-state") === "open";
const newState = isOpen ? "closed" : "open";
// Update states
root.setAttribute("data-tui-collapsible-state", newState);
trigger.setAttribute("aria-expanded", !isOpen);
// Toggle class on content for nested collapsible support
if (content) {
content.classList.toggle("tui-collapsible-open", !isOpen);
}
}
// Initialize collapsibles on page load
function initializeCollapsibles() {
document.querySelectorAll('[data-tui-collapsible="root"]').forEach((root) => {
const isOpen = root.getAttribute("data-tui-collapsible-state") === "open";
const content = findDirectContent(root);
if (content) {
content.classList.toggle("tui-collapsible-open", isOpen);
}
});
}
// Click handler
document.addEventListener("click", (e) => {
const trigger = e.target.closest('[data-tui-collapsible="trigger"]');
if (trigger) {
e.preventDefault();
toggle(trigger);
}
});
// Keyboard handler
document.addEventListener("keydown", (e) => {
if (e.key !== " " && e.key !== "Enter") return;
const trigger = e.target.closest('[data-tui-collapsible="trigger"]');
if (trigger) {
e.preventDefault();
toggle(trigger);
}
});
// Run on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeCollapsibles);
} else {
initializeCollapsibles();
}
})();

101
assets/js/copybutton.js Normal file
View file

@ -0,0 +1,101 @@
(function () {
"use strict";
// Copy button click delegation
document.addEventListener("click", (e) => {
const copyButton = e.target.closest("[data-copy-button]");
if (!copyButton) return;
const targetId = copyButton.dataset.targetId;
if (!targetId) {
console.error("CopyButton: No target-id specified");
return;
}
const targetElement = document.getElementById(targetId);
if (!targetElement) {
console.error(`CopyButton: Element with id '${targetId}' not found`);
return;
}
// Smart detection: use value for inputs/textareas, textContent for everything else
let textToCopy = "";
if (targetElement.value !== undefined) {
textToCopy = targetElement.value;
} else {
textToCopy = targetElement.textContent || "";
}
// Get icon elements
const iconClipboard = copyButton.querySelector(
"[data-copy-icon-clipboard]",
);
const iconCheck = copyButton.querySelector("[data-copy-icon-check]");
if (!iconClipboard || !iconCheck) return;
const showCopied = () => {
iconClipboard.style.display = "none";
iconCheck.style.display = "inline";
// Update tooltip text if it exists
const tooltipText = copyButton
.closest(".inline-block")
?.parentElement?.parentElement?.querySelector(
"[data-copy-tooltip-text]",
);
const originalText = tooltipText?.textContent;
if (tooltipText) {
tooltipText.textContent = "Copied!";
}
setTimeout(() => {
iconClipboard.style.display = "inline";
iconCheck.style.display = "none";
// Restore original tooltip text
if (tooltipText && originalText) {
tooltipText.textContent = originalText;
}
}, 2000);
};
// Try to copy using modern API first
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
.writeText(textToCopy.trim())
.then(showCopied)
.catch((err) => {
console.error("CopyButton: Failed to copy text", err);
fallbackCopy(textToCopy.trim(), showCopied);
});
} else {
// Fallback for older browsers or non-secure contexts
fallbackCopy(textToCopy.trim(), showCopied);
}
});
// Fallback copy method for older browsers
function fallbackCopy(text, callback) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.top = "-9999px";
textArea.style.left = "-9999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand("copy");
if (successful) {
callback();
} else {
console.error("CopyButton: Fallback copy failed");
}
} catch (err) {
console.error("CopyButton: Fallback copy error", err);
}
document.body.removeChild(textArea);
}
})();

View file

@ -1,251 +1,199 @@
(function () {
"use strict";
"use strict";
/**
* Reactive Binding for hidden inputs
*
* Problem: Setting input.value programmatically (e.g., via Datastar/Alpine)
* does NOT fire 'input' events - this is standard browser behavior since the 90s.
*
* Solution: Override the value setter to dispatch 'input' events on change.
* This is the same pattern used by Vue.js, MobX, and other reactive frameworks.
*/
function enableReactiveBinding(input) {
if (input._tui) return;
input._tui = true;
/**
* Reactive Binding for hidden inputs
*
* Problem: Setting input.value programmatically (e.g., via Datastar/Alpine)
* does NOT fire 'input' events - this is standard browser behavior since the 90s.
*
* Solution: Override the value setter to dispatch 'input' events on change.
* This is the same pattern used by Vue.js, MobX, and other reactive frameworks.
*/
function enableReactiveBinding(input) {
if (input._tui) return;
input._tui = true;
const desc = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
);
if (!desc?.set) return;
const desc = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
);
if (!desc?.set) return;
Object.defineProperty(input, "value", {
get: desc.get,
set(v) {
const old = this.value;
desc.set.call(this, v);
if (old !== v) {
this.dispatchEvent(new Event("input", { bubbles: true }));
}
},
configurable: true,
});
}
Object.defineProperty(input, "value", {
get: desc.get,
set(v) {
const old = this.value;
desc.set.call(this, v);
if (old !== v) {
this.dispatchEvent(new Event("input", { bubbles: true }));
}
},
configurable: true,
});
}
function todayISO() {
var d = new Date();
return (
d.getFullYear() +
"-" +
String(d.getMonth() + 1).padStart(2, "0") +
"-" +
String(d.getDate()).padStart(2, "0")
);
}
// Utility functions
function parseISODate(isoString) {
if (!isoString) return null;
const parts = isoString.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!parts) return null;
function setDefaultToday(input) {
if (!input.value) {
input.value = todayISO();
}
const form = input.closest("form");
if (form) {
form.addEventListener("reset", function () {
setTimeout(() => {
input.value = todayISO();
}, 0);
});
}
}
const year = parseInt(parts[1], 10);
const month = parseInt(parts[2], 10) - 1;
const day = parseInt(parts[3], 10);
const date = new Date(Date.UTC(year, month, day));
// Utility functions
function parseISODate(isoString) {
if (!isoString) return null;
const parts = isoString.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!parts) return null;
if (
date.getUTCFullYear() === year &&
date.getUTCMonth() === month &&
date.getUTCDate() === day
) {
return date;
}
return null;
}
const year = parseInt(parts[1], 10);
const month = parseInt(parts[2], 10) - 1;
const day = parseInt(parts[3], 10);
const date = new Date(Date.UTC(year, month, day));
function formatDate(date, format, locale) {
if (!date || isNaN(date.getTime())) return "";
if (
date.getUTCFullYear() === year &&
date.getUTCMonth() === month &&
date.getUTCDate() === day
) {
return date;
}
return null;
}
const options = { timeZone: "UTC" };
const formatMap = {
"locale-short": "short",
"locale-long": "long",
"locale-full": "full",
"locale-medium": "medium",
};
function formatDate(date, format, locale) {
if (!date || isNaN(date.getTime())) return "";
options.dateStyle = formatMap[format] || "medium";
const options = { timeZone: "UTC" };
const formatMap = {
"locale-short": "short",
"locale-long": "long",
"locale-full": "full",
"locale-medium": "medium",
};
try {
return new Intl.DateTimeFormat(locale, options).format(date);
} catch (e) {
// Fallback to ISO format
const year = date.getUTCFullYear();
const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
const day = date.getUTCDate().toString().padStart(2, "0");
return `${year}-${month}-${day}`;
}
}
options.dateStyle = formatMap[format] || "medium";
function findRoot(element) {
return element?.closest("[data-tui-datepicker-root]") || null;
}
try {
return new Intl.DateTimeFormat(locale, options).format(date);
} catch (e) {
// Fallback to ISO format
const year = date.getUTCFullYear();
const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
const day = date.getUTCDate().toString().padStart(2, "0");
return `${year}-${month}-${day}`;
}
}
function findElements(root) {
const trigger = root?.querySelector("[data-tui-datepicker='true']");
const calendar = root?.querySelector("[data-tui-calendar-container]");
const hiddenInput = root?.querySelector(
"[data-tui-datepicker-hidden-input]",
);
const display = trigger?.querySelector("[data-tui-datepicker-display]");
// Find elements
function findElements(trigger) {
const calendarId = trigger.id + "-calendar-instance";
const calendar = document.getElementById(calendarId);
const hiddenInput =
document.getElementById(trigger.id + "-hidden") ||
trigger.parentElement?.querySelector(
"[data-tui-datepicker-hidden-input]",
);
const display = trigger.querySelector("[data-tui-datepicker-display]");
return { trigger, calendar, hiddenInput, display };
}
return { calendar, hiddenInput, display };
}
function closePopover(root) {
const popoverContent = root?.querySelector("[data-tui-popover-content]");
if (!popoverContent?.matches(":popover-open")) return;
// Update display
function updateDisplay(trigger) {
const elements = findElements(trigger);
if (!elements.display || !elements.hiddenInput) return;
try {
popoverContent.hidePopover();
} catch {
// ignore
}
}
const format =
trigger.getAttribute("data-tui-datepicker-display-format") ||
"locale-medium";
const locale =
trigger.getAttribute("data-tui-datepicker-locale-tag") || "en-US";
const placeholder =
trigger.getAttribute("data-tui-datepicker-placeholder") ||
"Select a date";
// Update display
function updateDisplay(root) {
const elements = findElements(root);
if (!elements.trigger || !elements.display || !elements.hiddenInput) return;
if (elements.hiddenInput.value) {
const date = parseISODate(elements.hiddenInput.value);
if (date) {
elements.display.textContent = formatDate(date, format, locale);
elements.display.classList.remove("text-muted-foreground");
return;
}
}
const format =
elements.trigger.getAttribute("data-tui-datepicker-display-format") ||
"locale-medium";
const locale =
elements.trigger.getAttribute("data-tui-datepicker-locale-tag") ||
"en-US";
const placeholder =
elements.trigger.getAttribute("data-tui-datepicker-placeholder") ||
"Select a date";
elements.display.textContent = placeholder;
elements.display.classList.add("text-muted-foreground");
}
if (elements.hiddenInput.value) {
const date = parseISODate(elements.hiddenInput.value);
if (date) {
elements.display.textContent = formatDate(date, format, locale);
elements.display.classList.remove("text-muted-foreground");
return;
}
}
// Handle calendar date selection
document.addEventListener("calendar-date-selected", (e) => {
// Find the datepicker trigger associated with this calendar
const calendar = e.target;
if (!calendar || !calendar.id.endsWith("-calendar-instance")) return;
elements.display.textContent = placeholder;
elements.display.classList.add("text-muted-foreground");
}
const triggerId = calendar.id.replace("-calendar-instance", "");
const trigger = document.getElementById(triggerId);
if (!trigger || !trigger.hasAttribute("data-tui-datepicker")) return;
function toISODate(date) {
if (!date || isNaN(date.getTime())) return "";
return date.toISOString().split("T")[0];
}
const elements = findElements(trigger);
if (!elements.display || !e.detail?.date) return;
// Handle calendar date selection
document.addEventListener("calendar-date-selected", (e) => {
const calendar = e.target;
const root = findRoot(calendar);
const elements = findElements(root);
if (!elements.hiddenInput || !e.detail?.date) return;
const format =
trigger.getAttribute("data-tui-datepicker-display-format") ||
"locale-medium";
const locale =
trigger.getAttribute("data-tui-datepicker-locale-tag") || "en-US";
elements.hiddenInput.value = toISODate(e.detail.date);
updateDisplay(root);
closePopover(root);
});
elements.display.textContent = formatDate(e.detail.date, format, locale);
elements.display.classList.remove("text-muted-foreground");
// Handle hidden input value changes (for reactive frameworks)
document.addEventListener("input", (e) => {
if (!e.target.matches("[data-tui-datepicker-hidden-input]")) return;
// Close the popover
if (window.closePopover) {
const popoverId =
trigger.getAttribute("aria-controls") || trigger.id + "-content";
window.closePopover(popoverId);
}
});
const root = findRoot(e.target);
if (root) {
updateDisplay(root);
}
});
// Handle hidden input value changes (for reactive frameworks)
document.addEventListener("input", (e) => {
if (!e.target.matches("[data-tui-datepicker-hidden-input]")) return;
// Form reset handling
document.addEventListener("reset", (e) => {
if (!e.target.matches("form")) return;
const trigger = document.getElementById(e.target.id.replace("-hidden", ""));
if (trigger) {
updateDisplay(trigger);
}
});
e.target.querySelectorAll("[data-tui-datepicker-root]").forEach((root) => {
const elements = findElements(root);
if (elements.hiddenInput) {
elements.hiddenInput.value = "";
}
updateDisplay(root);
});
});
// Form reset handling
document.addEventListener("reset", (e) => {
if (!e.target.matches("form")) return;
// Initialize datepickers
function initializeDatePickers() {
document.querySelectorAll("[data-tui-datepicker-root]").forEach((root) => {
const elements = findElements(root);
if (!elements.hiddenInput || elements.hiddenInput._tui) return;
e.target
.querySelectorAll('[data-tui-datepicker="true"]')
.forEach((trigger) => {
const elements = findElements(trigger);
if (elements.hiddenInput) {
elements.hiddenInput.value = "";
}
updateDisplay(trigger);
});
});
// Enable reactive binding for hidden input
enableReactiveBinding(elements.hiddenInput);
updateDisplay(root);
});
}
// Clear button handling
document.addEventListener("click", (e) => {
const trigger = e.target.closest('[data-tui-datepicker-clear="true"]');
if (!trigger) return;
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeDatePickers);
} else {
initializeDatePickers();
}
const triggerID = trigger.id;
const baseID = triggerID.replace(new RegExp("-clear-button" + "$"), "");
const datepicker = document.getElementById(baseID);
if (!datepicker) return;
const elements = findElements(datepicker);
if (elements.hiddenInput) {
elements.hiddenInput.value = "";
}
updateDisplay(datepicker);
});
// Initialize datepickers
function initializeDatePickers() {
document
.querySelectorAll('[data-tui-datepicker="true"]')
.forEach((trigger) => {
const elements = findElements(trigger);
if (!elements.hiddenInput || elements.hiddenInput._tui) return;
// Enable reactive binding for hidden input
enableReactiveBinding(elements.hiddenInput);
// If required, set default date to today
if (trigger.dataset.tuiDatepickerRequired === "true") {
setDefaultToday(elements.hiddenInput);
}
updateDisplay(trigger);
});
}
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeDatePickers);
} else {
initializeDatePickers();
}
// MutationObserver for dynamically added elements
new MutationObserver(initializeDatePickers).observe(document.body, {
childList: true,
subtree: true,
});
// MutationObserver for dynamically added elements
new MutationObserver(initializeDatePickers).observe(document.body, {
childList: true,
subtree: true,
});
})();

View file

@ -1 +1 @@
(()=>{(function(){"use strict";function p(t){if(t._tui)return;t._tui=!0;let e=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value");e?.set&&Object.defineProperty(t,"value",{get:e.get,set(a){let n=this.value;e.set.call(this,a),n!==a&&this.dispatchEvent(new Event("input",{bubbles:!0}))},configurable:!0})}function m(t){if(!t)return null;let e=t.match(/^(\d{4})-(\d{2})-(\d{2})$/);if(!e)return null;let a=parseInt(e[1],10),n=parseInt(e[2],10)-1,d=parseInt(e[3],10),i=new Date(Date.UTC(a,n,d));return i.getUTCFullYear()===a&&i.getUTCMonth()===n&&i.getUTCDate()===d?i:null}function s(t,e,a){if(!t||isNaN(t.getTime()))return"";let n={timeZone:"UTC"},d={"locale-short":"short","locale-long":"long","locale-full":"full","locale-medium":"medium"};n.dateStyle=d[e]||"medium";try{return new Intl.DateTimeFormat(a,n).format(t)}catch{let u=t.getUTCFullYear(),l=(t.getUTCMonth()+1).toString().padStart(2,"0"),f=t.getUTCDate().toString().padStart(2,"0");return`${u}-${l}-${f}`}}function r(t){let e=t.id+"-calendar-instance",a=document.getElementById(e),n=document.getElementById(t.id+"-hidden")||t.parentElement?.querySelector("[data-tui-datepicker-hidden-input]"),d=t.querySelector("[data-tui-datepicker-display]");return{calendar:a,hiddenInput:n,display:d}}function o(t){let e=r(t);if(!e.display||!e.hiddenInput)return;let a=t.getAttribute("data-tui-datepicker-display-format")||"locale-medium",n=t.getAttribute("data-tui-datepicker-locale-tag")||"en-US",d=t.getAttribute("data-tui-datepicker-placeholder")||"Select a date";if(e.hiddenInput.value){let i=m(e.hiddenInput.value);if(i){e.display.textContent=s(i,a,n),e.display.classList.remove("text-muted-foreground");return}}e.display.textContent=d,e.display.classList.add("text-muted-foreground")}document.addEventListener("calendar-date-selected",t=>{let e=t.target;if(!e||!e.id.endsWith("-calendar-instance"))return;let a=e.id.replace("-calendar-instance",""),n=document.getElementById(a);if(!n||!n.hasAttribute("data-tui-datepicker"))return;let d=r(n);if(!d.display||!t.detail?.date)return;let i=n.getAttribute("data-tui-datepicker-display-format")||"locale-medium",u=n.getAttribute("data-tui-datepicker-locale-tag")||"en-US";if(d.display.textContent=s(t.detail.date,i,u),d.display.classList.remove("text-muted-foreground"),window.closePopover){let l=n.getAttribute("aria-controls")||n.id+"-content";window.closePopover(l)}}),document.addEventListener("input",t=>{if(!t.target.matches("[data-tui-datepicker-hidden-input]"))return;let e=document.getElementById(t.target.id.replace("-hidden",""));e&&o(e)}),document.addEventListener("reset",t=>{t.target.matches("form")&&t.target.querySelectorAll('[data-tui-datepicker="true"]').forEach(e=>{let a=r(e);a.hiddenInput&&(a.hiddenInput.value=""),o(e)})});function c(){document.querySelectorAll('[data-tui-datepicker="true"]').forEach(t=>{let e=r(t);!e.hiddenInput||e.hiddenInput._tui||(p(e.hiddenInput),o(t))})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",c):c(),new MutationObserver(c).observe(document.body,{childList:!0,subtree:!0})})();})();
(()=>{(function(){"use strict";function l(e){if(e._tui)return;e._tui=!0;let t=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value");t?.set&&Object.defineProperty(e,"value",{get:t.get,set(n){let r=this.value;t.set.call(this,n),r!==n&&this.dispatchEvent(new Event("input",{bubbles:!0}))},configurable:!0})}function s(e){if(!e)return null;let t=e.match(/^(\d{4})-(\d{2})-(\d{2})$/);if(!t)return null;let n=parseInt(t[1],10),r=parseInt(t[2],10)-1,a=parseInt(t[3],10),i=new Date(Date.UTC(n,r,a));return i.getUTCFullYear()===n&&i.getUTCMonth()===r&&i.getUTCDate()===a?i:null}function p(e,t,n){if(!e||isNaN(e.getTime()))return"";let r={timeZone:"UTC"},a={"locale-short":"short","locale-long":"long","locale-full":"full","locale-medium":"medium"};r.dateStyle=a[t]||"medium";try{return new Intl.DateTimeFormat(n,r).format(e)}catch{let g=e.getUTCFullYear(),h=(e.getUTCMonth()+1).toString().padStart(2,"0"),y=e.getUTCDate().toString().padStart(2,"0");return`${g}-${h}-${y}`}}function c(e){return e?.closest("[data-tui-datepicker-root]")||null}function o(e){let t=e?.querySelector("[data-tui-datepicker='true']"),n=e?.querySelector("[data-tui-calendar-container]"),r=e?.querySelector("[data-tui-datepicker-hidden-input]"),a=t?.querySelector("[data-tui-datepicker-display]");return{trigger:t,calendar:n,hiddenInput:r,display:a}}function f(e){let t=e?.querySelector("[data-tui-popover-content]");if(t?.matches(":popover-open"))try{t.hidePopover()}catch{}}function d(e){let t=o(e);if(!t.trigger||!t.display||!t.hiddenInput)return;let n=t.trigger.getAttribute("data-tui-datepicker-display-format")||"locale-medium",r=t.trigger.getAttribute("data-tui-datepicker-locale-tag")||"en-US",a=t.trigger.getAttribute("data-tui-datepicker-placeholder")||"Select a date";if(t.hiddenInput.value){let i=s(t.hiddenInput.value);if(i){t.display.textContent=p(i,n,r),t.display.classList.remove("text-muted-foreground");return}}t.display.textContent=a,t.display.classList.add("text-muted-foreground")}function m(e){return!e||isNaN(e.getTime())?"":e.toISOString().split("T")[0]}document.addEventListener("calendar-date-selected",e=>{let t=e.target,n=c(t),r=o(n);!r.hiddenInput||!e.detail?.date||(r.hiddenInput.value=m(e.detail.date),d(n),f(n))}),document.addEventListener("input",e=>{if(!e.target.matches("[data-tui-datepicker-hidden-input]"))return;let t=c(e.target);t&&d(t)}),document.addEventListener("reset",e=>{e.target.matches("form")&&e.target.querySelectorAll("[data-tui-datepicker-root]").forEach(t=>{let n=o(t);n.hiddenInput&&(n.hiddenInput.value=""),d(t)})});function u(){document.querySelectorAll("[data-tui-datepicker-root]").forEach(e=>{let t=o(e);!t.hiddenInput||t.hiddenInput._tui||(l(t.hiddenInput),d(e))})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",u):u(),new MutationObserver(u).observe(document.body,{childList:!0,subtree:!0})})();})();

215
assets/js/dialog.js Normal file
View file

@ -0,0 +1,215 @@
(function () {
"use strict";
const CLOSE_DURATION_MS = 200;
function getRoot(target) {
if (!target) return null;
if (typeof target === "string") {
const byId = document.getElementById(target);
if (byId?.matches?.("[data-tui-dialog]")) {
return byId;
}
try {
return document.querySelector(target)?.closest("[data-tui-dialog]") || null;
} catch {
return null;
}
}
if (target.matches?.("[data-tui-dialog]")) {
return target;
}
return target.closest?.("[data-tui-dialog]") || null;
}
function getDialog(root) {
if (!root) return null;
return ensureDialog(root.querySelector("[data-tui-dialog-content]"));
}
function getOwnedTriggers(root) {
if (!root) return [];
return Array.from(root.querySelectorAll("[data-tui-dialog-trigger]")).filter(
(trigger) => !trigger.hasAttribute("data-tui-dialog-target"),
);
}
function getTargetedTriggers(targetId) {
if (!targetId) return [];
return Array.from(
document.querySelectorAll(
`[data-tui-dialog-trigger][data-tui-dialog-target="${targetId}"]`,
),
);
}
function getTargetValue(element) {
const target = element?.getAttribute("data-tui-dialog-target");
return target && target.trim() ? target.trim() : null;
}
function getRootForElement(element) {
return getRoot(getTargetValue(element) || element);
}
function ensureDialog(dialog) {
if (!dialog || dialog.dataset.tuiDialogInitialized === "true") return dialog;
dialog.dataset.tuiDialogInitialized = "true";
dialog.addEventListener("cancel", (event) => {
const root = getRoot(dialog);
if (root?.hasAttribute("data-tui-dialog-disable-esc")) {
event.preventDefault();
return;
}
event.preventDefault();
closeDialog(root);
});
dialog.addEventListener("close", () => {
const root = getRoot(dialog);
window.clearTimeout(dialog._tuiDialogCloseTimer);
delete dialog._tuiDialogCloseTimer;
dialog.removeAttribute("data-tui-dialog-closing");
root?.removeAttribute("data-tui-dialog-closing");
updateState(getRoot(dialog), false);
});
dialog.addEventListener("click", (event) => {
if (event.target !== dialog) return;
const root = getRoot(dialog);
if (root?.hasAttribute("data-tui-dialog-disable-click-away")) {
return;
}
closeDialog(root);
});
return dialog;
}
function updateState(root, isOpen) {
const dialog = getDialog(root);
dialog?.setAttribute("data-tui-dialog-open", isOpen ? "true" : "false");
root?.setAttribute("data-tui-dialog-open", isOpen ? "true" : "false");
getOwnedTriggers(root).forEach((trigger) => {
trigger.setAttribute("data-tui-dialog-trigger-open", isOpen ? "true" : "false");
});
if (root?.id) {
getTargetedTriggers(root.id).forEach((trigger) => {
trigger.setAttribute("data-tui-dialog-trigger-open", isOpen ? "true" : "false");
});
}
}
function openDialog(target) {
const root = getRoot(target);
const dialog = getDialog(root);
if (!dialog) return;
window.clearTimeout(dialog._tuiDialogCloseTimer);
delete dialog._tuiDialogCloseTimer;
dialog.removeAttribute("data-tui-dialog-closing");
root?.removeAttribute("data-tui-dialog-closing");
if (!dialog.open) {
try {
dialog.showModal();
} catch {
return;
}
}
updateState(root, true);
}
function closeDialog(target) {
const root = getRoot(target);
const dialog = getDialog(root);
if (!dialog) return;
if (!dialog.open) {
updateState(root, false);
return;
}
if (dialog.dataset.tuiDialogClosing === "true") {
return;
}
dialog.setAttribute("data-tui-dialog-closing", "true");
root?.setAttribute("data-tui-dialog-closing", "true");
updateState(root, false);
dialog._tuiDialogCloseTimer = window.setTimeout(() => {
if (dialog.open) {
dialog.close();
} else {
dialog.removeAttribute("data-tui-dialog-closing");
root?.removeAttribute("data-tui-dialog-closing");
}
}, CLOSE_DURATION_MS);
}
function isDialogOpen(target) {
return getDialog(getRoot(target))?.open || false;
}
function toggleDialog(target) {
isDialogOpen(target) ? closeDialog(target) : openDialog(target);
}
function initDialogs(root = document) {
root.querySelectorAll("[data-tui-dialog]").forEach((dialogRoot) => {
const dialog = getDialog(dialogRoot);
if (!dialog) return;
ensureDialog(dialog);
if (dialog.getAttribute("data-tui-dialog-initial-open") === "true") {
openDialog(dialogRoot);
} else {
updateState(dialogRoot, dialog.open);
}
});
}
document.addEventListener("click", (event) => {
const trigger = event.target.closest("[data-tui-dialog-trigger]");
if (trigger) {
toggleDialog(getRootForElement(trigger));
return;
}
const closeButton = event.target.closest("[data-tui-dialog-close]");
if (closeButton) {
closeDialog(getRootForElement(closeButton));
}
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => initDialogs());
} else {
initDialogs();
}
window.tui = window.tui || {};
window.tui.dialog = {
open: openDialog,
close: closeDialog,
toggle: toggleDialog,
isOpen: isDialogOpen,
};
})();

View file

@ -1 +1 @@
(()=>{(function(){"use strict";function u(t){let a=document.querySelector(`[data-tui-dialog-backdrop][data-dialog-instance="${t}"]`),e=document.querySelector(`[data-tui-dialog-content][data-dialog-instance="${t}"]`);!a||!e||(a.removeAttribute("data-tui-dialog-hidden"),e.removeAttribute("data-tui-dialog-hidden"),requestAnimationFrame(()=>{a.setAttribute("data-tui-dialog-open","true"),e.setAttribute("data-tui-dialog-open","true"),document.body.style.overflow="hidden",document.querySelectorAll(`[data-tui-dialog-trigger][data-dialog-instance="${t}"]`).forEach(o=>{o.setAttribute("data-tui-dialog-trigger-open","true")}),e.hasAttribute("data-tui-dialog-disable-autofocus")||setTimeout(()=>{e.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')?.focus()},50)}))}function n(t){let a=document.querySelector(`[data-tui-dialog-backdrop][data-dialog-instance="${t}"]`),e=document.querySelector(`[data-tui-dialog-content][data-dialog-instance="${t}"]`);!a||!e||(a.setAttribute("data-tui-dialog-open","false"),e.setAttribute("data-tui-dialog-open","false"),document.querySelectorAll(`[data-tui-dialog-trigger][data-dialog-instance="${t}"]`).forEach(i=>{i.setAttribute("data-tui-dialog-trigger-open","false")}),setTimeout(()=>{a.setAttribute("data-tui-dialog-hidden","true"),e.setAttribute("data-tui-dialog-hidden","true"),document.querySelector('[data-tui-dialog-content][data-tui-dialog-open="true"]')||(document.body.style.overflow="")},300))}function l(t){let a=t.getAttribute("data-dialog-instance");if(a)return a;let e=t.closest("[data-tui-dialog]");return e?e.getAttribute("data-dialog-instance"):null}function r(t){return document.querySelector(`[data-tui-dialog-content][data-dialog-instance="${t}"]`)?.getAttribute("data-tui-dialog-open")==="true"||!1}function c(t){r(t)?n(t):u(t)}document.addEventListener("click",t=>{let a=t.target.closest("[data-tui-dialog-trigger]");if(a){let o=a.getAttribute("data-dialog-instance");if(!o)return;c(o);return}let e=t.target.closest("[data-tui-dialog-close]");if(e){let d=e.getAttribute("data-tui-dialog-close")||l(e);d&&n(d);return}let i=t.target.closest("[data-tui-dialog-backdrop]");if(i){let o=i.getAttribute("data-dialog-instance");if(!o)return;let d=document.querySelector(`[data-tui-dialog][data-dialog-instance="${o}"]`),s=document.querySelector(`[data-tui-dialog-content][data-dialog-instance="${o}"]`);d?.hasAttribute("data-tui-dialog-disable-click-away")||s?.hasAttribute("data-tui-dialog-disable-click-away")||n(o)}}),document.addEventListener("keydown",t=>{if(t.key==="Escape"){let a=document.querySelectorAll('[data-tui-dialog-content][data-tui-dialog-open="true"]');if(a.length===0)return;let e=a[a.length-1],i=e.getAttribute("data-dialog-instance");if(!i)return;document.querySelector(`[data-tui-dialog][data-dialog-instance="${i}"]`)?.hasAttribute("data-tui-dialog-disable-esc")||e?.hasAttribute("data-tui-dialog-disable-esc")||n(i)}}),document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll('[data-tui-dialog-content][data-tui-dialog-open="true"]').length>0&&(document.body.style.overflow="hidden")}),new MutationObserver(()=>{document.querySelector('[data-tui-dialog-content][data-tui-dialog-open="true"]')||(document.body.style.overflow="")}).observe(document.body,{childList:!0,subtree:!0}),window.tui=window.tui||{},window.tui.dialog={open:u,close:n,toggle:c,isOpen:r}})();})();
(()=>{(function(){"use strict";function r(t){if(!t)return null;if(typeof t=="string"){let e=document.getElementById(t);if(e?.matches?.("[data-tui-dialog]"))return e;try{return document.querySelector(t)?.closest("[data-tui-dialog]")||null}catch{return null}}return t.matches?.("[data-tui-dialog]")?t:t.closest?.("[data-tui-dialog]")||null}function a(t){return t?s(t.querySelector("[data-tui-dialog-content]")):null}function m(t){return t?Array.from(t.querySelectorAll("[data-tui-dialog-trigger]")).filter(e=>!e.hasAttribute("data-tui-dialog-target")):[]}function A(t){return t?Array.from(document.querySelectorAll(`[data-tui-dialog-trigger][data-tui-dialog-target="${t}"]`)):[]}function b(t){let e=t?.getAttribute("data-tui-dialog-target");return e&&e.trim()?e.trim():null}function d(t){return r(b(t)||t)}function s(t){return!t||t.dataset.tuiDialogInitialized==="true"||(t.dataset.tuiDialogInitialized="true",t.addEventListener("cancel",e=>{let i=r(t);if(i?.hasAttribute("data-tui-dialog-disable-esc")){e.preventDefault();return}e.preventDefault(),u(i)}),t.addEventListener("close",()=>{let e=r(t);window.clearTimeout(t._tuiDialogCloseTimer),delete t._tuiDialogCloseTimer,t.removeAttribute("data-tui-dialog-closing"),e?.removeAttribute("data-tui-dialog-closing"),o(r(t),!1)}),t.addEventListener("click",e=>{if(e.target!==t)return;let i=r(t);i?.hasAttribute("data-tui-dialog-disable-click-away")||u(i)})),t}function o(t,e){a(t)?.setAttribute("data-tui-dialog-open",e?"true":"false"),t?.setAttribute("data-tui-dialog-open",e?"true":"false"),m(t).forEach(l=>{l.setAttribute("data-tui-dialog-trigger-open",e?"true":"false")}),t?.id&&A(t.id).forEach(l=>{l.setAttribute("data-tui-dialog-trigger-open",e?"true":"false")})}function n(t){let e=r(t),i=a(e);if(i){if(window.clearTimeout(i._tuiDialogCloseTimer),delete i._tuiDialogCloseTimer,i.removeAttribute("data-tui-dialog-closing"),e?.removeAttribute("data-tui-dialog-closing"),!i.open)try{i.showModal()}catch{return}o(e,!0)}}function u(t){let e=r(t),i=a(e);if(i){if(!i.open){o(e,!1);return}i.dataset.tuiDialogClosing!=="true"&&(i.setAttribute("data-tui-dialog-closing","true"),e?.setAttribute("data-tui-dialog-closing","true"),o(e,!1),i._tuiDialogCloseTimer=window.setTimeout(()=>{i.open?i.close():(i.removeAttribute("data-tui-dialog-closing"),e?.removeAttribute("data-tui-dialog-closing"))},200))}}function c(t){return a(r(t))?.open||!1}function g(t){c(t)?u(t):n(t)}function f(t=document){t.querySelectorAll("[data-tui-dialog]").forEach(e=>{let i=a(e);i&&(s(i),i.getAttribute("data-tui-dialog-initial-open")==="true"?n(e):o(e,i.open))})}document.addEventListener("click",t=>{let e=t.target.closest("[data-tui-dialog-trigger]");if(e){g(d(e));return}let i=t.target.closest("[data-tui-dialog-close]");i&&u(d(i))}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>f()):f(),window.tui=window.tui||{},window.tui.dialog={open:n,close:u,toggle:g,isOpen:c}})();})();

20
assets/js/dropdown.js Normal file
View file

@ -0,0 +1,20 @@
(function () {
'use strict';
document.addEventListener('click', (e) => {
const item = e.target.closest('[data-tui-dropdown-item]');
if (!item ||
item.hasAttribute('data-tui-dropdown-submenu-trigger') ||
item.getAttribute('data-tui-dropdown-prevent-close') === 'true') return;
const popoverRoot = item.closest('[data-tui-popover-root]');
const popoverContent = popoverRoot?.querySelector(':scope > [data-tui-popover-content]');
if (!popoverContent?.matches(':popover-open')) return;
try {
popoverContent.hidePopover();
} catch {
// ignore
}
});
})();

View file

@ -1 +1 @@
(()=>{(function(){"use strict";document.addEventListener("click",o=>{let t=o.target.closest("[data-tui-dropdown-item]");if(!t||t.hasAttribute("data-tui-dropdown-submenu-trigger")||t.getAttribute("data-tui-dropdown-prevent-close")==="true")return;let e=t.closest("[data-tui-popover-id]");if(!e)return;let i=e.getAttribute("data-tui-popover-id")||e.id;window.closePopover&&window.closePopover(i)})})();})();
(()=>{(function(){"use strict";document.addEventListener("click",e=>{let t=e.target.closest("[data-tui-dropdown-item]");if(!t||t.hasAttribute("data-tui-dropdown-submenu-trigger")||t.getAttribute("data-tui-dropdown-prevent-close")==="true")return;let o=t.closest("[data-tui-popover-root]")?.querySelector(":scope > [data-tui-popover-content]");if(o?.matches(":popover-open"))try{o.hidePopover()}catch{}})})();})();

View file

@ -0,0 +1,858 @@
// https://cdn.jsdelivr.net/npm/@floating-ui/core@1.7.0
!(function (t, e) {
"object" == typeof exports && "undefined" != typeof module
? e(exports)
: "function" == typeof define && define.amd
? define(["exports"], e)
: e(
((t =
"undefined" != typeof globalThis
? globalThis
: t || self).FloatingUICore = {})
);
})(this, function (t) {
"use strict";
const e = ["top", "right", "bottom", "left"],
n = ["start", "end"],
i = e.reduce((t, e) => t.concat(e, e + "-" + n[0], e + "-" + n[1]), []),
o = Math.min,
r = Math.max,
a = { left: "right", right: "left", bottom: "top", top: "bottom" },
l = { start: "end", end: "start" };
function s(t, e, n) {
return r(t, o(e, n));
}
function f(t, e) {
return "function" == typeof t ? t(e) : t;
}
function c(t) {
return t.split("-")[0];
}
function u(t) {
return t.split("-")[1];
}
function m(t) {
return "x" === t ? "y" : "x";
}
function d(t) {
return "y" === t ? "height" : "width";
}
function g(t) {
return ["top", "bottom"].includes(c(t)) ? "y" : "x";
}
function p(t) {
return m(g(t));
}
function h(t, e, n) {
void 0 === n && (n = !1);
const i = u(t),
o = p(t),
r = d(o);
let a =
"x" === o
? i === (n ? "end" : "start")
? "right"
: "left"
: "start" === i
? "bottom"
: "top";
return e.reference[r] > e.floating[r] && (a = w(a)), [a, w(a)];
}
function y(t) {
return t.replace(/start|end/g, (t) => l[t]);
}
function w(t) {
return t.replace(/left|right|bottom|top/g, (t) => a[t]);
}
function x(t) {
return "number" != typeof t
? (function (t) {
return { top: 0, right: 0, bottom: 0, left: 0, ...t };
})(t)
: { top: t, right: t, bottom: t, left: t };
}
function v(t) {
const { x: e, y: n, width: i, height: o } = t;
return {
width: i,
height: o,
top: n,
left: e,
right: e + i,
bottom: n + o,
x: e,
y: n,
};
}
function b(t, e, n) {
let { reference: i, floating: o } = t;
const r = g(e),
a = p(e),
l = d(a),
s = c(e),
f = "y" === r,
m = i.x + i.width / 2 - o.width / 2,
h = i.y + i.height / 2 - o.height / 2,
y = i[l] / 2 - o[l] / 2;
let w;
switch (s) {
case "top":
w = { x: m, y: i.y - o.height };
break;
case "bottom":
w = { x: m, y: i.y + i.height };
break;
case "right":
w = { x: i.x + i.width, y: h };
break;
case "left":
w = { x: i.x - o.width, y: h };
break;
default:
w = { x: i.x, y: i.y };
}
switch (u(e)) {
case "start":
w[a] -= y * (n && f ? -1 : 1);
break;
case "end":
w[a] += y * (n && f ? -1 : 1);
}
return w;
}
async function A(t, e) {
var n;
void 0 === e && (e = {});
const { x: i, y: o, platform: r, rects: a, elements: l, strategy: s } = t,
{
boundary: c = "clippingAncestors",
rootBoundary: u = "viewport",
elementContext: m = "floating",
altBoundary: d = !1,
padding: g = 0,
} = f(e, t),
p = x(g),
h = l[d ? ("floating" === m ? "reference" : "floating") : m],
y = v(
await r.getClippingRect({
element:
null ==
(n = await (null == r.isElement ? void 0 : r.isElement(h))) || n
? h
: h.contextElement ||
(await (null == r.getDocumentElement
? void 0
: r.getDocumentElement(l.floating))),
boundary: c,
rootBoundary: u,
strategy: s,
})
),
w =
"floating" === m
? { x: i, y: o, width: a.floating.width, height: a.floating.height }
: a.reference,
b = await (null == r.getOffsetParent
? void 0
: r.getOffsetParent(l.floating)),
A = ((await (null == r.isElement ? void 0 : r.isElement(b))) &&
(await (null == r.getScale ? void 0 : r.getScale(b)))) || {
x: 1,
y: 1,
},
R = v(
r.convertOffsetParentRelativeRectToViewportRelativeRect
? await r.convertOffsetParentRelativeRectToViewportRelativeRect({
elements: l,
rect: w,
offsetParent: b,
strategy: s,
})
: w
);
return {
top: (y.top - R.top + p.top) / A.y,
bottom: (R.bottom - y.bottom + p.bottom) / A.y,
left: (y.left - R.left + p.left) / A.x,
right: (R.right - y.right + p.right) / A.x,
};
}
function R(t, e) {
return {
top: t.top - e.height,
right: t.right - e.width,
bottom: t.bottom - e.height,
left: t.left - e.width,
};
}
function P(t) {
return e.some((e) => t[e] >= 0);
}
function D(t) {
const e = o(...t.map((t) => t.left)),
n = o(...t.map((t) => t.top));
return {
x: e,
y: n,
width: r(...t.map((t) => t.right)) - e,
height: r(...t.map((t) => t.bottom)) - n,
};
}
(t.arrow = (t) => ({
name: "arrow",
options: t,
async fn(e) {
const {
x: n,
y: i,
placement: r,
rects: a,
platform: l,
elements: c,
middlewareData: m,
} = e,
{ element: g, padding: h = 0 } = f(t, e) || {};
if (null == g) return {};
const y = x(h),
w = { x: n, y: i },
v = p(r),
b = d(v),
A = await l.getDimensions(g),
R = "y" === v,
P = R ? "top" : "left",
D = R ? "bottom" : "right",
T = R ? "clientHeight" : "clientWidth",
O = a.reference[b] + a.reference[v] - w[v] - a.floating[b],
E = w[v] - a.reference[v],
L = await (null == l.getOffsetParent ? void 0 : l.getOffsetParent(g));
let k = L ? L[T] : 0;
(k && (await (null == l.isElement ? void 0 : l.isElement(L)))) ||
(k = c.floating[T] || a.floating[b]);
const C = O / 2 - E / 2,
B = k / 2 - A[b] / 2 - 1,
H = o(y[P], B),
S = o(y[D], B),
F = H,
j = k - A[b] - S,
z = k / 2 - A[b] / 2 + C,
M = s(F, z, j),
V =
!m.arrow &&
null != u(r) &&
z !== M &&
a.reference[b] / 2 - (z < F ? H : S) - A[b] / 2 < 0,
W = V ? (z < F ? z - F : z - j) : 0;
return {
[v]: w[v] + W,
data: {
[v]: M,
centerOffset: z - M - W,
...(V && { alignmentOffset: W }),
},
reset: V,
};
},
})),
(t.autoPlacement = function (t) {
return (
void 0 === t && (t = {}),
{
name: "autoPlacement",
options: t,
async fn(e) {
var n, o, r;
const {
rects: a,
middlewareData: l,
placement: s,
platform: m,
elements: d,
} = e,
{
crossAxis: g = !1,
alignment: p,
allowedPlacements: w = i,
autoAlignment: x = !0,
...v
} = f(t, e),
b =
void 0 !== p || w === i
? (function (t, e, n) {
return (
t
? [
...n.filter((e) => u(e) === t),
...n.filter((e) => u(e) !== t),
]
: n.filter((t) => c(t) === t)
).filter((n) => !t || u(n) === t || (!!e && y(n) !== n));
})(p || null, x, w)
: w,
R = await A(e, v),
P = (null == (n = l.autoPlacement) ? void 0 : n.index) || 0,
D = b[P];
if (null == D) return {};
const T = h(
D,
a,
await (null == m.isRTL ? void 0 : m.isRTL(d.floating))
);
if (s !== D) return { reset: { placement: b[0] } };
const O = [R[c(D)], R[T[0]], R[T[1]]],
E = [
...((null == (o = l.autoPlacement) ? void 0 : o.overflows) ||
[]),
{ placement: D, overflows: O },
],
L = b[P + 1];
if (L)
return {
data: { index: P + 1, overflows: E },
reset: { placement: L },
};
const k = E.map((t) => {
const e = u(t.placement);
return [
t.placement,
e && g
? t.overflows.slice(0, 2).reduce((t, e) => t + e, 0)
: t.overflows[0],
t.overflows,
];
}).sort((t, e) => t[1] - e[1]),
C =
(null ==
(r = k.filter((t) =>
t[2].slice(0, u(t[0]) ? 2 : 3).every((t) => t <= 0)
)[0])
? void 0
: r[0]) || k[0][0];
return C !== s
? {
data: { index: P + 1, overflows: E },
reset: { placement: C },
}
: {};
},
}
);
}),
(t.computePosition = async (t, e, n) => {
const {
placement: i = "bottom",
strategy: o = "absolute",
middleware: r = [],
platform: a,
} = n,
l = r.filter(Boolean),
s = await (null == a.isRTL ? void 0 : a.isRTL(e));
let f = await a.getElementRects({
reference: t,
floating: e,
strategy: o,
}),
{ x: c, y: u } = b(f, i, s),
m = i,
d = {},
g = 0;
for (let n = 0; n < l.length; n++) {
const { name: r, fn: p } = l[n],
{
x: h,
y: y,
data: w,
reset: x,
} = await p({
x: c,
y: u,
initialPlacement: i,
placement: m,
strategy: o,
middlewareData: d,
rects: f,
platform: a,
elements: { reference: t, floating: e },
});
(c = null != h ? h : c),
(u = null != y ? y : u),
(d = { ...d, [r]: { ...d[r], ...w } }),
x &&
g <= 50 &&
(g++,
"object" == typeof x &&
(x.placement && (m = x.placement),
x.rects &&
(f =
!0 === x.rects
? await a.getElementRects({
reference: t,
floating: e,
strategy: o,
})
: x.rects),
({ x: c, y: u } = b(f, m, s))),
(n = -1));
}
return { x: c, y: u, placement: m, strategy: o, middlewareData: d };
}),
(t.detectOverflow = A),
(t.flip = function (t) {
return (
void 0 === t && (t = {}),
{
name: "flip",
options: t,
async fn(e) {
var n, i;
const {
placement: o,
middlewareData: r,
rects: a,
initialPlacement: l,
platform: s,
elements: m,
} = e,
{
mainAxis: d = !0,
crossAxis: p = !0,
fallbackPlacements: x,
fallbackStrategy: v = "bestFit",
fallbackAxisSideDirection: b = "none",
flipAlignment: R = !0,
...P
} = f(t, e);
if (null != (n = r.arrow) && n.alignmentOffset) return {};
const D = c(o),
T = g(l),
O = c(l) === l,
E = await (null == s.isRTL ? void 0 : s.isRTL(m.floating)),
L =
x ||
(O || !R
? [w(l)]
: (function (t) {
const e = w(t);
return [y(t), e, y(e)];
})(l)),
k = "none" !== b;
!x &&
k &&
L.push(
...(function (t, e, n, i) {
const o = u(t);
let r = (function (t, e, n) {
const i = ["left", "right"],
o = ["right", "left"],
r = ["top", "bottom"],
a = ["bottom", "top"];
switch (t) {
case "top":
case "bottom":
return n ? (e ? o : i) : e ? i : o;
case "left":
case "right":
return e ? r : a;
default:
return [];
}
})(c(t), "start" === n, i);
return (
o &&
((r = r.map((t) => t + "-" + o)),
e && (r = r.concat(r.map(y)))),
r
);
})(l, R, b, E)
);
const C = [l, ...L],
B = await A(e, P),
H = [];
let S = (null == (i = r.flip) ? void 0 : i.overflows) || [];
if ((d && H.push(B[D]), p)) {
const t = h(o, a, E);
H.push(B[t[0]], B[t[1]]);
}
if (
((S = [...S, { placement: o, overflows: H }]),
!H.every((t) => t <= 0))
) {
var F, j;
const t = ((null == (F = r.flip) ? void 0 : F.index) || 0) + 1,
e = C[t];
if (e) {
var z;
const n = "alignment" === p && T !== g(e),
i = (null == (z = S[0]) ? void 0 : z.overflows[0]) > 0;
if (!n || i)
return {
data: { index: t, overflows: S },
reset: { placement: e },
};
}
let n =
null ==
(j = S.filter((t) => t.overflows[0] <= 0).sort(
(t, e) => t.overflows[1] - e.overflows[1]
)[0])
? void 0
: j.placement;
if (!n)
switch (v) {
case "bestFit": {
var M;
const t =
null ==
(M = S.filter((t) => {
if (k) {
const e = g(t.placement);
return e === T || "y" === e;
}
return !0;
})
.map((t) => [
t.placement,
t.overflows
.filter((t) => t > 0)
.reduce((t, e) => t + e, 0),
])
.sort((t, e) => t[1] - e[1])[0])
? void 0
: M[0];
t && (n = t);
break;
}
case "initialPlacement":
n = l;
}
if (o !== n) return { reset: { placement: n } };
}
return {};
},
}
);
}),
(t.hide = function (t) {
return (
void 0 === t && (t = {}),
{
name: "hide",
options: t,
async fn(e) {
const { rects: n } = e,
{ strategy: i = "referenceHidden", ...o } = f(t, e);
switch (i) {
case "referenceHidden": {
const t = R(
await A(e, { ...o, elementContext: "reference" }),
n.reference
);
return {
data: { referenceHiddenOffsets: t, referenceHidden: P(t) },
};
}
case "escaped": {
const t = R(await A(e, { ...o, altBoundary: !0 }), n.floating);
return { data: { escapedOffsets: t, escaped: P(t) } };
}
default:
return {};
}
},
}
);
}),
(t.inline = function (t) {
return (
void 0 === t && (t = {}),
{
name: "inline",
options: t,
async fn(e) {
const {
placement: n,
elements: i,
rects: a,
platform: l,
strategy: s,
} = e,
{ padding: u = 2, x: m, y: d } = f(t, e),
p = Array.from(
(await (null == l.getClientRects
? void 0
: l.getClientRects(i.reference))) || []
),
h = (function (t) {
const e = t.slice().sort((t, e) => t.y - e.y),
n = [];
let i = null;
for (let t = 0; t < e.length; t++) {
const o = e[t];
!i || o.y - i.y > i.height / 2
? n.push([o])
: n[n.length - 1].push(o),
(i = o);
}
return n.map((t) => v(D(t)));
})(p),
y = v(D(p)),
w = x(u);
const b = await l.getElementRects({
reference: {
getBoundingClientRect: function () {
if (
2 === h.length &&
h[0].left > h[1].right &&
null != m &&
null != d
)
return (
h.find(
(t) =>
m > t.left - w.left &&
m < t.right + w.right &&
d > t.top - w.top &&
d < t.bottom + w.bottom
) || y
);
if (h.length >= 2) {
if ("y" === g(n)) {
const t = h[0],
e = h[h.length - 1],
i = "top" === c(n),
o = t.top,
r = e.bottom,
a = i ? t.left : e.left,
l = i ? t.right : e.right;
return {
top: o,
bottom: r,
left: a,
right: l,
width: l - a,
height: r - o,
x: a,
y: o,
};
}
const t = "left" === c(n),
e = r(...h.map((t) => t.right)),
i = o(...h.map((t) => t.left)),
a = h.filter((n) => (t ? n.left === i : n.right === e)),
l = a[0].top,
s = a[a.length - 1].bottom;
return {
top: l,
bottom: s,
left: i,
right: e,
width: e - i,
height: s - l,
x: i,
y: l,
};
}
return y;
},
},
floating: i.floating,
strategy: s,
});
return a.reference.x !== b.reference.x ||
a.reference.y !== b.reference.y ||
a.reference.width !== b.reference.width ||
a.reference.height !== b.reference.height
? { reset: { rects: b } }
: {};
},
}
);
}),
(t.limitShift = function (t) {
return (
void 0 === t && (t = {}),
{
options: t,
fn(e) {
const { x: n, y: i, placement: o, rects: r, middlewareData: a } = e,
{ offset: l = 0, mainAxis: s = !0, crossAxis: u = !0 } = f(t, e),
d = { x: n, y: i },
p = g(o),
h = m(p);
let y = d[h],
w = d[p];
const x = f(l, e),
v =
"number" == typeof x
? { mainAxis: x, crossAxis: 0 }
: { mainAxis: 0, crossAxis: 0, ...x };
if (s) {
const t = "y" === h ? "height" : "width",
e = r.reference[h] - r.floating[t] + v.mainAxis,
n = r.reference[h] + r.reference[t] - v.mainAxis;
y < e ? (y = e) : y > n && (y = n);
}
if (u) {
var b, A;
const t = "y" === h ? "width" : "height",
e = ["top", "left"].includes(c(o)),
n =
r.reference[p] -
r.floating[t] +
((e && (null == (b = a.offset) ? void 0 : b[p])) || 0) +
(e ? 0 : v.crossAxis),
i =
r.reference[p] +
r.reference[t] +
(e ? 0 : (null == (A = a.offset) ? void 0 : A[p]) || 0) -
(e ? v.crossAxis : 0);
w < n ? (w = n) : w > i && (w = i);
}
return { [h]: y, [p]: w };
},
}
);
}),
(t.offset = function (t) {
return (
void 0 === t && (t = 0),
{
name: "offset",
options: t,
async fn(e) {
var n, i;
const { x: o, y: r, placement: a, middlewareData: l } = e,
s = await (async function (t, e) {
const { placement: n, platform: i, elements: o } = t,
r = await (null == i.isRTL ? void 0 : i.isRTL(o.floating)),
a = c(n),
l = u(n),
s = "y" === g(n),
m = ["left", "top"].includes(a) ? -1 : 1,
d = r && s ? -1 : 1,
p = f(e, t);
let {
mainAxis: h,
crossAxis: y,
alignmentAxis: w,
} = "number" == typeof p
? { mainAxis: p, crossAxis: 0, alignmentAxis: null }
: {
mainAxis: p.mainAxis || 0,
crossAxis: p.crossAxis || 0,
alignmentAxis: p.alignmentAxis,
};
return (
l && "number" == typeof w && (y = "end" === l ? -1 * w : w),
s ? { x: y * d, y: h * m } : { x: h * m, y: y * d }
);
})(e, t);
return a === (null == (n = l.offset) ? void 0 : n.placement) &&
null != (i = l.arrow) &&
i.alignmentOffset
? {}
: { x: o + s.x, y: r + s.y, data: { ...s, placement: a } };
},
}
);
}),
(t.rectToClientRect = v),
(t.shift = function (t) {
return (
void 0 === t && (t = {}),
{
name: "shift",
options: t,
async fn(e) {
const { x: n, y: i, placement: o } = e,
{
mainAxis: r = !0,
crossAxis: a = !1,
limiter: l = {
fn: (t) => {
let { x: e, y: n } = t;
return { x: e, y: n };
},
},
...u
} = f(t, e),
d = { x: n, y: i },
p = await A(e, u),
h = g(c(o)),
y = m(h);
let w = d[y],
x = d[h];
if (r) {
const t = "y" === y ? "bottom" : "right";
w = s(w + p["y" === y ? "top" : "left"], w, w - p[t]);
}
if (a) {
const t = "y" === h ? "bottom" : "right";
x = s(x + p["y" === h ? "top" : "left"], x, x - p[t]);
}
const v = l.fn({ ...e, [y]: w, [h]: x });
return {
...v,
data: { x: v.x - n, y: v.y - i, enabled: { [y]: r, [h]: a } },
};
},
}
);
}),
(t.size = function (t) {
return (
void 0 === t && (t = {}),
{
name: "size",
options: t,
async fn(e) {
var n, i;
const { placement: a, rects: l, platform: s, elements: m } = e,
{ apply: d = () => {}, ...p } = f(t, e),
h = await A(e, p),
y = c(a),
w = u(a),
x = "y" === g(a),
{ width: v, height: b } = l.floating;
let R, P;
"top" === y || "bottom" === y
? ((R = y),
(P =
w ===
((await (null == s.isRTL ? void 0 : s.isRTL(m.floating)))
? "start"
: "end")
? "left"
: "right"))
: ((P = y), (R = "end" === w ? "top" : "bottom"));
const D = b - h.top - h.bottom,
T = v - h.left - h.right,
O = o(b - h[R], D),
E = o(v - h[P], T),
L = !e.middlewareData.shift;
let k = O,
C = E;
if (
(null != (n = e.middlewareData.shift) && n.enabled.x && (C = T),
null != (i = e.middlewareData.shift) && i.enabled.y && (k = D),
L && !w)
) {
const t = r(h.left, 0),
e = r(h.right, 0),
n = r(h.top, 0),
i = r(h.bottom, 0);
x
? (C =
v - 2 * (0 !== t || 0 !== e ? t + e : r(h.left, h.right)))
: (k =
b - 2 * (0 !== n || 0 !== i ? n + i : r(h.top, h.bottom)));
}
await d({ ...e, availableWidth: C, availableHeight: k });
const B = await s.getDimensions(m.floating);
return v !== B.width || b !== B.height
? { reset: { rects: !0 } }
: {};
},
}
);
});
});

View file

@ -0,0 +1,604 @@
// https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.7.0
!(function (t, e) {
"object" == typeof exports && "undefined" != typeof module
? e(exports, require("./floating_ui_core"))
: "function" == typeof define && define.amd
? define(["exports", "./floatingUICore"], e)
: e(
((t =
"undefined" != typeof globalThis
? globalThis
: t || self).FloatingUIDOM = {}),
t.FloatingUICore
);
})(this, function (t, e) {
"use strict";
const n = Math.min,
o = Math.max,
i = Math.round,
r = Math.floor,
c = (t) => ({ x: t, y: t });
function l() {
return "undefined" != typeof window;
}
function s(t) {
return a(t) ? (t.nodeName || "").toLowerCase() : "#document";
}
function f(t) {
var e;
return (
(null == t || null == (e = t.ownerDocument) ? void 0 : e.defaultView) ||
window
);
}
function u(t) {
var e;
return null ==
(e = (a(t) ? t.ownerDocument : t.document) || window.document)
? void 0
: e.documentElement;
}
function a(t) {
return !!l() && (t instanceof Node || t instanceof f(t).Node);
}
function d(t) {
return !!l() && (t instanceof Element || t instanceof f(t).Element);
}
function h(t) {
return !!l() && (t instanceof HTMLElement || t instanceof f(t).HTMLElement);
}
function p(t) {
return (
!(!l() || "undefined" == typeof ShadowRoot) &&
(t instanceof ShadowRoot || t instanceof f(t).ShadowRoot)
);
}
function g(t) {
const { overflow: e, overflowX: n, overflowY: o, display: i } = b(t);
return (
/auto|scroll|overlay|hidden|clip/.test(e + o + n) &&
!["inline", "contents"].includes(i)
);
}
function m(t) {
return ["table", "td", "th"].includes(s(t));
}
function y(t) {
return [":popover-open", ":modal"].some((e) => {
try {
return t.matches(e);
} catch (t) {
return !1;
}
});
}
function w(t) {
const e = x(),
n = d(t) ? b(t) : t;
return (
["transform", "translate", "scale", "rotate", "perspective"].some(
(t) => !!n[t] && "none" !== n[t]
) ||
(!!n.containerType && "normal" !== n.containerType) ||
(!e && !!n.backdropFilter && "none" !== n.backdropFilter) ||
(!e && !!n.filter && "none" !== n.filter) ||
[
"transform",
"translate",
"scale",
"rotate",
"perspective",
"filter",
].some((t) => (n.willChange || "").includes(t)) ||
["paint", "layout", "strict", "content"].some((t) =>
(n.contain || "").includes(t)
)
);
}
function x() {
return (
!("undefined" == typeof CSS || !CSS.supports) &&
CSS.supports("-webkit-backdrop-filter", "none")
);
}
function v(t) {
return ["html", "body", "#document"].includes(s(t));
}
function b(t) {
return f(t).getComputedStyle(t);
}
function T(t) {
return d(t)
? { scrollLeft: t.scrollLeft, scrollTop: t.scrollTop }
: { scrollLeft: t.scrollX, scrollTop: t.scrollY };
}
function L(t) {
if ("html" === s(t)) return t;
const e = t.assignedSlot || t.parentNode || (p(t) && t.host) || u(t);
return p(e) ? e.host : e;
}
function R(t) {
const e = L(t);
return v(e)
? t.ownerDocument
? t.ownerDocument.body
: t.body
: h(e) && g(e)
? e
: R(e);
}
function C(t, e, n) {
var o;
void 0 === e && (e = []), void 0 === n && (n = !0);
const i = R(t),
r = i === (null == (o = t.ownerDocument) ? void 0 : o.body),
c = f(i);
if (r) {
const t = E(c);
return e.concat(
c,
c.visualViewport || [],
g(i) ? i : [],
t && n ? C(t) : []
);
}
return e.concat(i, C(i, [], n));
}
function E(t) {
return t.parent && Object.getPrototypeOf(t.parent) ? t.frameElement : null;
}
function S(t) {
const e = b(t);
let n = parseFloat(e.width) || 0,
o = parseFloat(e.height) || 0;
const r = h(t),
c = r ? t.offsetWidth : n,
l = r ? t.offsetHeight : o,
s = i(n) !== c || i(o) !== l;
return s && ((n = c), (o = l)), { width: n, height: o, $: s };
}
function F(t) {
return d(t) ? t : t.contextElement;
}
function O(t) {
const e = F(t);
if (!h(e)) return c(1);
const n = e.getBoundingClientRect(),
{ width: o, height: r, $: l } = S(e);
let s = (l ? i(n.width) : n.width) / o,
f = (l ? i(n.height) : n.height) / r;
return (
(s && Number.isFinite(s)) || (s = 1),
(f && Number.isFinite(f)) || (f = 1),
{ x: s, y: f }
);
}
const D = c(0);
function H(t) {
const e = f(t);
return x() && e.visualViewport
? { x: e.visualViewport.offsetLeft, y: e.visualViewport.offsetTop }
: D;
}
function P(t, n, o, i) {
void 0 === n && (n = !1), void 0 === o && (o = !1);
const r = t.getBoundingClientRect(),
l = F(t);
let s = c(1);
n && (i ? d(i) && (s = O(i)) : (s = O(t)));
const u = (function (t, e, n) {
return void 0 === e && (e = !1), !(!n || (e && n !== f(t))) && e;
})(l, o, i)
? H(l)
: c(0);
let a = (r.left + u.x) / s.x,
h = (r.top + u.y) / s.y,
p = r.width / s.x,
g = r.height / s.y;
if (l) {
const t = f(l),
e = i && d(i) ? f(i) : i;
let n = t,
o = E(n);
for (; o && i && e !== n; ) {
const t = O(o),
e = o.getBoundingClientRect(),
i = b(o),
r = e.left + (o.clientLeft + parseFloat(i.paddingLeft)) * t.x,
c = e.top + (o.clientTop + parseFloat(i.paddingTop)) * t.y;
(a *= t.x),
(h *= t.y),
(p *= t.x),
(g *= t.y),
(a += r),
(h += c),
(n = f(o)),
(o = E(n));
}
}
return e.rectToClientRect({ width: p, height: g, x: a, y: h });
}
function W(t, e) {
const n = T(t).scrollLeft;
return e ? e.left + n : P(u(t)).left + n;
}
function M(t, e, n) {
void 0 === n && (n = !1);
const o = t.getBoundingClientRect();
return {
x: o.left + e.scrollLeft - (n ? 0 : W(t, o)),
y: o.top + e.scrollTop,
};
}
function z(t, n, i) {
let r;
if ("viewport" === n)
r = (function (t, e) {
const n = f(t),
o = u(t),
i = n.visualViewport;
let r = o.clientWidth,
c = o.clientHeight,
l = 0,
s = 0;
if (i) {
(r = i.width), (c = i.height);
const t = x();
(!t || (t && "fixed" === e)) &&
((l = i.offsetLeft), (s = i.offsetTop));
}
return { width: r, height: c, x: l, y: s };
})(t, i);
else if ("document" === n)
r = (function (t) {
const e = u(t),
n = T(t),
i = t.ownerDocument.body,
r = o(e.scrollWidth, e.clientWidth, i.scrollWidth, i.clientWidth),
c = o(e.scrollHeight, e.clientHeight, i.scrollHeight, i.clientHeight);
let l = -n.scrollLeft + W(t);
const s = -n.scrollTop;
return (
"rtl" === b(i).direction &&
(l += o(e.clientWidth, i.clientWidth) - r),
{ width: r, height: c, x: l, y: s }
);
})(u(t));
else if (d(n))
r = (function (t, e) {
const n = P(t, !0, "fixed" === e),
o = n.top + t.clientTop,
i = n.left + t.clientLeft,
r = h(t) ? O(t) : c(1);
return {
width: t.clientWidth * r.x,
height: t.clientHeight * r.y,
x: i * r.x,
y: o * r.y,
};
})(n, i);
else {
const e = H(t);
r = { x: n.x - e.x, y: n.y - e.y, width: n.width, height: n.height };
}
return e.rectToClientRect(r);
}
function A(t, e) {
const n = L(t);
return (
!(n === e || !d(n) || v(n)) && ("fixed" === b(n).position || A(n, e))
);
}
function B(t, e, n) {
const o = h(e),
i = u(e),
r = "fixed" === n,
l = P(t, !0, r, e);
let f = { scrollLeft: 0, scrollTop: 0 };
const a = c(0);
function d() {
a.x = W(i);
}
if (o || (!o && !r))
if ((("body" !== s(e) || g(i)) && (f = T(e)), o)) {
const t = P(e, !0, r, e);
(a.x = t.x + e.clientLeft), (a.y = t.y + e.clientTop);
} else i && d();
r && !o && i && d();
const p = !i || o || r ? c(0) : M(i, f);
return {
x: l.left + f.scrollLeft - a.x - p.x,
y: l.top + f.scrollTop - a.y - p.y,
width: l.width,
height: l.height,
};
}
function V(t) {
return "static" === b(t).position;
}
function N(t, e) {
if (!h(t) || "fixed" === b(t).position) return null;
if (e) return e(t);
let n = t.offsetParent;
return u(t) === n && (n = n.ownerDocument.body), n;
}
function I(t, e) {
const n = f(t);
if (y(t)) return n;
if (!h(t)) {
let e = L(t);
for (; e && !v(e); ) {
if (d(e) && !V(e)) return e;
e = L(e);
}
return n;
}
let o = N(t, e);
for (; o && m(o) && V(o); ) o = N(o, e);
return o && v(o) && V(o) && !w(o)
? n
: o ||
(function (t) {
let e = L(t);
for (; h(e) && !v(e); ) {
if (w(e)) return e;
if (y(e)) return null;
e = L(e);
}
return null;
})(t) ||
n;
}
const k = {
convertOffsetParentRelativeRectToViewportRelativeRect: function (t) {
let { elements: e, rect: n, offsetParent: o, strategy: i } = t;
const r = "fixed" === i,
l = u(o),
f = !!e && y(e.floating);
if (o === l || (f && r)) return n;
let a = { scrollLeft: 0, scrollTop: 0 },
d = c(1);
const p = c(0),
m = h(o);
if (
(m || (!m && !r)) &&
(("body" !== s(o) || g(l)) && (a = T(o)), h(o))
) {
const t = P(o);
(d = O(o)), (p.x = t.x + o.clientLeft), (p.y = t.y + o.clientTop);
}
const w = !l || m || r ? c(0) : M(l, a, !0);
return {
width: n.width * d.x,
height: n.height * d.y,
x: n.x * d.x - a.scrollLeft * d.x + p.x + w.x,
y: n.y * d.y - a.scrollTop * d.y + p.y + w.y,
};
},
getDocumentElement: u,
getClippingRect: function (t) {
let { element: e, boundary: i, rootBoundary: r, strategy: c } = t;
const l = [
...("clippingAncestors" === i
? y(e)
? []
: (function (t, e) {
const n = e.get(t);
if (n) return n;
let o = C(t, [], !1).filter((t) => d(t) && "body" !== s(t)),
i = null;
const r = "fixed" === b(t).position;
let c = r ? L(t) : t;
for (; d(c) && !v(c); ) {
const e = b(c),
n = w(c);
n || "fixed" !== e.position || (i = null),
(
r
? !n && !i
: (!n &&
"static" === e.position &&
i &&
["absolute", "fixed"].includes(i.position)) ||
(g(c) && !n && A(t, c))
)
? (o = o.filter((t) => t !== c))
: (i = e),
(c = L(c));
}
return e.set(t, o), o;
})(e, this._c)
: [].concat(i)),
r,
],
f = l[0],
u = l.reduce((t, i) => {
const r = z(e, i, c);
return (
(t.top = o(r.top, t.top)),
(t.right = n(r.right, t.right)),
(t.bottom = n(r.bottom, t.bottom)),
(t.left = o(r.left, t.left)),
t
);
}, z(e, f, c));
return {
width: u.right - u.left,
height: u.bottom - u.top,
x: u.left,
y: u.top,
};
},
getOffsetParent: I,
getElementRects: async function (t) {
const e = this.getOffsetParent || I,
n = this.getDimensions,
o = await n(t.floating);
return {
reference: B(t.reference, await e(t.floating), t.strategy),
floating: { x: 0, y: 0, width: o.width, height: o.height },
};
},
getClientRects: function (t) {
return Array.from(t.getClientRects());
},
getDimensions: function (t) {
const { width: e, height: n } = S(t);
return { width: e, height: n };
},
getScale: O,
isElement: d,
isRTL: function (t) {
return "rtl" === b(t).direction;
},
};
function q(t, e) {
return (
t.x === e.x && t.y === e.y && t.width === e.width && t.height === e.height
);
}
const U = e.detectOverflow,
j = e.offset,
X = e.autoPlacement,
Y = e.shift,
$ = e.flip,
_ = e.size,
G = e.hide,
J = e.arrow,
K = e.inline,
Q = e.limitShift;
(t.arrow = J),
(t.autoPlacement = X),
(t.autoUpdate = function (t, e, i, c) {
void 0 === c && (c = {});
const {
ancestorScroll: l = !0,
ancestorResize: s = !0,
elementResize: f = "function" == typeof ResizeObserver,
layoutShift: a = "function" == typeof IntersectionObserver,
animationFrame: d = !1,
} = c,
h = F(t),
p = l || s ? [...(h ? C(h) : []), ...C(e)] : [];
p.forEach((t) => {
l && t.addEventListener("scroll", i, { passive: !0 }),
s && t.addEventListener("resize", i);
});
const g =
h && a
? (function (t, e) {
let i,
c = null;
const l = u(t);
function s() {
var t;
clearTimeout(i), null == (t = c) || t.disconnect(), (c = null);
}
return (
(function f(u, a) {
void 0 === u && (u = !1), void 0 === a && (a = 1), s();
const d = t.getBoundingClientRect(),
{ left: h, top: p, width: g, height: m } = d;
if ((u || e(), !g || !m)) return;
const y = {
rootMargin:
-r(p) +
"px " +
-r(l.clientWidth - (h + g)) +
"px " +
-r(l.clientHeight - (p + m)) +
"px " +
-r(h) +
"px",
threshold: o(0, n(1, a)) || 1,
};
let w = !0;
function x(e) {
const n = e[0].intersectionRatio;
if (n !== a) {
if (!w) return f();
n
? f(!1, n)
: (i = setTimeout(() => {
f(!1, 1e-7);
}, 1e3));
}
1 !== n || q(d, t.getBoundingClientRect()) || f(), (w = !1);
}
try {
c = new IntersectionObserver(x, {
...y,
root: l.ownerDocument,
});
} catch (t) {
c = new IntersectionObserver(x, y);
}
c.observe(t);
})(!0),
s
);
})(h, i)
: null;
let m,
y = -1,
w = null;
f &&
((w = new ResizeObserver((t) => {
let [n] = t;
n &&
n.target === h &&
w &&
(w.unobserve(e),
cancelAnimationFrame(y),
(y = requestAnimationFrame(() => {
var t;
null == (t = w) || t.observe(e);
}))),
i();
})),
h && !d && w.observe(h),
w.observe(e));
let x = d ? P(t) : null;
return (
d &&
(function e() {
const n = P(t);
x && !q(x, n) && i();
(x = n), (m = requestAnimationFrame(e));
})(),
i(),
() => {
var t;
p.forEach((t) => {
l && t.removeEventListener("scroll", i),
s && t.removeEventListener("resize", i);
}),
null == g || g(),
null == (t = w) || t.disconnect(),
(w = null),
d && cancelAnimationFrame(m);
}
);
}),
(t.computePosition = (t, n, o) => {
const i = new Map(),
r = { platform: k, ...o },
c = { ...r.platform, _c: i };
return e.computePosition(t, n, { ...r, platform: c });
}),
(t.detectOverflow = U),
(t.flip = $),
(t.getOverflowAncestors = C),
(t.hide = G),
(t.inline = K),
(t.limitShift = Q),
(t.offset = j),
(t.platform = k),
(t.shift = Y),
(t.size = _);
// We put this manually here because we need to make sure it's available
// before the popover component is initialized.
window.FloatingUIDOM = t;
return t;
});

25
assets/js/input.js Normal file
View file

@ -0,0 +1,25 @@
(function () {
'use strict';
document.addEventListener('click', (e) => {
const button = e.target.closest('[data-tui-input-toggle-password]');
if (!button) return;
const inputId = button.getAttribute('data-tui-input-toggle-password');
const input = document.getElementById(inputId);
if (!input) return;
const iconOpen = button.querySelector('.icon-open');
const iconClosed = button.querySelector('.icon-closed');
if (input.type === 'password') {
input.type = 'text';
if (iconOpen) iconOpen.classList.add('hidden');
if (iconClosed) iconClosed.classList.remove('hidden');
} else {
input.type = 'password';
if (iconOpen) iconOpen.classList.remove('hidden');
if (iconClosed) iconClosed.classList.add('hidden');
}
});
})();

207
assets/js/inputotp.js Normal file
View file

@ -0,0 +1,207 @@
(function () {
'use strict';
function getEventTargetElement(event) {
return event.target instanceof Element ? event.target : null;
}
// Utility functions
function getSlots(container) {
return Array.from(container.querySelectorAll('[data-tui-inputotp-slot]')).sort(
(a, b) => parseInt(a.getAttribute('data-tui-inputotp-index')) - parseInt(b.getAttribute('data-tui-inputotp-index'))
);
}
function focusSlot(slot) {
if (!slot) return;
slot.focus();
setTimeout(() => slot.select(), 0);
}
function updateHiddenValue(container) {
const hiddenInput = container.querySelector('[data-tui-inputotp-value-target]');
const slots = getSlots(container);
if (hiddenInput && slots.length) {
hiddenInput.value = slots.map(s => s.value).join('');
}
}
function findFirstEmptySlot(container) {
const slots = getSlots(container);
for (const slot of slots) {
if (!slot.value) return slot;
}
return null;
}
function getNextSlot(container, currentSlot) {
const slots = getSlots(container);
const index = slots.indexOf(currentSlot);
return index >= 0 && index < slots.length - 1 ? slots[index + 1] : null;
}
function getPrevSlot(container, currentSlot) {
const slots = getSlots(container);
const index = slots.indexOf(currentSlot);
return index > 0 ? slots[index - 1] : null;
}
// Event handlers
document.addEventListener('input', (e) => {
const target = getEventTargetElement(e);
if (!target?.matches('[data-tui-inputotp-slot]')) return;
const slot = target;
const container = slot.closest('[data-tui-inputotp]');
if (!container) return;
// Handle space as empty
if (slot.value === ' ') {
slot.value = '';
return;
}
// Keep only last character
if (slot.value.length > 1) {
slot.value = slot.value.slice(-1);
}
// Move to next slot if filled
if (slot.value) {
const nextSlot = getNextSlot(container, slot);
if (nextSlot) focusSlot(nextSlot);
}
updateHiddenValue(container);
});
document.addEventListener('keydown', (e) => {
const target = getEventTargetElement(e);
if (!target?.matches('[data-tui-inputotp-slot]')) return;
const slot = target;
const container = slot.closest('[data-tui-inputotp]');
if (!container) return;
if (e.key === 'Backspace') {
e.preventDefault();
if (slot.value) {
slot.value = '';
updateHiddenValue(container);
} else {
const prevSlot = getPrevSlot(container, slot);
if (prevSlot) {
prevSlot.value = '';
updateHiddenValue(container);
focusSlot(prevSlot);
}
}
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
const prevSlot = getPrevSlot(container, slot);
if (prevSlot) focusSlot(prevSlot);
} else if (e.key === 'ArrowRight') {
e.preventDefault();
const nextSlot = getNextSlot(container, slot);
if (nextSlot) focusSlot(nextSlot);
}
});
document.addEventListener('focus', (e) => {
const target = getEventTargetElement(e);
if (!target?.matches('[data-tui-inputotp-slot]')) return;
const slot = target;
const container = slot.closest('[data-tui-inputotp]');
if (!container) return;
// Redirect to first empty slot
const firstEmpty = findFirstEmptySlot(container);
if (firstEmpty && firstEmpty !== slot) {
focusSlot(firstEmpty);
return;
}
setTimeout(() => slot.select(), 0);
}, true);
document.addEventListener('paste', (e) => {
const target = getEventTargetElement(e);
const slot = target?.closest('[data-tui-inputotp-slot]');
if (!slot) return;
e.preventDefault();
const container = slot.closest('[data-tui-inputotp]');
if (!container) return;
const pastedData = (e.clipboardData || window.clipboardData).getData('text');
const chars = pastedData.replace(/\s/g, '').split('');
const slots = getSlots(container);
let startIndex = slots.indexOf(slot);
for (let i = 0; i < chars.length && startIndex + i < slots.length; i++) {
slots[startIndex + i].value = chars[i];
}
updateHiddenValue(container);
// Focus next empty or last filled slot
const nextEmpty = findFirstEmptySlot(container);
focusSlot(nextEmpty || slots[Math.min(startIndex + chars.length, slots.length - 1)]);
});
// Label click handling
document.addEventListener('click', (e) => {
const target = getEventTargetElement(e);
if (!target?.matches('label[for]')) return;
const targetId = target.getAttribute('for');
const hiddenInput = document.getElementById(targetId);
if (!hiddenInput?.matches('[data-tui-inputotp-value-target]')) return;
e.preventDefault();
const container = hiddenInput.closest('[data-tui-inputotp]');
const slots = getSlots(container);
if (slots.length > 0) focusSlot(slots[0]);
});
// Form reset
document.addEventListener('reset', (e) => {
const target = getEventTargetElement(e);
if (!target?.matches('form')) return;
target.querySelectorAll('[data-tui-inputotp]').forEach(container => {
getSlots(container).forEach(slot => {
slot.value = '';
});
updateHiddenValue(container);
});
});
// MutationObserver for initial setup and autofocus
new MutationObserver(() => {
document.querySelectorAll('[data-tui-inputotp]').forEach(container => {
const slots = getSlots(container);
if (slots.length === 0) return;
// Set initial value if provided
const initialValue = container.getAttribute('data-tui-inputotp-value');
if (initialValue && !slots[0].value) {
for (let i = 0; i < slots.length && i < initialValue.length; i++) {
if (!slots[i].value) slots[i].value = initialValue[i];
}
updateHiddenValue(container);
}
// Handle autofocus
if (container.hasAttribute('autofocus') && !slots.some(s => s === document.activeElement)) {
requestAnimationFrame(() => {
if (slots[0] && !slots.some(s => s === document.activeElement)) {
focusSlot(slots[0]);
}
});
}
});
}).observe(document.body, { childList: true, subtree: true });
})();

View file

@ -1 +1 @@
(()=>{(function(){"use strict";function u(e){return Array.from(e.querySelectorAll("[data-tui-inputotp-slot]")).sort((t,n)=>parseInt(t.getAttribute("data-tui-inputotp-index"))-parseInt(n.getAttribute("data-tui-inputotp-index")))}function a(e){e&&(e.focus(),setTimeout(()=>e.select(),0))}function i(e){let t=e.querySelector("[data-tui-inputotp-value-target]"),n=u(e);t&&n.length&&(t.value=n.map(o=>o.value).join(""))}function d(e){let t=u(e);for(let n of t)if(!n.value)return n;return null}function f(e,t){let n=u(e),o=n.indexOf(t);return o>=0&&o<n.length-1?n[o+1]:null}function p(e,t){let n=u(e),o=n.indexOf(t);return o>0?n[o-1]:null}document.addEventListener("input",e=>{if(!e.target.matches("[data-tui-inputotp-slot]"))return;let t=e.target,n=t.closest("[data-tui-inputotp]");if(n){if(t.value===" "){t.value="";return}if(t.value.length>1&&(t.value=t.value.slice(-1)),t.value){let o=f(n,t);o&&a(o)}i(n)}}),document.addEventListener("keydown",e=>{if(!e.target.matches("[data-tui-inputotp-slot]"))return;let t=e.target,n=t.closest("[data-tui-inputotp]");if(n){if(e.key==="Backspace")if(e.preventDefault(),t.value)t.value="",i(n);else{let o=p(n,t);o&&(o.value="",i(n),a(o))}else if(e.key==="ArrowLeft"){e.preventDefault();let o=p(n,t);o&&a(o)}else if(e.key==="ArrowRight"){e.preventDefault();let o=f(n,t);o&&a(o)}}}),document.addEventListener("focus",e=>{if(!e.target.matches("[data-tui-inputotp-slot]"))return;let t=e.target,n=t.closest("[data-tui-inputotp]");if(!n)return;let o=d(n);if(o&&o!==t){a(o);return}setTimeout(()=>t.select(),0)},!0),document.addEventListener("paste",e=>{let t=e.target.closest("[data-tui-inputotp-slot]");if(!t)return;e.preventDefault();let n=t.closest("[data-tui-inputotp]");if(!n)return;let r=(e.clipboardData||window.clipboardData).getData("text").replace(/\s/g,"").split(""),s=u(n),c=s.indexOf(t);for(let l=0;l<r.length&&c+l<s.length;l++)s[c+l].value=r[l];i(n);let v=d(n);a(v||s[Math.min(c+r.length,s.length-1)])}),document.addEventListener("click",e=>{if(!e.target.matches("label[for]"))return;let t=e.target.getAttribute("for"),n=document.getElementById(t);if(!n?.matches("[data-tui-inputotp-value-target]"))return;e.preventDefault();let o=n.closest("[data-tui-inputotp]"),r=u(o);r.length>0&&a(r[0])}),document.addEventListener("reset",e=>{e.target.matches("form")&&e.target.querySelectorAll("[data-tui-inputotp]").forEach(t=>{u(t).forEach(n=>{n.value=""}),i(t)})}),new MutationObserver(()=>{document.querySelectorAll("[data-tui-inputotp]").forEach(e=>{let t=u(e);if(t.length===0)return;let n=e.getAttribute("data-tui-inputotp-value");if(n&&!t[0].value){for(let o=0;o<t.length&&o<n.length;o++)t[o].value||(t[o].value=n[o]);i(e)}e.hasAttribute("autofocus")&&!t.some(o=>o===document.activeElement)&&requestAnimationFrame(()=>{t[0]&&!t.some(o=>o===document.activeElement)&&a(t[0])})})}).observe(document.body,{childList:!0,subtree:!0})})();})();
(()=>{(function(){"use strict";function s(e){return e.target instanceof Element?e.target:null}function a(e){return Array.from(e.querySelectorAll("[data-tui-inputotp-slot]")).sort((o,t)=>parseInt(o.getAttribute("data-tui-inputotp-index"))-parseInt(t.getAttribute("data-tui-inputotp-index")))}function i(e){e&&(e.focus(),setTimeout(()=>e.select(),0))}function r(e){let o=e.querySelector("[data-tui-inputotp-value-target]"),t=a(e);o&&t.length&&(o.value=t.map(n=>n.value).join(""))}function p(e){let o=a(e);for(let t of o)if(!t.value)return t;return null}function v(e,o){let t=a(e),n=t.indexOf(o);return n>=0&&n<t.length-1?t[n+1]:null}function g(e,o){let t=a(e),n=t.indexOf(o);return n>0?t[n-1]:null}document.addEventListener("input",e=>{let o=s(e);if(!o?.matches("[data-tui-inputotp-slot]"))return;let t=o,n=t.closest("[data-tui-inputotp]");if(n){if(t.value===" "){t.value="";return}if(t.value.length>1&&(t.value=t.value.slice(-1)),t.value){let u=v(n,t);u&&i(u)}r(n)}}),document.addEventListener("keydown",e=>{let o=s(e);if(!o?.matches("[data-tui-inputotp-slot]"))return;let t=o,n=t.closest("[data-tui-inputotp]");if(n){if(e.key==="Backspace")if(e.preventDefault(),t.value)t.value="",r(n);else{let u=g(n,t);u&&(u.value="",r(n),i(u))}else if(e.key==="ArrowLeft"){e.preventDefault();let u=g(n,t);u&&i(u)}else if(e.key==="ArrowRight"){e.preventDefault();let u=v(n,t);u&&i(u)}}}),document.addEventListener("focus",e=>{let o=s(e);if(!o?.matches("[data-tui-inputotp-slot]"))return;let t=o,n=t.closest("[data-tui-inputotp]");if(!n)return;let u=p(n);if(u&&u!==t){i(u);return}setTimeout(()=>t.select(),0)},!0),document.addEventListener("paste",e=>{let t=s(e)?.closest("[data-tui-inputotp-slot]");if(!t)return;e.preventDefault();let n=t.closest("[data-tui-inputotp]");if(!n)return;let l=(e.clipboardData||window.clipboardData).getData("text").replace(/\s/g,"").split(""),c=a(n),d=c.indexOf(t);for(let f=0;f<l.length&&d+f<c.length;f++)c[d+f].value=l[f];r(n);let m=p(n);i(m||c[Math.min(d+l.length,c.length-1)])}),document.addEventListener("click",e=>{let o=s(e);if(!o?.matches("label[for]"))return;let t=o.getAttribute("for"),n=document.getElementById(t);if(!n?.matches("[data-tui-inputotp-value-target]"))return;e.preventDefault();let u=n.closest("[data-tui-inputotp]"),l=a(u);l.length>0&&i(l[0])}),document.addEventListener("reset",e=>{let o=s(e);o?.matches("form")&&o.querySelectorAll("[data-tui-inputotp]").forEach(t=>{a(t).forEach(n=>{n.value=""}),r(t)})}),new MutationObserver(()=>{document.querySelectorAll("[data-tui-inputotp]").forEach(e=>{let o=a(e);if(o.length===0)return;let t=e.getAttribute("data-tui-inputotp-value");if(t&&!o[0].value){for(let n=0;n<o.length&&n<t.length;n++)o[n].value||(o[n].value=t[n]);r(e)}e.hasAttribute("autofocus")&&!o.some(n=>n===document.activeElement)&&requestAnimationFrame(()=>{o[0]&&!o.some(n=>n===document.activeElement)&&i(o[0])})})}).observe(document.body,{childList:!0,subtree:!0})})();})();

58
assets/js/label.js Normal file
View file

@ -0,0 +1,58 @@
(function () {
'use strict';
function updateLabelStyle(label) {
const forId = label.getAttribute('for');
const targetElement = forId ? document.getElementById(forId) : null;
const disabledStyle = label.getAttribute('data-tui-label-disabled-style');
if (!targetElement || !disabledStyle) return;
const classes = disabledStyle.split(' ').filter(Boolean);
if (targetElement.disabled) {
label.classList.add(...classes);
} else {
label.classList.remove(...classes);
}
}
document.addEventListener('DOMContentLoaded', () => {
const labelsToObserve = new Set();
// Find all labels and their targets
function setupLabels() {
document.querySelectorAll('label[for][data-tui-label-disabled-style]').forEach(label => {
updateLabelStyle(label);
const forId = label.getAttribute('for');
if (forId) labelsToObserve.add(forId);
});
}
setupLabels();
// Observe disabled changes on any element
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' &&
mutation.attributeName === 'disabled' &&
mutation.target.id &&
labelsToObserve.has(mutation.target.id)) {
document.querySelectorAll(`label[for="${mutation.target.id}"][data-tui-label-disabled-style]`).forEach(updateLabelStyle);
}
});
});
// Observe the whole document for disabled changes
observer.observe(document.body, {
attributes: true,
attributeFilter: ['disabled'],
subtree: true
});
// Watch for new labels
new MutationObserver(() => {
setupLabels();
}).observe(document.body, { childList: true, subtree: true });
});
})();

447
assets/js/popover.js Normal file
View file

@ -0,0 +1,447 @@
import "./floating_ui_core.js";
import "./floating_ui_dom.js";
(function () {
"use strict";
const floatingCleanups = new WeakMap();
const hoverTimeouts = new WeakMap();
const arrowBaseClass =
"absolute h-2.5 w-2.5 rotate-45 bg-popover border border-border";
const exitAnimationDuration = 150;
function getRootById(id) {
const root = document.getElementById(id);
return root?.matches("[data-tui-popover-root]") ? root : null;
}
function getRoots() {
return Array.from(document.querySelectorAll("[data-tui-popover-root]"));
}
function getContent(root) {
return Array.from(root?.children || []).find((child) =>
child.matches("[data-tui-popover-content]"),
);
}
function getTriggers(root) {
return Array.from(root?.children || []).filter((child) =>
child.matches("[data-tui-popover-trigger]"),
);
}
function getReferenceElement(trigger) {
let ref = trigger;
let maxArea = 0;
for (const child of trigger.children) {
const rect = child.getBoundingClientRect?.();
if (!rect) continue;
const area = rect.width * rect.height;
if (area > maxArea) {
maxArea = area;
ref = child;
}
}
return ref;
}
function isHoverRoot(root) {
return getTriggers(root).some(
(trigger) => trigger.getAttribute("data-tui-popover-type") === "hover",
);
}
function isOpenRoot(root) {
return getContent(root)?.getAttribute("data-tui-popover-open") === "true";
}
function isOpen(id) {
const root = getRootById(id);
return root ? isOpenRoot(root) : false;
}
function clearHoverTimeouts(root) {
const timeouts = hoverTimeouts.get(root);
if (!timeouts) return;
clearTimeout(timeouts.enter);
clearTimeout(timeouts.leave);
hoverTimeouts.delete(root);
}
function stopAutoUpdate(root) {
const cleanup = floatingCleanups.get(root);
if (!cleanup) return;
cleanup();
floatingCleanups.delete(root);
}
function showContent(content) {
clearTimeout(content._tuiPopoverCloseTimeout);
content._tuiPopoverCloseTimeout = null;
if (!content.matches(":popover-open")) {
try {
content.showPopover();
} catch {
return false;
}
}
requestAnimationFrame(() => {
content.setAttribute("data-tui-popover-open", "true");
});
return true;
}
function hideContent(content) {
clearTimeout(content._tuiPopoverCloseTimeout);
content._tuiPopoverCloseTimeout = null;
content.setAttribute("data-tui-popover-open", "false");
if (!content.matches(":popover-open")) {
return;
}
content._tuiPopoverCloseTimeout = setTimeout(() => {
content._tuiPopoverCloseTimeout = null;
if (content.matches(":popover-open")) {
try {
content.hidePopover();
} catch {
// ignore
}
}
}, exitAnimationDuration);
}
function arrowClassForPlacement(placement) {
switch (placement) {
case "top-start":
case "top":
case "top-end":
return `${arrowBaseClass} -bottom-[5px] border-t-transparent border-l-transparent`;
case "right-start":
case "right":
case "right-end":
return `${arrowBaseClass} -left-[5px] border-r-transparent border-t-transparent`;
case "bottom-start":
case "bottom":
case "bottom-end":
return `${arrowBaseClass} -top-[5px] border-b-transparent border-r-transparent`;
case "left-start":
case "left":
case "left-end":
return `${arrowBaseClass} -right-[5px] border-l-transparent border-b-transparent`;
default:
return `${arrowBaseClass} -top-[5px] border-b-transparent border-r-transparent`;
}
}
function updatePosition(root, triggerOverride = null) {
if (!window.FloatingUIDOM) return;
const trigger = triggerOverride || getTriggers(root)[0];
const content = getContent(root);
if (!trigger || !content) return;
const { computePosition, offset, flip, shift, arrow } =
window.FloatingUIDOM;
const reference = getReferenceElement(trigger);
const arrowEl = content.querySelector("[data-tui-popover-arrow]");
const placement =
content.getAttribute("data-tui-popover-placement") || "bottom";
const offsetValue =
parseInt(content.getAttribute("data-tui-popover-offset"), 10) ||
(arrowEl ? 8 : 4);
const middleware = [
offset(offsetValue),
flip({ padding: 10 }),
shift({ padding: 10 }),
];
if (arrowEl) {
middleware.push(arrow({ element: arrowEl, padding: 5 }));
}
// Match the fixed-position popover layer so scroll offsets stay correct.
computePosition(reference, content, {
placement,
middleware,
strategy: "fixed",
}).then(({ x, y, placement: finalPlacement, middlewareData }) => {
Object.assign(content.style, {
left: `${x}px`,
top: `${y}px`,
});
if (arrowEl && middlewareData.arrow) {
const { x: arrowX, y: arrowY } = middlewareData.arrow;
arrowEl.setAttribute("data-tui-popover-placement", finalPlacement);
arrowEl.className = arrowClassForPlacement(finalPlacement);
Object.assign(arrowEl.style, {
left: arrowX != null ? `${arrowX}px` : "",
top: arrowY != null ? `${arrowY}px` : "",
});
}
});
}
function closeRoot(root) {
const content = getContent(root);
if (!content) return;
stopAutoUpdate(root);
clearHoverTimeouts(root);
getTriggers(root).forEach((trigger) => {
trigger.setAttribute("data-tui-popover-open", "false");
});
hideContent(content);
}
function close(id) {
const root = getRootById(id);
if (root) {
closeRoot(root);
}
}
function closeAllRoots(exceptRoot = null) {
getRoots().forEach((root) => {
if (root !== exceptRoot && isOpenRoot(root)) {
closeRoot(root);
}
});
}
function closeAll(exceptId = null) {
closeAllRoots(exceptId ? getRootById(exceptId) : null);
}
function openRoot(root, triggerOverride = null) {
const content = getContent(root);
const trigger = triggerOverride || getTriggers(root)[0];
if (!content || !trigger) return;
if (content.getAttribute("data-tui-popover-exclusive") === "true") {
closeAllRoots(root);
}
if (!showContent(content)) return;
getTriggers(root).forEach((item) => {
item.setAttribute("data-tui-popover-open", "true");
});
stopAutoUpdate(root);
updatePosition(root, trigger);
if (window.FloatingUIDOM) {
const cleanup = window.FloatingUIDOM.autoUpdate(
trigger,
content,
() => updatePosition(root, trigger),
{ animationFrame: true },
);
floatingCleanups.set(root, cleanup);
}
}
function open(id) {
const root = getRootById(id);
if (root) {
openRoot(root);
}
}
function toggleRoot(root, triggerOverride = null) {
if (isOpenRoot(root)) {
closeRoot(root);
return;
}
openRoot(root, triggerOverride);
}
function toggle(id) {
const root = getRootById(id);
if (root) {
toggleRoot(root);
}
}
function clearOtherHoverRoots(activeRoot) {
getRoots().forEach((root) => {
if (root === activeRoot || !isHoverRoot(root)) {
return;
}
clearHoverTimeouts(root);
closeRoot(root);
});
}
function handleHoverEnter(root, trigger) {
const content = getContent(root);
if (!content || !isHoverRoot(root)) return;
const delay =
parseInt(content.getAttribute("data-tui-popover-hover-delay"), 10) || 100;
const timeouts = hoverTimeouts.get(root) || {};
clearOtherHoverRoots(root);
clearTimeout(timeouts.leave);
clearTimeout(timeouts.enter);
timeouts.enter = setTimeout(() => openRoot(root, trigger), delay);
hoverTimeouts.set(root, timeouts);
}
function handleHoverLeave(root, movingWithinPair) {
const content = getContent(root);
if (!content || !isHoverRoot(root)) return;
const delay =
parseInt(content.getAttribute("data-tui-popover-hover-out-delay"), 10) ||
200;
const timeouts = hoverTimeouts.get(root) || {};
clearTimeout(timeouts.enter);
if (!movingWithinPair) {
timeouts.leave = setTimeout(() => closeRoot(root), delay);
hoverTimeouts.set(root, timeouts);
}
}
document.addEventListener("click", (event) => {
const trigger = event.target.closest("[data-tui-popover-trigger]");
const root = trigger?.closest("[data-tui-popover-root]");
const triggerType = trigger?.getAttribute("data-tui-popover-type");
if (
trigger &&
root &&
triggerType !== "hover" &&
triggerType !== "manual"
) {
const disabledChild = trigger.querySelector(
':disabled, [disabled], [aria-disabled="true"]',
);
if (disabledChild) {
return;
}
event.preventDefault();
event.stopPropagation();
toggleRoot(root, trigger);
return;
}
getRoots().forEach((currentRoot) => {
const content = getContent(currentRoot);
if (
!content ||
!content.matches(":popover-open") ||
content.getAttribute("data-tui-popover-disable-clickaway") === "true"
) {
return;
}
const clickedInsideContent = content.contains(event.target);
const clickedTrigger = getTriggers(currentRoot).some((item) =>
item.contains(event.target),
);
if (!clickedInsideContent && !clickedTrigger) {
closeRoot(currentRoot);
}
});
});
document.addEventListener("mouseover", (event) => {
const trigger = event.target.closest("[data-tui-popover-trigger]");
const root = trigger?.closest("[data-tui-popover-root]");
if (
trigger &&
root &&
!trigger.contains(event.relatedTarget) &&
trigger.getAttribute("data-tui-popover-type") === "hover"
) {
handleHoverEnter(root, trigger);
}
const content = event.target.closest("[data-tui-popover-content]");
const contentRoot = content?.closest("[data-tui-popover-root]");
if (
content &&
contentRoot &&
isHoverRoot(contentRoot) &&
!content.contains(event.relatedTarget) &&
content.matches(":popover-open")
) {
const timeouts = hoverTimeouts.get(contentRoot) || {};
clearTimeout(timeouts.leave);
hoverTimeouts.set(contentRoot, timeouts);
}
});
document.addEventListener("mouseout", (event) => {
const trigger = event.target.closest("[data-tui-popover-trigger]");
const root = trigger?.closest("[data-tui-popover-root]");
if (
trigger &&
root &&
!trigger.contains(event.relatedTarget) &&
trigger.getAttribute("data-tui-popover-type") === "hover"
) {
const content = getContent(root);
handleHoverLeave(root, !!content?.contains(event.relatedTarget));
}
const content = event.target.closest("[data-tui-popover-content]");
const contentRoot = content?.closest("[data-tui-popover-root]");
if (
content &&
contentRoot &&
isHoverRoot(contentRoot) &&
!content.contains(event.relatedTarget) &&
content.matches(":popover-open")
) {
const movingToTrigger = getTriggers(contentRoot).some((item) =>
item.contains(event.relatedTarget),
);
handleHoverLeave(contentRoot, movingToTrigger);
}
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
getRoots().forEach((root) => {
const content = getContent(root);
if (
content &&
content.matches(":popover-open") &&
content.getAttribute("data-tui-popover-disable-esc") !== "true"
) {
closeRoot(root);
}
});
});
window.tui = window.tui || {};
window.tui.popover = {
open,
close,
closeAll,
toggle,
isOpen,
};
})();

File diff suppressed because one or more lines are too long

49
assets/js/progress.js Normal file
View file

@ -0,0 +1,49 @@
(function () {
'use strict';
function updateProgress(progressBar) {
const indicator = progressBar.querySelector('[data-tui-progress-indicator]');
if (!indicator) return;
const value = parseFloat(progressBar.getAttribute('aria-valuenow') || '0');
const max = parseFloat(progressBar.getAttribute('aria-valuemax') || '100') || 100;
const percentage = Math.max(0, Math.min(100, (value / max) * 100));
indicator.style.width = percentage + '%';
}
// Update all progress bars
function updateAll() {
document.querySelectorAll('[role="progressbar"]').forEach(updateProgress);
}
// Initial update and observe for changes
document.addEventListener('DOMContentLoaded', () => {
updateAll();
// Observe for attribute changes on progress bars
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' &&
(mutation.attributeName === 'aria-valuenow' ||
mutation.attributeName === 'aria-valuemax')) {
updateProgress(mutation.target);
}
});
});
// Observe all current and future progress bars
new MutationObserver(() => {
document.querySelectorAll('[role="progressbar"]').forEach((bar) => {
if (!bar.hasAttribute('data-tui-progress-observed')) {
bar.setAttribute('data-tui-progress-observed', 'true');
updateProgress(bar);
observer.observe(bar, {
attributes: true,
attributeFilter: ['aria-valuenow', 'aria-valuemax']
});
}
});
}).observe(document.body, { childList: true, subtree: true });
});
})();

216
assets/js/rating.js Normal file
View file

@ -0,0 +1,216 @@
(function () {
'use strict';
/**
* Reactive Binding for hidden inputs
*
* Problem: Setting input.value programmatically (e.g., via Datastar/Alpine)
* does NOT fire 'input' events - this is standard browser behavior since the 90s.
*
* Solution: Override the value setter to dispatch 'input' events on change.
* This is the same pattern used by Vue.js, MobX, and other reactive frameworks.
*/
function enableReactiveBinding(input) {
if (input._tui) return;
input._tui = true;
const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
if (!desc?.set) return;
Object.defineProperty(input, 'value', {
get: desc.get,
set(v) {
const old = this.value;
desc.set.call(this, v);
if (old !== v) {
this.dispatchEvent(new Event('input', { bubbles: true }));
}
},
configurable: true
});
}
// Utility functions
function getConfig(ratingElement) {
return {
value: parseFloat(ratingElement.getAttribute('data-tui-rating-initial-value')) || 0,
precision: parseFloat(ratingElement.getAttribute('data-tui-rating-precision')) || 1,
readonly: ratingElement.getAttribute('data-tui-rating-readonly') === 'true',
name: ratingElement.getAttribute('data-tui-rating-name') || '',
onlyInteger: ratingElement.getAttribute('data-tui-rating-onlyinteger') === 'true'
};
}
function getCurrentValue(ratingElement) {
return parseFloat(ratingElement.getAttribute('data-tui-rating-current')) ||
parseFloat(ratingElement.getAttribute('data-tui-rating-initial-value')) || 0;
}
function setCurrentValue(ratingElement, value) {
ratingElement.setAttribute('data-tui-rating-current', value);
const hiddenInput = ratingElement.querySelector('[data-tui-rating-hidden-input]');
if (hiddenInput) {
hiddenInput.value = value.toFixed(2);
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
}
}
function updateItemStyles(ratingElement, displayValue) {
const currentValue = getCurrentValue(ratingElement);
const valueToCompare = displayValue > 0 ? displayValue : currentValue;
ratingElement.querySelectorAll('[data-tui-rating-item]').forEach(item => {
const itemValue = parseInt(item.getAttribute('data-tui-rating-value'), 10);
if (isNaN(itemValue)) return;
const foreground = item.querySelector('[data-tui-rating-item-foreground]');
if (!foreground) return;
const filled = itemValue <= Math.floor(valueToCompare);
const partial = !filled && itemValue - 1 < valueToCompare && valueToCompare < itemValue;
const percentage = partial ? (valueToCompare - Math.floor(valueToCompare)) * 100 : 0;
foreground.style.width = filled ? '100%' : partial ? `${percentage}%` : '0%';
});
}
function getMaxValue(ratingElement) {
let max = 0;
ratingElement.querySelectorAll('[data-tui-rating-item]').forEach(item => {
const value = parseInt(item.getAttribute('data-tui-rating-value'), 10);
if (!isNaN(value) && value > max) max = value;
});
return Math.max(1, max);
}
// Event handlers
document.addEventListener('click', (e) => {
const item = e.target.closest('[data-tui-rating-item]');
if (!item) return;
const ratingElement = item.closest('[data-tui-rating-component]');
if (!ratingElement) return;
const config = getConfig(ratingElement);
if (config.readonly) return;
const itemValue = parseInt(item.getAttribute('data-tui-rating-value'), 10);
if (isNaN(itemValue)) return;
const currentValue = getCurrentValue(ratingElement);
const maxValue = getMaxValue(ratingElement);
let newValue = itemValue;
if (config.onlyInteger) {
newValue = Math.round(newValue);
} else {
if (currentValue === newValue && newValue % 1 === 0) {
newValue = Math.max(0, newValue - config.precision);
} else {
newValue = Math.round(newValue / config.precision) * config.precision;
}
}
newValue = Math.max(0, Math.min(maxValue, newValue));
setCurrentValue(ratingElement, newValue);
updateItemStyles(ratingElement, 0);
ratingElement.dispatchEvent(
new CustomEvent('rating-change', {
bubbles: true,
detail: { name: config.name, value: newValue, maxValue }
})
);
});
document.addEventListener('mouseover', (e) => {
const item = e.target.closest('[data-tui-rating-item]');
if (!item) return;
const ratingElement = item.closest('[data-tui-rating-component]');
if (!ratingElement || getConfig(ratingElement).readonly) return;
const previewValue = parseInt(item.getAttribute('data-tui-rating-value'), 10);
if (!isNaN(previewValue)) {
updateItemStyles(ratingElement, previewValue);
}
});
document.addEventListener('mouseout', (e) => {
const ratingElement = e.target.closest('[data-tui-rating-component]');
if (!ratingElement || getConfig(ratingElement).readonly) return;
// Check if we're leaving the rating component entirely
if (!ratingElement.contains(e.relatedTarget)) {
updateItemStyles(ratingElement, 0);
}
});
// Handle hidden input value changes (for reactive frameworks)
document.addEventListener('input', (e) => {
if (!e.target.matches('[data-tui-rating-hidden-input]')) return;
const ratingElement = e.target.closest('[data-tui-rating-component]');
if (ratingElement) {
const value = parseFloat(e.target.value) || 0;
const config = getConfig(ratingElement);
const maxValue = getMaxValue(ratingElement);
const clampedValue = Math.max(0, Math.min(maxValue, value));
ratingElement.setAttribute('data-tui-rating-current', clampedValue);
updateItemStyles(ratingElement, 0);
}
});
// Form reset
document.addEventListener('reset', (e) => {
if (!e.target.matches('form')) return;
e.target.querySelectorAll('[data-tui-rating-component]').forEach(ratingElement => {
const config = getConfig(ratingElement);
setCurrentValue(ratingElement, config.value);
updateItemStyles(ratingElement, 0);
});
});
// Initialize ratings
function initializeRatings() {
document.querySelectorAll('[data-tui-rating-component]').forEach(ratingElement => {
// Enable reactive binding for hidden input
const hiddenInput = ratingElement.querySelector('[data-tui-rating-hidden-input]');
if (hiddenInput && !hiddenInput._tui) {
enableReactiveBinding(hiddenInput);
}
// Initialize current value if not set
if (!ratingElement.hasAttribute('data-tui-rating-current')) {
const config = getConfig(ratingElement);
const maxValue = getMaxValue(ratingElement);
const value = Math.max(0, Math.min(maxValue, config.value));
setCurrentValue(ratingElement, Math.round(value / config.precision) * config.precision);
}
// Update styles
updateItemStyles(ratingElement, 0);
// Set cursor styles
const config = getConfig(ratingElement);
if (config.readonly) {
ratingElement.style.cursor = 'default';
ratingElement.querySelectorAll('[data-tui-rating-item]').forEach(item => {
item.style.cursor = 'default';
});
}
});
}
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeRatings);
} else {
initializeRatings();
}
// MutationObserver for dynamically added elements
new MutationObserver(initializeRatings).observe(document.body, { childList: true, subtree: true });
})();

507
assets/js/selectbox.js Normal file
View file

@ -0,0 +1,507 @@
(function () {
'use strict';
/**
* Reactive Binding for hidden inputs
*
* Problem: Setting input.value programmatically (e.g., via Datastar/Alpine)
* does NOT fire 'input' events - this is standard browser behavior since the 90s.
*
* Solution: Override the value setter to dispatch 'input' events on change.
* This is the same pattern used by Vue.js, MobX, and other reactive frameworks.
*/
function enableReactiveBinding(input) {
if (input._tui) return;
input._tui = true;
const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
if (!desc?.set) return;
Object.defineProperty(input, 'value', {
get: desc.get,
set(v) {
const old = this.value;
desc.set.call(this, v);
if (old !== v) {
this.dispatchEvent(new Event('input', { bubbles: true }));
}
},
configurable: true
});
}
function updateTriggerClearState(trigger) {
const clearTrigger = trigger.querySelector('[data-tui-selectbox-clear-trigger]');
if (!clearTrigger) return;
const chevron = trigger.querySelector('[data-tui-selectbox-chevron]');
const hiddenInput = trigger.querySelector('input[type="hidden"]');
const hasSelection = !!hiddenInput?.value && !trigger.disabled;
clearTrigger.classList.toggle('hidden', !hasSelection);
if (chevron) chevron.classList.toggle('hidden', hasSelection);
}
function clearFromTrigger(trigger) {
const hiddenInput = trigger.querySelector('input[type="hidden"]');
if (!hiddenInput || !hiddenInput.value) return;
hiddenInput.value = '';
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
}
function getContainer(element) {
return element?.closest('.select-container') || null;
}
function getTriggerFromContainer(container) {
return container?.querySelector('button.select-trigger') || null;
}
function getContentFromContainer(container) {
return container?.querySelector('[data-tui-selectbox-content]') || null;
}
function getContentFromTrigger(trigger) {
return getContentFromContainer(getContainer(trigger));
}
function syncContentWidth(trigger) {
const content = getContentFromTrigger(trigger);
if (!content) return;
const width = trigger.getBoundingClientRect().width;
content.style.width = `${width}px`;
content.style.minWidth = `${width}px`;
}
function closePopover(trigger) {
const content = getContentFromTrigger(trigger);
if (!content?.matches(':popover-open')) return;
try {
content.hidePopover();
} catch {
// ignore
}
}
// Helper to sync selections from hidden input value
function syncSelectionsFromValue(trigger) {
const hiddenInput = trigger.querySelector('input[type="hidden"]');
const content = getContentFromTrigger(trigger);
if (!hiddenInput || !content) return;
const isMultiple = trigger.getAttribute('data-tui-selectbox-multiple') === 'true';
const values = hiddenInput.value ? (isMultiple ? hiddenInput.value.split(',') : [hiddenInput.value]) : [];
content.querySelectorAll('.select-item').forEach(item => {
const itemValue = item.getAttribute('data-tui-selectbox-value') || '';
const shouldBeSelected = values.includes(itemValue);
const isSelected = item.getAttribute('data-tui-selectbox-selected') === 'true';
if (shouldBeSelected !== isSelected) {
item.setAttribute('data-tui-selectbox-selected', shouldBeSelected.toString());
}
});
}
// Helper to update display value
function updateDisplayValue(trigger) {
const valueEl = trigger.querySelector('.select-value');
const hiddenInput = trigger.querySelector('input[type="hidden"]');
const content = getContentFromTrigger(trigger);
if (!valueEl) {
updateTriggerClearState(trigger);
return;
}
// If no content yet (not opened), try to init from hidden input value
if (!content && hiddenInput && hiddenInput.value) {
valueEl.textContent = hiddenInput.value; // Simple fallback
valueEl.classList.remove('text-muted-foreground');
updateTriggerClearState(trigger);
return;
}
if (!content) {
updateTriggerClearState(trigger);
return;
}
const isMultiple = trigger.getAttribute('data-tui-selectbox-multiple') === 'true';
const showPills = trigger.getAttribute('data-tui-selectbox-show-pills') === 'true';
const placeholder = valueEl.getAttribute('data-tui-selectbox-placeholder') || 'Select...';
const selectedItems = content.querySelectorAll('.select-item[data-tui-selectbox-selected="true"]');
if (selectedItems.length === 0) {
valueEl.textContent = placeholder;
valueEl.classList.add('text-muted-foreground');
if (hiddenInput) hiddenInput.value = '';
updateTriggerClearState(trigger);
return;
}
valueEl.classList.remove('text-muted-foreground');
if (isMultiple) {
if (showPills) {
// Create pills container
valueEl.innerHTML = '';
const pillsContainer = document.createElement('div');
pillsContainer.className = 'flex flex-wrap gap-1 items-center min-h-[1.5rem]';
const pills = [];
Array.from(selectedItems).forEach(selectedItem => {
const pill = document.createElement('span');
pill.className = 'inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded-md bg-primary text-primary-foreground';
const text = document.createElement('span');
text.textContent = selectedItem.querySelector('.select-item-text')?.textContent || '';
pill.appendChild(text);
// Add remove button for pills
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-0.5 hover:text-destructive focus:outline-none';
removeBtn.type = 'button';
removeBtn.innerHTML = '×';
removeBtn.setAttribute('data-tui-selectbox-pill-remove', '');
removeBtn.setAttribute('data-tui-selectbox-value', selectedItem.getAttribute('data-tui-selectbox-value'));
pill.appendChild(removeBtn);
pills.push(pill);
});
// Try adding pills and check overflow
pills.forEach(pill => pillsContainer.appendChild(pill));
valueEl.appendChild(pillsContainer);
// Check overflow after render
requestAnimationFrame(() => {
const containerWidth = valueEl.offsetWidth;
const contentWidth = pillsContainer.scrollWidth;
// If pills overflow and we have more than 3 items, switch to count
if (contentWidth > containerWidth - 10 && selectedItems.length > 3) {
const countText = trigger.getAttribute('data-tui-selectbox-selected-count-text') || '{n} items selected';
valueEl.textContent = countText.replace('{n}', selectedItems.length);
}
});
} else {
const countText = trigger.getAttribute('data-tui-selectbox-selected-count-text') || '{n} items selected';
valueEl.textContent = countText.replace('{n}', selectedItems.length);
}
// Update hidden input with CSV
const values = Array.from(selectedItems).map(item =>
item.getAttribute('data-tui-selectbox-value') || ''
);
if (hiddenInput) hiddenInput.value = values.join(',');
} else {
// Single selection
const selectedItem = selectedItems[0];
const text = selectedItem.querySelector('.select-item-text')?.textContent || '';
valueEl.textContent = text;
if (hiddenInput) {
hiddenInput.value = selectedItem.getAttribute('data-tui-selectbox-value') || '';
}
}
updateTriggerClearState(trigger);
}
function normalizeSearchValue(value) {
return (value || '').toLowerCase().trim().replace(/\s+/g, ' ');
}
function fuzzyMatch(searchTerm, candidate) {
const needle = normalizeSearchValue(searchTerm).replace(/\s+/g, '');
const haystack = normalizeSearchValue(candidate).replace(/\s+/g, '');
if (!needle) return true;
if (!haystack) return false;
if (haystack.includes(needle)) return true;
let needleIndex = 0;
for (let i = 0; i < haystack.length && needleIndex < needle.length; i += 1) {
if (haystack[i] === needle[needleIndex]) {
needleIndex += 1;
}
}
return needleIndex === needle.length;
}
// Helper to filter items based on search
function filterItems(searchInput) {
const searchTerm = normalizeSearchValue(searchInput.value);
const content = searchInput.closest('[data-tui-selectbox-content]');
if (!content) return;
content.querySelectorAll('.select-item').forEach(item => {
const text = normalizeSearchValue(item.querySelector('.select-item-text')?.textContent);
const value = normalizeSearchValue(item.getAttribute('data-tui-selectbox-value'));
const visible = !searchTerm || fuzzyMatch(searchTerm, text) || fuzzyMatch(searchTerm, value);
item.style.display = visible ? '' : 'none';
});
}
// Helper to select/deselect item
function toggleItem(item) {
if (item.getAttribute('data-tui-selectbox-disabled') === 'true') return;
const content = item.closest('[data-tui-selectbox-content]');
const trigger = getTriggerFromContainer(getContainer(item));
if (!trigger) return;
const isMultiple = trigger.getAttribute('data-tui-selectbox-multiple') === 'true';
const isSelected = item.getAttribute('data-tui-selectbox-selected') === 'true';
if (!isMultiple) {
// Single selection - deselect all others
content.querySelectorAll('.select-item').forEach(el => {
el.setAttribute('data-tui-selectbox-selected', 'false');
});
}
// Toggle this item
item.setAttribute('data-tui-selectbox-selected', (!isSelected).toString());
// Update display
updateDisplayValue(trigger);
// Trigger change event
const hiddenInput = trigger.querySelector('input[type="hidden"]');
if (hiddenInput) {
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
}
// Close on single selection
if (!isMultiple) {
closePopover(trigger);
setTimeout(() => trigger.focus(), 50);
}
}
// Initialize display values for existing selectboxes
function initializeDisplayValues() {
document.querySelectorAll('.select-container').forEach(container => {
const trigger = container.querySelector('button.select-trigger');
if (trigger) {
updateDisplayValue(trigger);
}
});
}
// Handle clear in capture phase so trigger popover doesn't open.
document.addEventListener('pointerdown', (e) => {
const clearTrigger = e.target.closest('[data-tui-selectbox-clear-trigger]');
if (!clearTrigger) return;
e.preventDefault();
e.stopPropagation();
const trigger = clearTrigger.closest('button.select-trigger');
if (trigger) {
// The clear icon may disappear before click fires; suppress that next click.
trigger.setAttribute('data-tui-selectbox-suppress-click', 'true');
clearFromTrigger(trigger);
}
}, true);
// Block follow-up click event after clear so trigger/popover handlers don't run.
document.addEventListener('click', (e) => {
const clearTrigger = e.target.closest('[data-tui-selectbox-clear-trigger]');
const trigger = e.target.closest('button.select-trigger');
if (clearTrigger) {
e.preventDefault();
e.stopPropagation();
return;
}
if (trigger?.getAttribute('data-tui-selectbox-suppress-click') === 'true') {
trigger.removeAttribute('data-tui-selectbox-suppress-click');
e.preventDefault();
e.stopPropagation();
}
}, true);
// Global click handler using Event Delegation
document.addEventListener('click', (e) => {
// Handle pill remove clicks
if (e.target.matches('[data-tui-selectbox-pill-remove]')) {
e.stopPropagation();
const value = e.target.getAttribute('data-tui-selectbox-value');
const trigger = e.target.closest('button.select-trigger');
const content = trigger ? getContentFromTrigger(trigger) : null;
const item = content?.querySelector(`.select-item[data-tui-selectbox-value="${value}"]`);
if (item) toggleItem(item);
return;
}
// Handle item clicks
const item = e.target.closest('.select-item');
if (item) {
e.preventDefault();
toggleItem(item);
return;
}
// Focus search when trigger clicked
const trigger = e.target.closest('button.select-trigger');
if (trigger) {
const content = getContentFromTrigger(trigger);
syncContentWidth(trigger);
const searchInput = content?.querySelector('[data-tui-selectbox-search]');
if (searchInput) {
requestAnimationFrame(() => {
if (content?.matches(':popover-open')) searchInput.focus();
});
} else {
requestAnimationFrame(() => {
const firstItem = content?.querySelector('.select-item');
if (firstItem) firstItem.focus();
});
}
}
});
// Global input handler for search and value changes
document.addEventListener('input', (e) => {
// Handle search input
if (e.target.matches('[data-tui-selectbox-search]')) {
filterItems(e.target);
return;
}
// Handle hidden input value changes (for reactive frameworks)
if (e.target.matches('[data-tui-selectbox-hidden-input]')) {
const trigger = e.target.closest('.select-trigger');
if (trigger) {
syncSelectionsFromValue(trigger);
updateDisplayValue(trigger);
}
}
});
// Global keydown handler
document.addEventListener('keydown', (e) => {
const activeElement = document.activeElement;
// Handle typing on trigger to open and search
if (activeElement?.matches('button.select-trigger')) {
if (e.key.length === 1 || e.key === 'Backspace') {
e.preventDefault();
const content = getContentFromTrigger(activeElement);
activeElement.click(); // Open popover
setTimeout(() => {
const searchInput = content?.querySelector('[data-tui-selectbox-search]');
if (searchInput) {
searchInput.focus();
if (e.key !== 'Backspace') searchInput.value = e.key;
}
}, 50);
}
}
// Handle arrow navigation in content
const content = activeElement?.closest('[data-tui-selectbox-content]');
if (content?.querySelector('.select-item')) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
const visibleItems = Array.from(content.querySelectorAll('.select-item'))
.filter(item => item.style.display !== 'none');
if (visibleItems.length === 0) return;
const currentFocused = content.querySelector('.select-item:focus');
let nextIndex = 0;
if (currentFocused) {
const currentIndex = visibleItems.indexOf(currentFocused);
nextIndex = e.key === 'ArrowDown'
? (currentIndex + 1) % visibleItems.length
: (currentIndex - 1 + visibleItems.length) % visibleItems.length;
}
visibleItems[nextIndex].focus();
} else if (e.key === 'Enter' && activeElement?.matches('.select-item')) {
e.preventDefault();
toggleItem(activeElement);
} else if (e.key === 'Escape') {
const searchInput = content.querySelector('[data-tui-selectbox-search]');
if (activeElement?.matches('.select-item')) {
searchInput?.focus();
} else if (activeElement === searchInput) {
const trigger = getTriggerFromContainer(getContainer(content));
if (trigger) {
closePopover(trigger);
}
setTimeout(() => trigger?.focus(), 50);
}
}
}
});
// Global form reset handler
document.addEventListener('reset', (e) => {
if (!e.target.matches('form')) return;
e.target.querySelectorAll('.select-container').forEach(wrapper => {
const trigger = wrapper.querySelector('button.select-trigger');
const content = trigger ? getContentFromTrigger(trigger) : null;
if (content) {
// Clear selections
content.querySelectorAll('.select-item').forEach(item => {
item.setAttribute('data-tui-selectbox-selected', 'false');
});
// Clear search
const searchInput = content.querySelector('[data-tui-selectbox-search]');
if (searchInput) {
searchInput.value = '';
filterItems(searchInput);
}
}
if (trigger) updateDisplayValue(trigger);
});
});
// Initialize selectboxes on DOM ready and handle dynamic content
function initializeSelectBoxes() {
document.querySelectorAll('.select-container').forEach(container => {
const trigger = container.querySelector('button.select-trigger');
if (trigger && !trigger.hasAttribute('data-initialized')) {
trigger.setAttribute('data-initialized', 'true');
// Enable reactive binding for hidden input
const hiddenInput = trigger.querySelector('input[type="hidden"]');
if (hiddenInput) {
enableReactiveBinding(hiddenInput);
}
updateDisplayValue(trigger);
}
});
}
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeSelectBoxes);
} else {
initializeSelectBoxes();
}
// Simple MutationObserver just for initialization
new MutationObserver(initializeSelectBoxes).observe(document.body, { childList: true, subtree: true });
})();

File diff suppressed because one or more lines are too long

129
assets/js/sidebar.js Normal file
View file

@ -0,0 +1,129 @@
(function () {
"use strict";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
// Portal setup - moves content between desktop and mobile views
function setupMobilePortals() {
const contentElements = document.querySelectorAll(
"[data-tui-sidebar-content]",
);
contentElements.forEach((content) => {
const sidebarId = content.getAttribute("data-tui-sidebar-content");
const portal = document.querySelector(
`[data-tui-sidebar-mobile-portal="${sidebarId}"]`,
);
if (!portal) return;
// Check viewport and move content if needed
const isMobile = window.matchMedia("(max-width: 767px)").matches;
if (isMobile && content.parentElement !== portal) {
// Move to mobile portal
portal.appendChild(content);
} else if (!isMobile && content.parentElement === portal) {
// Move back to desktop sidebar
const desktopContainer = document.querySelector(
`[data-tui-sidebar-wrapper][data-tui-sidebar-id="${sidebarId}"] [data-sidebar="sidebar"] > div`,
);
if (desktopContainer) {
desktopContainer.appendChild(content);
}
}
});
}
// Initial setup
setupMobilePortals();
// Handle viewport changes
window.addEventListener("resize", setupMobilePortals);
// Handle DOM updates (for HTMX, Alpine, etc.)
const observer = new MutationObserver(setupMobilePortals);
observer.observe(document.body, {
childList: true,
subtree: true,
});
// Event delegation for sidebar interactions (Desktop only - Mobile uses Sheet)
document.addEventListener("click", (e) => {
const trigger = e.target.closest("[data-tui-sidebar-trigger]");
if (trigger) {
e.preventDefault();
const targetId = trigger.getAttribute("data-tui-sidebar-target");
if (targetId) {
toggleSidebar(targetId);
}
}
});
// Handle keyboard shortcuts
document.addEventListener("keydown", (e) => {
// Ctrl/Cmd + key - toggle sidebar
if ((e.ctrlKey || e.metaKey) && e.key.length === 1) {
const wrapper = document.querySelector('[data-tui-sidebar-wrapper]');
if (!wrapper) return;
const shortcut = wrapper.getAttribute("data-tui-sidebar-keyboard-shortcut");
if (!shortcut || shortcut.toLowerCase() !== e.key.toLowerCase()) return;
e.preventDefault();
const sidebar = wrapper.querySelector('[data-sidebar="sidebar"]');
if (sidebar && sidebar.id) {
toggleSidebar(sidebar.id);
}
}
});
function toggleSidebar(sidebarId) {
const wrapper = document.querySelector(
`[data-tui-sidebar-wrapper][data-tui-sidebar-id="${sidebarId}"]`,
);
if (!wrapper) return;
const collapsible = wrapper.getAttribute("data-tui-sidebar-collapsible");
// Don't toggle if collapsible is "none"
if (collapsible === "none") {
return;
}
const currentState = wrapper.getAttribute("data-tui-sidebar-state");
const newState = currentState === "expanded" ? "collapsed" : "expanded";
setSidebarState(sidebarId, newState);
}
function setSidebarState(sidebarId, state) {
const wrapper = document.querySelector(
`[data-tui-sidebar-wrapper][data-tui-sidebar-id="${sidebarId}"]`,
);
if (!wrapper) return;
const collapsible = wrapper.getAttribute("data-tui-sidebar-collapsible");
// Don't change state if collapsible is "none"
if (collapsible === "none") {
return;
}
// Update data-tui-sidebar-state attribute
wrapper.setAttribute("data-tui-sidebar-state", state);
// For icon mode, also set data-tui-sidebar-collapsible when collapsed
if (state === "collapsed" && collapsible) {
wrapper.setAttribute("data-tui-sidebar-collapsible", collapsible);
}
// Save state to cookie
const cookieValue = state === "expanded" ? "true" : "false";
saveSidebarState(sidebarId, cookieValue);
}
function saveSidebarState(sidebarId, cookieValue) {
document.cookie = `${SIDEBAR_COOKIE_NAME}=${cookieValue}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
}
})();

26
assets/js/slider.js Normal file
View file

@ -0,0 +1,26 @@
(function () {
'use strict';
// Update value display elements
document.addEventListener('input', (e) => {
const slider = e.target.closest('input[type="range"][data-tui-slider-input]');
if (!slider || !slider.id) return;
document.querySelectorAll(`[data-tui-slider-value][data-tui-slider-value-for="${slider.id}"]`).forEach(el => {
el.textContent = slider.value;
});
});
// MutationObserver for initial value setup
new MutationObserver(() => {
document.querySelectorAll('input[type="range"][data-tui-slider-input]').forEach(slider => {
if (!slider.id) return;
document.querySelectorAll(`[data-tui-slider-value][data-tui-slider-value-for="${slider.id}"]`).forEach(el => {
if (!el.textContent || el.textContent === '') {
el.textContent = slider.value;
}
});
});
}).observe(document.body, { childList: true, subtree: true });
})();

72
assets/js/tabs.js Normal file
View file

@ -0,0 +1,72 @@
(function () {
"use strict";
// Update tab state
function setActiveTab(tabsId, value) {
// Update all triggers with this tabs-id
document
.querySelectorAll(`[data-tui-tabs-trigger][data-tui-tabs-id="${tabsId}"]`)
.forEach((trigger) => {
const isActive = trigger.getAttribute("data-tui-tabs-value") === value;
trigger.setAttribute(
"data-tui-tabs-state",
isActive ? "active" : "inactive",
);
});
// Update all contents with this tabs-id
document
.querySelectorAll(`[data-tui-tabs-content][data-tui-tabs-id="${tabsId}"]`)
.forEach((content) => {
const isActive = content.getAttribute("data-tui-tabs-value") === value;
content.setAttribute(
"data-tui-tabs-state",
isActive ? "active" : "inactive",
);
content.classList.toggle("hidden", !isActive);
});
}
// Click handler
document.addEventListener("click", (e) => {
const trigger = e.target.closest("[data-tui-tabs-trigger]");
if (!trigger) return;
const tabsId = trigger.getAttribute("data-tui-tabs-id");
const value = trigger.getAttribute("data-tui-tabs-value");
if (tabsId && value) {
setActiveTab(tabsId, value);
}
});
// Initialize active states
function setupInitialStates() {
document.querySelectorAll("[data-tui-tabs]").forEach((container) => {
const tabsId = container.getAttribute("data-tui-tabs-id");
if (!tabsId) return;
// Find active trigger or use first
const activeTrigger =
container.querySelector(
`[data-tui-tabs-trigger][data-tui-tabs-state="active"]`,
) || container.querySelector(`[data-tui-tabs-trigger]`);
if (activeTrigger) {
setActiveTab(tabsId, activeTrigger.getAttribute("data-tui-tabs-value"));
}
});
}
// Setup on load and mutations
document.addEventListener("DOMContentLoaded", setupInitialStates);
new MutationObserver(setupInitialStates).observe(document.body, {
childList: true,
subtree: true,
});
// Expose public API
window.tui = window.tui || {};
window.tui.tabs = {
setActive: setActiveTab,
};
})();

213
assets/js/tagsinput.js Normal file
View file

@ -0,0 +1,213 @@
(function () {
'use strict';
function getSelectedTags(container) {
return Array.from(container.querySelectorAll('[data-tui-tagsinput-hidden-inputs] input'))
.map(i => i.value.toLowerCase());
}
function getSuggestionsRoot(container) {
const id = container.getAttribute('data-tui-tagsinput-suggestions-id');
if (!id) return null;
const root = document.getElementById(id);
return root?.matches('[data-tui-popover-root]') ? root : null;
}
function getSuggestionsContent(container) {
return getSuggestionsRoot(container)?.querySelector('[data-tui-popover-content]') || null;
}
function showSuggestions(container, query) {
const id = container.getAttribute('data-tui-tagsinput-suggestions-id');
const popup = getSuggestionsContent(container);
const input = container.querySelector('[data-tui-tagsinput-text-input]');
if (!id || !popup || !input) return;
popup.style.setProperty('--trigger-width', `${input.getBoundingClientRect().width}px`);
const selected = getSelectedTags(container);
const q = query.toLowerCase().trim();
let first = null;
popup.querySelectorAll('[data-tui-tagsinput-suggestion]').forEach(el => {
const val = el.getAttribute('data-tui-tagsinput-suggestion-value').toLowerCase();
const show = !selected.includes(val) && val.includes(q);
el.style.display = show ? '' : 'none';
el.classList.remove('bg-accent');
if (show && !first) {
first = el;
}
});
if (first) {
first.classList.add('bg-accent');
window.tui?.popover?.open(id);
} else {
window.tui?.popover?.close(id);
}
}
function getVisibleSuggestions(container) {
const popup = getSuggestionsContent(container);
if (!popup) return [];
return Array.from(popup.querySelectorAll('[data-tui-tagsinput-suggestion]'))
.filter(el => el.style.display !== 'none');
}
function moveSelection(container, dir) {
const items = getVisibleSuggestions(container);
if (!items.length) return;
const current = items.findIndex(el => el.classList.contains('bg-accent'));
items.forEach(el => el.classList.remove('bg-accent'));
let next = dir === 'down' ? current + 1 : current - 1;
if (next >= items.length) next = 0;
if (next < 0) next = items.length - 1;
items[next].classList.add('bg-accent');
items[next].scrollIntoView({ block: 'nearest' });
}
function addTag(container, value) {
const input = container.querySelector('[data-tui-tagsinput-text-input]');
const val = value.trim();
if (!val || input?.disabled) return;
if (getSelectedTags(container).includes(val.toLowerCase())) {
input.value = '';
return;
}
// Create chip
const chip = document.createElement('div');
chip.className = 'inline-flex items-center gap-2 rounded-md border px-2.5 py-0.5 text-xs font-semibold border-transparent bg-primary text-primary-foreground';
chip.setAttribute('data-tui-tagsinput-chip', '');
chip.innerHTML = `<span>${val}</span><button type="button" class="ml-1 hover:text-destructive cursor-pointer" data-tui-tagsinput-remove><svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button>`;
// Add chip to chips container
container.querySelector('[data-tui-tagsinput-chips]').appendChild(chip);
// Add hidden input
const hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = container.getAttribute('data-tui-tagsinput-name') || '';
hidden.value = val;
container.querySelector('[data-tui-tagsinput-hidden-inputs]').appendChild(hidden);
input.value = '';
}
// Focus → show suggestions
document.addEventListener('focusin', e => {
const input = e.target.closest('[data-tui-tagsinput-text-input]');
if (!input) return;
const container = input.closest('[data-tui-tagsinput]');
if (container) showSuggestions(container, input.value);
});
// Focusout → close suggestions
document.addEventListener('focusout', e => {
const input = e.target.closest('[data-tui-tagsinput-text-input]');
if (!input) return;
const container = input.closest('[data-tui-tagsinput]');
const id = container?.getAttribute('data-tui-tagsinput-suggestions-id');
const nextTarget = e.relatedTarget;
if (container?.contains(nextTarget) || getSuggestionsContent(container)?.contains(nextTarget)) return;
if (id) window.tui?.popover?.close(id);
});
// Input → filter suggestions
document.addEventListener('input', e => {
const input = e.target.closest('[data-tui-tagsinput-text-input]');
if (!input) return;
const container = input.closest('[data-tui-tagsinput]');
if (container) showSuggestions(container, input.value);
});
// Suggestion selection (mousedown to fire before focusout)
document.addEventListener('mousedown', e => {
const suggestion = e.target.closest('[data-tui-tagsinput-suggestion]');
if (!suggestion) return;
e.preventDefault(); // Prevent focus change
const popupRoot = suggestion.closest('[data-tui-popover-root]');
const container = document.querySelector(`[data-tui-tagsinput-suggestions-id="${popupRoot?.id}"]`);
if (container) {
addTag(container, suggestion.getAttribute('data-tui-tagsinput-suggestion-value'));
showSuggestions(container, '');
}
});
// Click
document.addEventListener('click', e => {
// Click on input → show suggestions (handles re-click on already focused input)
const inputClick = e.target.closest('[data-tui-tagsinput-text-input]');
if (inputClick) {
const container = inputClick.closest('[data-tui-tagsinput]');
if (container) showSuggestions(container, inputClick.value);
return;
}
// Remove click
const remove = e.target.closest('[data-tui-tagsinput-remove]');
if (remove) {
const chip = remove.closest('[data-tui-tagsinput-chip]');
const container = chip?.closest('[data-tui-tagsinput]');
const val = chip?.querySelector('span')?.textContent;
chip?.remove();
container?.querySelector(`[data-tui-tagsinput-hidden-inputs] input[value="${val}"]`)?.remove();
return;
}
// Click on container → focus input
const container = e.target.closest('[data-tui-tagsinput]');
if (container && !e.target.closest('input')) {
container.querySelector('[data-tui-tagsinput-text-input]')?.focus();
}
});
// Keyboard
document.addEventListener('keydown', e => {
const input = e.target.closest('[data-tui-tagsinput-text-input]');
if (!input) return;
const container = input.closest('[data-tui-tagsinput]');
if (!container) return;
const id = container.getAttribute('data-tui-tagsinput-suggestions-id');
const isOpen = id && window.tui?.popover?.isOpen(id);
if (e.key === 'ArrowDown' && isOpen) {
e.preventDefault();
moveSelection(container, 'down');
} else if (e.key === 'ArrowUp' && isOpen) {
e.preventDefault();
moveSelection(container, 'up');
} else if (e.key === 'Enter') {
e.preventDefault();
const items = getVisibleSuggestions(container);
const selected = items.find(el => el.classList.contains('bg-accent'));
if (selected) {
addTag(container, selected.getAttribute('data-tui-tagsinput-suggestion-value'));
showSuggestions(container, '');
} else {
addTag(container, input.value);
showSuggestions(container, '');
}
} else if (e.key === ',') {
e.preventDefault();
addTag(container, input.value);
showSuggestions(container, '');
} else if (e.key === 'Escape' && isOpen) {
e.preventDefault();
window.tui?.popover?.close(id);
} else if (e.key === 'Backspace' && input.value === '') {
const chipsContainer = container.querySelector('[data-tui-tagsinput-chips]');
const lastChip = chipsContainer?.lastElementChild;
if (lastChip) {
const val = lastChip.querySelector('span')?.textContent;
lastChip.remove();
container.querySelector(`[data-tui-tagsinput-hidden-inputs] input[value="${val}"]`)?.remove();
}
}
});
})();

View file

@ -1,11 +1 @@
(()=>{(function(){"use strict";function d(t,e){let n=document.createElement("div");return n.setAttribute("data-tui-tagsinput-chip",""),n.className="inline-flex items-center gap-2 rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground",n.innerHTML=`
<span>${t}</span>
<button type="button"
class="ml-1 text-current hover:text-destructive disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
data-tui-tagsinput-remove=""
${e?"disabled":""}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
`,n}function c(t,e){let n=t.querySelector("[data-tui-tagsinput-text-input]");if(n?.hasAttribute("disabled"))return;let i=e.trim();if(!i)return;let a=t.querySelector("[data-tui-tagsinput-hidden-inputs]"),u=t.querySelector("[data-tui-tagsinput-container]"),p=t.getAttribute("data-tui-tagsinput-name"),o=t.getAttribute("data-tui-tagsinput-form"),l=a.querySelectorAll('input[type="hidden"]');for(let f of l)if(f.value.toLowerCase()===i.toLowerCase()){n.value="";return}let g=d(i,n?.hasAttribute("disabled"));u.appendChild(g);let r=document.createElement("input");r.type="hidden",r.name=p,r.value=i,o!==null&&o!==""&&r.setAttribute("form",o),a.appendChild(r),n.value=""}function s(t){let e=t.closest("[data-tui-tagsinput-chip]");if(!e)return;let n=e.closest("[data-tui-tagsinput]"),i=e.querySelector("span").textContent.trim(),u=n.querySelector("[data-tui-tagsinput-hidden-inputs]").querySelector(`input[type="hidden"][value="${i}"]`);u&&u.remove(),e.remove()}document.addEventListener("keydown",t=>{let e=t.target.closest("[data-tui-tagsinput-text-input]");if(!e)return;let n=e.closest("[data-tui-tagsinput]");if(n){if(t.key==="Enter"||t.key===",")t.preventDefault(),c(n,e.value);else if(t.key==="Backspace"&&e.value===""){t.preventDefault();let a=n.querySelector("[data-tui-tagsinput-chip]:last-child")?.querySelector("[data-tui-tagsinput-remove]");a&&!a.disabled&&s(a)}}}),document.addEventListener("click",t=>{let e=t.target.closest("[data-tui-tagsinput-remove]");if(e&&!e.disabled){t.preventDefault(),t.stopPropagation(),s(e);return}let n=t.target.closest("[data-tui-tagsinput]");if(n&&!t.target.closest("input")){let i=n.querySelector("[data-tui-tagsinput-text-input]");i&&i.focus()}}),document.addEventListener("reset",t=>{t.target.matches("form")&&t.target.querySelectorAll("[data-tui-tagsinput]").forEach(e=>{e.querySelectorAll("[data-tui-tagsinput-chip]").forEach(i=>i.remove()),e.querySelectorAll('[data-tui-tagsinput-hidden-inputs] input[type="hidden"]').forEach(i=>i.remove());let n=e.querySelector("[data-tui-tagsinput-text-input]");n&&(n.value="")})})})();})();
(()=>{(function(){"use strict";function p(e){return Array.from(e.querySelectorAll("[data-tui-tagsinput-hidden-inputs] input")).map(n=>n.value.toLowerCase())}function y(e){let n=e.getAttribute("data-tui-tagsinput-suggestions-id");if(!n)return null;let t=document.getElementById(n);return t?.matches("[data-tui-popover-root]")?t:null}function l(e){return y(e)?.querySelector("[data-tui-popover-content]")||null}function r(e,n){let t=e.getAttribute("data-tui-tagsinput-suggestions-id"),s=l(e),i=e.querySelector("[data-tui-tagsinput-text-input]");if(!t||!s||!i)return;s.style.setProperty("--trigger-width",`${i.getBoundingClientRect().width}px`);let u=p(e),o=n.toLowerCase().trim(),a=null;s.querySelectorAll("[data-tui-tagsinput-suggestion]").forEach(d=>{let v=d.getAttribute("data-tui-tagsinput-suggestion-value").toLowerCase(),m=!u.includes(v)&&v.includes(o);d.style.display=m?"":"none",d.classList.remove("bg-accent"),m&&!a&&(a=d)}),a?(a.classList.add("bg-accent"),window.tui?.popover?.open(t)):window.tui?.popover?.close(t)}function g(e){let n=l(e);return n?Array.from(n.querySelectorAll("[data-tui-tagsinput-suggestion]")).filter(t=>t.style.display!=="none"):[]}function f(e,n){let t=g(e);if(!t.length)return;let s=t.findIndex(u=>u.classList.contains("bg-accent"));t.forEach(u=>u.classList.remove("bg-accent"));let i=n==="down"?s+1:s-1;i>=t.length&&(i=0),i<0&&(i=t.length-1),t[i].classList.add("bg-accent"),t[i].scrollIntoView({block:"nearest"})}function c(e,n){let t=e.querySelector("[data-tui-tagsinput-text-input]"),s=n.trim();if(!s||t?.disabled)return;if(p(e).includes(s.toLowerCase())){t.value="";return}let i=document.createElement("div");i.className="inline-flex items-center gap-2 rounded-md border px-2.5 py-0.5 text-xs font-semibold border-transparent bg-primary text-primary-foreground",i.setAttribute("data-tui-tagsinput-chip",""),i.innerHTML=`<span>${s}</span><button type="button" class="ml-1 hover:text-destructive cursor-pointer" data-tui-tagsinput-remove><svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button>`,e.querySelector("[data-tui-tagsinput-chips]").appendChild(i);let u=document.createElement("input");u.type="hidden",u.name=e.getAttribute("data-tui-tagsinput-name")||"",u.value=s,e.querySelector("[data-tui-tagsinput-hidden-inputs]").appendChild(u),t.value=""}document.addEventListener("focusin",e=>{let n=e.target.closest("[data-tui-tagsinput-text-input]");if(!n)return;let t=n.closest("[data-tui-tagsinput]");t&&r(t,n.value)}),document.addEventListener("focusout",e=>{let n=e.target.closest("[data-tui-tagsinput-text-input]");if(!n)return;let t=n.closest("[data-tui-tagsinput]"),s=t?.getAttribute("data-tui-tagsinput-suggestions-id"),i=e.relatedTarget;t?.contains(i)||l(t)?.contains(i)||s&&window.tui?.popover?.close(s)}),document.addEventListener("input",e=>{let n=e.target.closest("[data-tui-tagsinput-text-input]");if(!n)return;let t=n.closest("[data-tui-tagsinput]");t&&r(t,n.value)}),document.addEventListener("mousedown",e=>{let n=e.target.closest("[data-tui-tagsinput-suggestion]");if(!n)return;e.preventDefault();let t=n.closest("[data-tui-popover-root]"),s=document.querySelector(`[data-tui-tagsinput-suggestions-id="${t?.id}"]`);s&&(c(s,n.getAttribute("data-tui-tagsinput-suggestion-value")),r(s,""))}),document.addEventListener("click",e=>{let n=e.target.closest("[data-tui-tagsinput-text-input]");if(n){let i=n.closest("[data-tui-tagsinput]");i&&r(i,n.value);return}let t=e.target.closest("[data-tui-tagsinput-remove]");if(t){let i=t.closest("[data-tui-tagsinput-chip]"),u=i?.closest("[data-tui-tagsinput]"),o=i?.querySelector("span")?.textContent;i?.remove(),u?.querySelector(`[data-tui-tagsinput-hidden-inputs] input[value="${o}"]`)?.remove();return}let s=e.target.closest("[data-tui-tagsinput]");s&&!e.target.closest("input")&&s.querySelector("[data-tui-tagsinput-text-input]")?.focus()}),document.addEventListener("keydown",e=>{let n=e.target.closest("[data-tui-tagsinput-text-input]");if(!n)return;let t=n.closest("[data-tui-tagsinput]");if(!t)return;let s=t.getAttribute("data-tui-tagsinput-suggestions-id"),i=s&&window.tui?.popover?.isOpen(s);if(e.key==="ArrowDown"&&i)e.preventDefault(),f(t,"down");else if(e.key==="ArrowUp"&&i)e.preventDefault(),f(t,"up");else if(e.key==="Enter"){e.preventDefault();let o=g(t).find(a=>a.classList.contains("bg-accent"));o?(c(t,o.getAttribute("data-tui-tagsinput-suggestion-value")),r(t,"")):(c(t,n.value),r(t,""))}else if(e.key===",")e.preventDefault(),c(t,n.value),r(t,"");else if(e.key==="Escape"&&i)e.preventDefault(),window.tui?.popover?.close(s);else if(e.key==="Backspace"&&n.value===""){let o=t.querySelector("[data-tui-tagsinput-chips]")?.lastElementChild;if(o){let a=o.querySelector("span")?.textContent;o.remove(),t.querySelector(`[data-tui-tagsinput-hidden-inputs] input[value="${a}"]`)?.remove()}}})})();})();

22
assets/js/textarea.js Normal file
View file

@ -0,0 +1,22 @@
(function () {
'use strict';
// Auto-resize handler
document.addEventListener('input', (e) => {
const textarea = e.target.closest('textarea[data-tui-textarea]');
if (!textarea || textarea.getAttribute('data-tui-textarea-auto-resize') !== 'true') return;
const minHeight = textarea.style.minHeight || window.getComputedStyle(textarea).minHeight;
textarea.style.height = minHeight;
textarea.style.height = `${textarea.scrollHeight}px`;
});
// MutationObserver for initial setup
new MutationObserver(() => {
document.querySelectorAll('textarea[data-tui-textarea][data-tui-textarea-auto-resize="true"]').forEach(textarea => {
if (!textarea.style.height || textarea.style.height === textarea.style.minHeight) {
textarea.style.height = `${textarea.scrollHeight}px`;
}
});
}).observe(document.body, { childList: true, subtree: true });
})();

379
assets/js/timepicker.js Normal file
View file

@ -0,0 +1,379 @@
(function () {
'use strict';
/**
* Reactive Binding for hidden inputs
*
* Problem: Setting input.value programmatically (e.g., via Datastar/Alpine)
* does NOT fire 'input' events - this is standard browser behavior since the 90s.
*
* Solution: Override the value setter to dispatch 'input' events on change.
* This is the same pattern used by Vue.js, MobX, and other reactive frameworks.
*/
function enableReactiveBinding(input) {
if (input._tui) return;
input._tui = true;
const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
if (!desc?.set) return;
Object.defineProperty(input, 'value', {
get: desc.get,
set(v) {
const old = this.value;
desc.set.call(this, v);
if (old !== v) {
this.dispatchEvent(new Event('input', { bubbles: true }));
}
},
configurable: true
});
}
// Utility functions
function parseTime(str) {
const match = str?.match(/^(\d{1,2}):(\d{2})$/);
if (!match) return null;
const [_, hour, minute] = match.map(Number);
return (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) ? { hour, minute } : null;
}
function formatTime(hour, minute, use12Hours) {
if (hour === null || minute === null) return null;
const pad = n => n.toString().padStart(2, '0');
if (use12Hours) {
const h = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
return `${pad(h)}:${pad(minute)} ${hour >= 12 ? 'PM' : 'AM'}`;
}
return `${pad(hour)}:${pad(minute)}`;
}
function isValidTime(hour, minute, minTime, maxTime) {
if (!minTime && !maxTime) return true;
const timeInMinutes = hour * 60 + minute;
if (minTime) {
const minInMinutes = minTime.hour * 60 + minTime.minute;
if (timeInMinutes < minInMinutes) return false;
}
if (maxTime) {
const maxInMinutes = maxTime.hour * 60 + maxTime.minute;
if (timeInMinutes > maxInMinutes) return false;
}
return true;
}
// DOM helpers
function findRoot(element) {
return element?.closest('[data-tui-timepicker-root]') || null;
}
function findTrigger(element) {
return findRoot(element)?.querySelector('[data-tui-timepicker="true"]') || null;
}
function getElements(trigger) {
const root = findRoot(trigger);
const popup = root?.querySelector('[data-tui-timepicker-popup]');
if (!popup) return null;
return {
root,
trigger,
popup,
hourList: popup.querySelector('[data-tui-timepicker-hour-list]'),
minuteList: popup.querySelector('[data-tui-timepicker-minute-list]'),
hiddenInput: root?.querySelector('[data-tui-timepicker-hidden-input]')
};
}
function closePopover(trigger) {
const root = findRoot(trigger);
const popoverContent = root?.querySelector('[data-tui-popover-content]');
if (!popoverContent?.matches(':popover-open')) return;
try {
popoverContent.hidePopover();
} catch {
// ignore
}
}
// State management
function getState(trigger) {
return {
hour: trigger.dataset.tuiTimepickerCurrentHour ? parseInt(trigger.dataset.tuiTimepickerCurrentHour) : null,
minute: trigger.dataset.tuiTimepickerCurrentMinute ? parseInt(trigger.dataset.tuiTimepickerCurrentMinute) : null,
use12Hours: trigger.getAttribute('data-tui-timepicker-use12hours') === 'true',
step: parseInt(trigger.getAttribute('data-tui-timepicker-step') || '1'),
minTime: parseTime(trigger.getAttribute('data-tui-timepicker-min-time')),
maxTime: parseTime(trigger.getAttribute('data-tui-timepicker-max-time')),
placeholder: trigger.getAttribute('data-tui-timepicker-placeholder') || 'Select time'
};
}
function setState(trigger, hour, minute) {
if (hour !== null) {
trigger.dataset.tuiTimepickerCurrentHour = hour;
} else {
delete trigger.dataset.tuiTimepickerCurrentHour;
}
if (minute !== null) {
trigger.dataset.tuiTimepickerCurrentMinute = minute;
} else {
delete trigger.dataset.tuiTimepickerCurrentMinute;
}
updateDisplay(trigger);
}
// Display updates
function updateDisplay(trigger) {
const state = getState(trigger);
const elements = getElements(trigger);
// Update trigger display
const display = trigger.querySelector('[data-tui-timepicker-display]');
if (display) {
const formatted = formatTime(state.hour, state.minute, state.use12Hours);
display.textContent = formatted || state.placeholder;
display.classList.toggle('text-muted-foreground', !formatted);
}
// Update hidden input
if (elements?.hiddenInput) {
elements.hiddenInput.value = (state.hour !== null && state.minute !== null) ?
formatTime(state.hour, state.minute, false) : '';
}
// Update selections if popup is visible
if (elements?.hourList && elements?.minuteList) {
updateSelections(elements, state);
}
}
function updateSelections(elements, state) {
// Update hour buttons
elements.hourList.querySelectorAll('[data-tui-timepicker-hour]').forEach(btn => {
const hour = parseInt(btn.getAttribute('data-tui-timepicker-hour'));
let isSelected = false;
if (state.hour !== null) {
if (state.use12Hours) {
isSelected = (hour === state.hour) ||
(hour === 0 && state.hour === 12) ||
(hour === state.hour - 12 && state.hour > 12);
} else {
isSelected = hour === state.hour;
}
}
btn.setAttribute('data-tui-timepicker-selected', isSelected);
// Check validity
let valid = false;
for (let m = 0; m < 60; m++) {
if (isValidTime(hour, m, state.minTime, state.maxTime)) {
valid = true;
break;
}
}
btn.disabled = !valid;
btn.classList.toggle('opacity-50', !valid);
btn.classList.toggle('cursor-not-allowed', !valid);
});
// Update minute buttons
elements.minuteList.querySelectorAll('[data-tui-timepicker-minute]').forEach(btn => {
const minute = parseInt(btn.getAttribute('data-tui-timepicker-minute'));
const isSelected = minute === state.minute;
const valid = state.hour === null || isValidTime(state.hour, minute, state.minTime, state.maxTime);
btn.setAttribute('data-tui-timepicker-selected', isSelected);
btn.disabled = !valid;
btn.classList.toggle('opacity-50', !valid);
btn.classList.toggle('cursor-not-allowed', !valid);
});
// Update AM/PM buttons
const amBtn = elements.popup.querySelector('[data-tui-timepicker-period="AM"]');
const pmBtn = elements.popup.querySelector('[data-tui-timepicker-period="PM"]');
if (amBtn && pmBtn) {
const isAM = state.hour === null || state.hour < 12;
amBtn.setAttribute('data-tui-timepicker-active', isAM);
pmBtn.setAttribute('data-tui-timepicker-active', !isAM);
}
}
// Event handlers
document.addEventListener('click', (e) => {
const target = e.target;
// Hour selection
if (target.matches('[data-tui-timepicker-hour]') && !target.disabled) {
const trigger = findTrigger(target);
if (!trigger) return;
const state = getState(trigger);
let hour = parseInt(target.getAttribute('data-tui-timepicker-hour'));
if (state.use12Hours) {
const isPM = state.hour !== null && state.hour >= 12;
hour = hour === 0 ? (isPM ? 12 : 0) : (isPM ? hour + 12 : hour);
}
if (!isValidTime(hour, state.minute, state.minTime, state.maxTime)) {
// Find first valid minute
for (let m = 0; m < 60; m += state.step) {
if (isValidTime(hour, m, state.minTime, state.maxTime)) {
setState(trigger, hour, m);
return;
}
}
} else {
setState(trigger, hour, state.minute);
}
return;
}
// Minute selection
if (target.matches('[data-tui-timepicker-minute]') && !target.disabled) {
const trigger = findTrigger(target);
if (!trigger) return;
const state = getState(trigger);
const minute = parseInt(target.getAttribute('data-tui-timepicker-minute'));
if (state.hour === null || isValidTime(state.hour, minute, state.minTime, state.maxTime)) {
setState(trigger, state.hour, minute);
}
return;
}
// AM/PM selection
if (target.matches('[data-tui-timepicker-period]')) {
const trigger = findTrigger(target);
if (!trigger) return;
const state = getState(trigger);
if (state.hour === null) return;
const period = target.getAttribute('data-tui-timepicker-period');
let newHour = state.hour;
if (period === 'AM' && state.hour >= 12) {
newHour = state.hour === 12 ? 0 : state.hour - 12;
} else if (period === 'PM' && state.hour < 12) {
newHour = state.hour === 0 ? 12 : state.hour + 12;
}
if (newHour !== state.hour) {
if (!isValidTime(newHour, state.minute, state.minTime, state.maxTime)) {
// Find first valid minute
for (let m = 0; m < 60; m += state.step) {
if (isValidTime(newHour, m, state.minTime, state.maxTime)) {
setState(trigger, newHour, m);
return;
}
}
} else {
setState(trigger, newHour, state.minute);
}
}
return;
}
// Done button
if (target.matches('[data-tui-timepicker-done]')) {
const trigger = findTrigger(target);
if (trigger) {
closePopover(trigger);
}
return;
}
});
// Handle hidden input value changes (for reactive frameworks)
document.addEventListener('input', (e) => {
if (!e.target.matches('[data-tui-timepicker-hidden-input]')) return;
const trigger = findTrigger(e.target);
if (trigger) {
const parsed = parseTime(e.target.value);
if (parsed) {
setState(trigger, parsed.hour, parsed.minute);
} else {
setState(trigger, null, null);
}
}
});
// Form reset
document.addEventListener('reset', (e) => {
if (!e.target.matches('form')) return;
e.target.querySelectorAll('[data-tui-timepicker-root]').forEach(root => {
const trigger = root.querySelector('[data-tui-timepicker="true"]');
if (!trigger) return;
setState(trigger, null, null);
const elements = getElements(trigger);
if (elements?.hiddenInput) {
elements.hiddenInput.value = '';
}
});
});
// Initialize timepickers
function initializeTimePickers() {
document.querySelectorAll('[data-tui-timepicker-root]').forEach(root => {
const trigger = root.querySelector('[data-tui-timepicker="true"]');
const hiddenInput = root.querySelector('[data-tui-timepicker-hidden-input]');
if (!trigger) return;
if (!hiddenInput || hiddenInput._tui) return;
// Read initial value from hidden input
const initialValue = hiddenInput.value;
if (initialValue) {
const parsed = parseTime(initialValue);
if (parsed) {
setState(trigger, parsed.hour, parsed.minute);
}
}
// Enable reactive binding for hidden input
enableReactiveBinding(hiddenInput);
updateDisplay(trigger);
});
}
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeTimePickers);
} else {
initializeTimePickers();
}
// MutationObserver for dynamically added elements
new MutationObserver(initializeTimePickers).observe(document.body, { childList: true, subtree: true });
// Scroll to selected values when timepicker popover opens
new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.target.getAttribute('data-tui-popover-open') !== 'true') continue;
const popup = m.target.querySelector('[data-tui-timepicker-popup]');
if (!popup) continue;
requestAnimationFrame(() => {
popup.querySelector('[data-tui-timepicker-hour-list] [data-tui-timepicker-selected="true"]')?.scrollIntoView({ block: 'center' });
popup.querySelector('[data-tui-timepicker-minute-list] [data-tui-timepicker-selected="true"]')?.scrollIntoView({ block: 'center' });
});
}
}).observe(document.body, { attributes: true, attributeFilter: ['data-tui-popover-open'], subtree: true });
})();

File diff suppressed because one or more lines are too long

128
assets/js/toast.js Normal file
View file

@ -0,0 +1,128 @@
(function () {
"use strict";
const toastTimers = new Map();
// Setup toast when it appears
function setupToast(toast) {
if (!toast || toastTimers.has(toast)) return;
const duration = parseInt(toast.dataset.tuiToastDuration || "3000");
const progress = toast.querySelector(".toast-progress");
// Initialize timer state
const state = {
timer: null,
startTime: Date.now(),
remaining: duration,
paused: false,
progressWidth: null,
};
toastTimers.set(toast, state);
// Animate progress bar if present
if (progress && duration > 0) {
progress.style.width = "100%";
void progress.offsetWidth;
progress.style.transition = `width ${duration}ms linear`;
progress.style.width = "0px";
}
// Auto-dismiss after duration
if (duration > 0) {
state.timer = setTimeout(() => dismissToast(toast), duration);
}
// Pause on hover
toast.addEventListener("mouseenter", () => {
const state = toastTimers.get(toast);
if (!state || state.paused) return;
// Clear the dismiss timer
clearTimeout(state.timer);
// Calculate remaining time
state.remaining = state.remaining - (Date.now() - state.startTime);
state.paused = true;
// Pause progress animation
if (progress) {
state.progressWidth = getComputedStyle(progress).width;
progress.style.transition = "none";
progress.style.width = state.progressWidth;
}
});
// Resume on mouse leave
toast.addEventListener("mouseleave", () => {
const state = toastTimers.get(toast);
if (!state || !state.paused || state.remaining <= 0) return;
// Resume timer with remaining time
state.startTime = Date.now();
state.paused = false;
state.timer = setTimeout(() => dismissToast(toast), state.remaining);
// Resume progress animation
if (progress) {
progress.style.width = state.progressWidth;
void progress.offsetWidth;
progress.style.transition = `width ${state.remaining}ms linear`;
progress.style.width = "0px";
}
});
}
// Dismiss toast with fade out
function dismissToast(toast) {
// Clean up timer state
toastTimers.delete(toast);
// Add transition for smooth fade out
toast.style.transition = "opacity 300ms, transform 300ms";
toast.style.opacity = "0";
toast.style.transform = "translateY(1rem)";
// Remove after animation
setTimeout(() => toast.remove(), 300);
}
// Handle dismiss button clicks
document.addEventListener("click", (e) => {
const dismissBtn = e.target.closest("[data-tui-toast-dismiss]");
if (dismissBtn) {
const toast = dismissBtn.closest("[data-tui-toast]");
if (toast) dismissToast(toast);
}
});
function initializeToasts(root) {
if (!root) return;
if (root.matches?.("[data-tui-toast]")) {
setupToast(root);
}
root.querySelectorAll?.("[data-tui-toast]").forEach(setupToast);
}
// Initialize pre-rendered toasts
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () =>
initializeToasts(document),
);
} else {
initializeToasts(document);
}
// Watch for new toasts
new MutationObserver((mutations) => {
mutations.forEach((m) => {
m.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
initializeToasts(node);
}
});
});
}).observe(document.body, { childList: true, subtree: true });
})();

View file

@ -1 +1 @@
(()=>{(function(){"use strict";let n=new Map;function o(e){let i=parseInt(e.dataset.tuiToastDuration||"3000"),t=e.querySelector(".toast-progress"),a={timer:null,startTime:Date.now(),remaining:i,paused:!1};n.set(e,a),t&&i>0&&(t.style.transitionDuration=i+"ms",requestAnimationFrame(()=>{requestAnimationFrame(()=>{t.style.transform="scaleX(0)"})})),i>0&&(a.timer=setTimeout(()=>r(e),i)),e.addEventListener("mouseenter",()=>{let s=n.get(e);if(!(!s||s.paused)&&(clearTimeout(s.timer),s.remaining=s.remaining-(Date.now()-s.startTime),s.paused=!0,t)){let m=getComputedStyle(t);t.style.transitionDuration="0ms",t.style.transform=m.transform}}),e.addEventListener("mouseleave",()=>{let s=n.get(e);!s||!s.paused||s.remaining<=0||(s.startTime=Date.now(),s.paused=!1,s.timer=setTimeout(()=>r(e),s.remaining),t&&(t.style.transitionDuration=s.remaining+"ms",requestAnimationFrame(()=>{requestAnimationFrame(()=>{t.style.transform="scaleX(0)"})})))})}function r(e){n.delete(e),e.style.transition="opacity 300ms, transform 300ms",e.style.opacity="0",e.style.transform="translateY(1rem)",setTimeout(()=>e.remove(),300)}document.addEventListener("click",e=>{let i=e.target.closest("[data-tui-toast-dismiss]");if(i){let t=i.closest("[data-tui-toast]");t&&r(t)}}),new MutationObserver(e=>{e.forEach(i=>{i.addedNodes.forEach(t=>{t.nodeType===1&&t.matches?.("[data-tui-toast]")&&o(t)})})}).observe(document.body,{childList:!0,subtree:!0})})();})();
(()=>{(function(){"use strict";let n=new Map;function d(e){if(!e||n.has(e))return;let s=parseInt(e.dataset.tuiToastDuration||"3000"),t=e.querySelector(".toast-progress"),o={timer:null,startTime:Date.now(),remaining:s,paused:!1,progressWidth:null};n.set(e,o),t&&s>0&&(t.style.width="100%",t.offsetWidth,t.style.transition=`width ${s}ms linear`,t.style.width="0px"),s>0&&(o.timer=setTimeout(()=>r(e),s)),e.addEventListener("mouseenter",()=>{let i=n.get(e);!i||i.paused||(clearTimeout(i.timer),i.remaining=i.remaining-(Date.now()-i.startTime),i.paused=!0,t&&(i.progressWidth=getComputedStyle(t).width,t.style.transition="none",t.style.width=i.progressWidth))}),e.addEventListener("mouseleave",()=>{let i=n.get(e);!i||!i.paused||i.remaining<=0||(i.startTime=Date.now(),i.paused=!1,i.timer=setTimeout(()=>r(e),i.remaining),t&&(t.style.width=i.progressWidth,t.offsetWidth,t.style.transition=`width ${i.remaining}ms linear`,t.style.width="0px"))})}function r(e){n.delete(e),e.style.transition="opacity 300ms, transform 300ms",e.style.opacity="0",e.style.transform="translateY(1rem)",setTimeout(()=>e.remove(),300)}document.addEventListener("click",e=>{let s=e.target.closest("[data-tui-toast-dismiss]");if(s){let t=s.closest("[data-tui-toast]");t&&r(t)}});function a(e){e&&(e.matches?.("[data-tui-toast]")&&d(e),e.querySelectorAll?.("[data-tui-toast]").forEach(d))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>a(document)):a(document),new MutationObserver(e=>{e.forEach(s=>{s.addedNodes.forEach(t=>{t.nodeType===1&&a(t)})})}).observe(document.body,{childList:!0,subtree:!0})})();})();

View file

@ -1,20 +1,20 @@
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json",
"packages": [
"go@1.25.5",
"templ@0.3.960",
"go-task@3.45.5",
"tailwindcss_4@4.2.1",
"nodejs@24",
"goose@latest"
],
"shell": {
"init_hook": [
"echo 'Welcome to devbox!' > /dev/null",
"unset DEVELOPER_DIR"
],
"scripts": {
"setup": ["go install github.com/mfridman/tparse@latest"]
}
}
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json",
"packages": [
"go@1.25.5",
"go-task@3.45.5",
"tailwindcss_4@4.2.1",
"nodejs@24",
"goose@latest",
"templ@0.3.1001"
],
"shell": {
"init_hook": [
"echo 'Welcome to devbox!' > /dev/null",
"unset DEVELOPER_DIR"
],
"scripts": {
"setup": ["go install github.com/mfridman/tparse@latest"]
}
}
}

View file

@ -278,51 +278,51 @@
}
}
},
"templ@0.3.960": {
"last_modified": "2025-12-31T03:27:36Z",
"resolved": "github:NixOS/nixpkgs/f665af0cdb70ed27e1bd8f9fdfecaf451260fc55#templ",
"templ@0.3.1001": {
"last_modified": "2026-03-21T07:29:51Z",
"resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#templ",
"source": "devbox-search",
"version": "0.3.960",
"version": "0.3.1001",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/g2b8z0ssdj1qxfkcvw4h47rzhdr91wwf-templ-0.3.960",
"path": "/nix/store/x9y3fnv2mb8j70r8k0i3n96qflrqq8d5-templ-0.3.1001",
"default": true
}
],
"store_path": "/nix/store/g2b8z0ssdj1qxfkcvw4h47rzhdr91wwf-templ-0.3.960"
"store_path": "/nix/store/x9y3fnv2mb8j70r8k0i3n96qflrqq8d5-templ-0.3.1001"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/ws4nkq75id3wp1i741zzjqs6casbd0cw-templ-0.3.960",
"path": "/nix/store/3nhjivm3l1bk9gc98qrmafdk5n18lrdd-templ-0.3.1001",
"default": true
}
],
"store_path": "/nix/store/ws4nkq75id3wp1i741zzjqs6casbd0cw-templ-0.3.960"
"store_path": "/nix/store/3nhjivm3l1bk9gc98qrmafdk5n18lrdd-templ-0.3.1001"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/a1a7qklai2fjn6ika5a8q25mapi5xz77-templ-0.3.960",
"path": "/nix/store/7wjjyfwhkhavm8xyba699w3r1rhrkyci-templ-0.3.1001",
"default": true
}
],
"store_path": "/nix/store/a1a7qklai2fjn6ika5a8q25mapi5xz77-templ-0.3.960"
"store_path": "/nix/store/7wjjyfwhkhavm8xyba699w3r1rhrkyci-templ-0.3.1001"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/033s4gq6nqywzxhzd8gr9l05xcvln4ld-templ-0.3.960",
"path": "/nix/store/v4qj3mml7lyps2nicavbys97w56dxvjy-templ-0.3.1001",
"default": true
}
],
"store_path": "/nix/store/033s4gq6nqywzxhzd8gr9l05xcvln4ld-templ-0.3.960"
"store_path": "/nix/store/v4qj3mml7lyps2nicavbys97w56dxvjy-templ-0.3.1001"
}
}
}

4
go.mod
View file

@ -4,7 +4,7 @@ go 1.25.1
require (
github.com/Oudwins/tailwind-merge-go v0.2.1
github.com/a-h/templ v0.3.960
github.com/a-h/templ v0.3.1001
github.com/alexedwards/argon2id v1.0.0
github.com/emersion/go-imap v1.2.1
github.com/golang-jwt/jwt/v5 v5.2.2
@ -62,7 +62,7 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/templui/templui v1.0.0 // indirect
github.com/templui/templui v1.9.5 // indirect
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect
github.com/vertica/vertica-sql-go v1.3.3 // indirect
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 // indirect

12
go.sum
View file

@ -23,8 +23,8 @@ github.com/Oudwins/tailwind-merge-go v0.2.1 h1:jxRaEqGtwwwF48UuFIQ8g8XT7YSualNuG
github.com/Oudwins/tailwind-merge-go v0.2.1/go.mod h1:kkZodgOPvZQ8f7SIrlWkG/w1g9JTbtnptnePIh3V72U=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY=
github.com/a-h/templ v0.3.1001/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
@ -148,8 +148,8 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -215,8 +215,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/templui/templui v1.0.0 h1:nsCh+tTL8U9rhh0hpwkvDpiDCPP43aoBB85TLgCh/Kg=
github.com/templui/templui v1.0.0/go.mod h1:SnKmOIs7t/ngsdWUws97CVodbz89ne9kQv3ivgdhiHo=
github.com/templui/templui v1.9.5 h1:qJyELi+xHCVYNgheAGkRP9EKjsufJgfpQOD+Cb4+Y+M=
github.com/templui/templui v1.9.5/go.mod h1:WWX9O4UebQiSipKaoUQ7Cb0UWtqopzZHtgBu1gtItzU=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=

View file

@ -25,6 +25,6 @@ templ ThemeSwitcher(props ...ThemeSwitcherProps) {
"aria-label": "Toggle theme",
},
}) {
@icon.Eclipse(icon.Props{Size: 20})
@icon.Eclipse(icon.Props{Class: ""})
}
}

View file

@ -1,4 +1,4 @@
// templui component accordion - version: v1.2.0 installed by templui v1.2.0
// templui component accordion - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/accordion
package accordion
@ -97,10 +97,7 @@ templ Trigger(props ...TriggerProps) {
{ p.Attributes... }
>
{ children... }
@icon.ChevronDown(icon.Props{
Size: 16,
Class: "size-4 shrink-0 translate-y-0.5 transition-transform duration-200 text-muted-foreground pointer-events-none",
})
@icon.ChevronDown(icon.Props{Class: "size-4 shrink-0 translate-y-0.5 transition-transform duration-200 text-muted-foreground pointer-events-none"})
</summary>
}

View file

@ -1,4 +1,4 @@
// templui component alert - version: v1.2.0 installed by templui v1.2.0
// templui component alert - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/alert
package alert

View file

@ -1,4 +1,4 @@
// templui component aspectratio - version: v1.2.0 installed by templui v1.2.0
// templui component aspectratio - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/aspect-ratio
package aspectratio

View file

@ -1,4 +1,4 @@
// templui component avatar - version: v1.2.0 installed by templui v1.2.0
// templui component avatar - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/avatar
package avatar
@ -92,6 +92,10 @@ templ Fallback(props ...FallbackProps) {
</span>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/avatar.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("avatar")
}
}

View file

@ -1,4 +1,4 @@
// templui component badge - version: v1.2.0 installed by templui v1.2.0
// templui component badge - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/badge
package badge

View file

@ -1,4 +1,4 @@
// templui component breadcrumb - version: v1.2.0 installed by templui v1.2.0
// templui component breadcrumb - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/breadcrumb
package breadcrumb
@ -148,7 +148,7 @@ templ Separator(props ...SeparatorProps) {
if p.UseCustom {
{ children... }
} else {
@icon.ChevronRight(icon.Props{Size: 14, Class: "text-muted-foreground"})
@icon.ChevronRight(icon.Props{Class: "size-3.5 text-muted-foreground"})
}
</span>
}

View file

@ -1,4 +1,4 @@
// templui component button - version: v1.2.0 installed by templui v1.2.0
// templui component button - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/button
package button

View file

@ -1,4 +1,4 @@
// templui component calendar - version: v1.2.0 installed by templui v1.2.0
// templui component calendar - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/calendar
package calendar
@ -35,15 +35,15 @@ var (
)
type Props struct {
ID string
Class string
LocaleTag LocaleTag
Value *time.Time
Name string
InitialMonth int // Optional: 0-11 (Default: current or from Value). Controls the initially displayed month view.
InitialYear int // Optional: (Default: current or from Value). Controls the initially displayed year view.
StartOfWeek *Day // Optional: 0-6 [Sun-Sat] (Default: 1).
RenderHiddenInput bool // Optional: Whether to render the hidden input (Default: true). Set to false when used inside DatePicker.
ID string
Class string
LocaleTag LocaleTag
Value *time.Time
Name string
InitialMonth int // Optional: 0-11 (Default: current or from Value). Controls the initially displayed month view.
InitialYear int // Optional: (Default: current or from Value). Controls the initially displayed year view.
StartOfWeek *Day // Optional: 0-6 [Sun-Sat] (Default: 1).
HideHiddenInput bool // Optional: Hide the hidden input when a parent component owns form submission.
}
templ Calendar(props ...Props) {
@ -62,13 +62,6 @@ templ Calendar(props ...Props) {
if p.LocaleTag == "" {
p.LocaleTag = LocaleDefaultTag
}
// Default to rendering hidden input unless explicitly set to false
if p.RenderHiddenInput == false && len(props) > 0 {
// Only respect false if it was explicitly passed
p.RenderHiddenInput = props[0].RenderHiddenInput
} else {
p.RenderHiddenInput = true
}
initialStartOfWeek := Monday
if p.StartOfWeek != nil {
@ -106,8 +99,12 @@ templ Calendar(props ...Props) {
// Generate short month names (English only, JS will update with localized)
monthNames := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
}}
<div class={ p.Class } id={ p.ID + "-wrapper" } data-tui-calendar-wrapper="true">
if p.RenderHiddenInput {
<div
class={ utils.TwMerge("inline-flex flex-col [--cell-size:2rem]", p.Class) }
id={ p.ID + "-wrapper" }
data-tui-calendar-wrapper="true"
>
if !p.HideHiddenInput {
<input
type="hidden"
name={ p.Name }
@ -118,6 +115,7 @@ templ Calendar(props ...Props) {
}
<div
id={ p.ID }
class="inline-flex flex-col"
data-tui-calendar-container="true"
data-tui-calendar-locale-tag={ string(p.LocaleTag) }
data-tui-calendar-initial-month={ strconv.Itoa(initialMonth) }
@ -126,7 +124,7 @@ templ Calendar(props ...Props) {
data-tui-calendar-start-of-week={ int(initialStartOfWeek) }
>
<!-- Calendar Header -->
<div class="flex items-center gap-2 mb-4">
<div class="flex w-full items-center gap-2 mb-4">
<button
type="button"
data-tui-calendar-prev
@ -152,7 +150,7 @@ templ Calendar(props ...Props) {
</select>
<span class="select-none font-medium rounded-md px-2 flex items-center justify-center gap-1 text-sm h-7 pointer-events-none" aria-hidden="true">
<span id={ p.ID + "-month-value" }>{ monthNames[currentMonth] }</span>
@icon.ChevronDown(icon.Props{Size: 14, Class: "text-muted-foreground"})
@icon.ChevronDown(icon.Props{Class: "size-3.5 text-muted-foreground"})
</span>
</div>
<!-- Year Select -->
@ -172,7 +170,7 @@ templ Calendar(props ...Props) {
</select>
<span class="select-none font-medium rounded-md px-2 flex items-center justify-center gap-1 text-sm h-7 pointer-events-none" aria-hidden="true">
<span id={ p.ID + "-year-value" }>{ strconv.Itoa(currentYear) }</span>
@icon.ChevronDown(icon.Props{Size: 14, Class: "text-muted-foreground"})
@icon.ChevronDown(icon.Props{Class: "size-3.5 text-muted-foreground"})
</span>
</div>
</div>
@ -185,13 +183,17 @@ templ Calendar(props ...Props) {
</button>
</div>
<!-- Weekday Headers -->
<div data-tui-calendar-weekdays class="grid grid-cols-7 gap-1 mb-1 place-items-center"></div>
<div data-tui-calendar-weekdays class="inline-grid grid-cols-[repeat(7,var(--cell-size))] gap-1 mb-1 place-items-center"></div>
<!-- Calendar Day Grid -->
<div data-tui-calendar-days class="grid grid-cols-7 gap-1 place-items-center"></div>
<div data-tui-calendar-days class="inline-grid grid-cols-[repeat(7,var(--cell-size))] gap-1 place-items-center"></div>
</div>
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/calendar.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("calendar")
}
}

View file

@ -1,4 +1,4 @@
// templui component card - version: v1.2.0 installed by templui v1.2.0
// templui component card - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/card
package card

View file

@ -1,4 +1,4 @@
// templui component carousel - version: v1.2.0 installed by templui v1.2.0
// templui component carousel - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/carousel
package carousel
@ -206,6 +206,10 @@ templ Indicators(props ...IndicatorsProps) {
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/carousel.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("carousel")
}
}

View file

@ -1,4 +1,4 @@
// templui component chart - version: v1.2.0 installed by templui v1.2.0
// templui component chart - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/charts
package chart
@ -53,11 +53,17 @@ type Config struct {
BeginAtZero *bool `json:"beginAtZero,omitempty"`
}
type ScriptConfig struct {
RawConfig map[string]any `json:"rawConfig,omitempty"`
GeneratedConfig *Config `json:"generatedConfig,omitempty"`
}
type Props struct {
ID string
Variant Variant
Data Data
Options Options
RawConfig map[string]any
ShowLegend bool
ShowXAxis bool
ShowYAxis bool
@ -96,27 +102,37 @@ templ Chart(props ...Props) {
<canvas id={ canvasId } data-tui-chart-id={ dataId }></canvas>
</div>
{{
chartConfig := Config{
Type: p.Variant,
Data: p.Data,
Options: p.Options,
ShowLegend: p.ShowLegend,
ShowXAxis: p.ShowXAxis,
ShowYAxis: p.ShowYAxis,
ShowXLabels: p.ShowXLabels,
ShowYLabels: p.ShowYLabels,
ShowXGrid: p.ShowXGrid,
ShowYGrid: p.ShowYGrid,
Horizontal: p.Horizontal,
Stacked: p.Stacked,
YMin: p.YMin,
YMax: p.YMax,
BeginAtZero: p.BeginAtZero,
scriptConfig := ScriptConfig{
RawConfig: p.RawConfig,
}
if p.RawConfig == nil {
generatedConfig := Config{
Type: p.Variant,
Data: p.Data,
Options: p.Options,
ShowLegend: p.ShowLegend,
ShowXAxis: p.ShowXAxis,
ShowYAxis: p.ShowYAxis,
ShowXLabels: p.ShowXLabels,
ShowYLabels: p.ShowYLabels,
ShowXGrid: p.ShowXGrid,
ShowYGrid: p.ShowYGrid,
Horizontal: p.Horizontal,
Stacked: p.Stacked,
YMin: p.YMin,
YMax: p.YMax,
BeginAtZero: p.BeginAtZero,
}
scriptConfig.GeneratedConfig = &generatedConfig
}
}}
@templ.JSONScript(dataId, chartConfig)
@templ.JSONScript(dataId, scriptConfig)
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/chart.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("chart")
}
}

View file

@ -1,4 +1,4 @@
// templui component checkbox - version: v1.2.0 installed by templui v1.2.0
// templui component checkbox - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/checkbox
package checkbox
@ -71,17 +71,21 @@ templ Checkbox(props ...Props) {
if p.Icon != nil {
@p.Icon
} else {
@icon.Check(icon.Props{Size: 14})
@icon.Check(icon.Props{Class: "size-3.5"})
}
</div>
<div
class="absolute inset-0 pointer-events-none flex items-center justify-center text-primary-foreground opacity-0 peer-indeterminate:opacity-100"
>
@icon.Minus(icon.Props{Size: 14})
@icon.Minus(icon.Props{Class: "size-3.5"})
</div>
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/checkbox.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("checkbox")
}
}

View file

@ -1,4 +1,4 @@
// templui component collapsible - version: v1.2.0 installed by templui v1.2.0
// templui component collapsible - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/collapsible
package collapsible
@ -81,6 +81,10 @@ templ Content(props ...ContentProps) {
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/collapsible.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("collapsible")
}
}

View file

@ -1,4 +1,4 @@
// templui component copybutton - version: v1.2.0 installed by templui v1.2.0
// templui component copybutton - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/copy-button
package copybutton
@ -34,15 +34,19 @@ templ CopyButton(props Props) {
Type: button.TypeButton,
}) {
<span data-copy-icon-clipboard>
@icon.Clipboard(icon.Props{Size: 16})
@icon.Clipboard(icon.Props{Class: "size-4"})
</span>
<span data-copy-icon-check class="hidden">
@icon.Check(icon.Props{Size: 16})
@icon.Check(icon.Props{Class: "size-4"})
</span>
}
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/copybutton.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("copybutton")
}
}

View file

@ -1,4 +1,4 @@
// templui component datepicker - version: v1.2.0 installed by templui v1.2.0
// templui component datepicker - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/date-picker
package datepicker
@ -47,8 +47,6 @@ type Props struct {
Placeholder string
Disabled bool
HasError bool
Required bool
Clearable bool
}
templ DatePicker(props ...Props) {
@ -73,99 +71,79 @@ templ DatePicker(props ...Props) {
p.Format = FormatLOCALE_MEDIUM
}
var contentID = p.ID + "-content"
var valuePtr *time.Time
var initialSelectedISO string
if !p.Value.IsZero() {
valuePtr = &p.Value
initialSelectedISO = p.Value.Format("2006-01-02")
}
var required = "false"
if p.Required {
required = "true"
}
}}
<div class="relative inline-block w-full">
<input
type="hidden"
name={ p.Name }
value={ initialSelectedISO }
if p.Form != "" {
form={ p.Form }
}
id={ p.ID + "-hidden" }
data-tui-datepicker-hidden-input
/>
@popover.Trigger(popover.TriggerProps{For: contentID}) {
@button.Button(button.Props{
ID: p.ID,
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(p.Attributes, templ.Attributes{
"data-tui-datepicker": "true",
"data-tui-datepicker-display-format": string(p.Format),
"data-tui-datepicker-locale-tag": string(p.LocaleTag),
"data-tui-datepicker-placeholder": p.Placeholder,
"data-tui-datepicker-required": required,
"aria-invalid": utils.If(p.HasError, "true"),
}),
}) {
if p.Placeholder != "" {
<span data-tui-datepicker-display class={ "text-left grow text-muted-foreground" }>
{ p.Placeholder }
<div class="relative inline-block w-full" data-tui-datepicker-root>
@popover.Root() {
<input
type="hidden"
name={ p.Name }
value={ initialSelectedISO }
if p.Form != "" {
form={ p.Form }
}
data-tui-datepicker-hidden-input
/>
@popover.Trigger() {
@button.Button(button.Props{
ID: p.ID,
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(p.Attributes, templ.Attributes{
"data-tui-datepicker": "true",
"data-tui-datepicker-display-format": string(p.Format),
"data-tui-datepicker-locale-tag": string(p.LocaleTag),
"data-tui-datepicker-placeholder": p.Placeholder,
"aria-invalid": utils.If(p.HasError, "true"),
}),
}) {
if p.Placeholder != "" {
<span data-tui-datepicker-display class={ "text-left grow text-muted-foreground" }>
{ p.Placeholder }
</span>
}
<span class="text-muted-foreground flex items-center ml-2">
@icon.Calendar(icon.Props{Class: "size-4"})
</span>
}
<span class="text-muted-foreground flex items-center ml-2">
@icon.Calendar(icon.Props{Size: 16})
</span>
}
}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: popover.PlacementBottomStart,
Class: "p-0",
}) {
@card.Card(card.Props{
Class: "border-0 shadow-none",
@popover.Content(popover.ContentProps{
Placement: popover.PlacementBottomStart,
Class: "p-0",
}) {
@card.Content(card.ContentProps{
Class: "p-3",
@card.Card(card.Props{
Class: "border-0 shadow-none",
}) {
@calendar.Calendar(calendar.Props{
ID: p.ID + "-calendar-instance", // Pass ID for calendar instance
LocaleTag: calendar.LocaleTag(p.LocaleTag), // Pass locale tag to calendar
StartOfWeek: p.StartOfWeek, // Pass start of week to calendar
Value: valuePtr, // Pass pointer to value
RenderHiddenInput: false, // Don't render hidden input inside popover
})
if p.Clearable {
@button.Button(button.Props{
ID: p.ID + "-clear-button",
Class: "mt-4 w-full",
Variant: button.VariantOutline,
Attributes: templ.Attributes{
"data-tui-datepicker-clear": "true",
},
}) {
Clear
}
@card.Content(card.ContentProps{
Class: "p-3",
}) {
@calendar.Calendar(calendar.Props{
LocaleTag: calendar.LocaleTag(p.LocaleTag),
StartOfWeek: p.StartOfWeek,
Value: valuePtr,
HideHiddenInput: true,
})
}
}
}
@ -173,6 +151,12 @@ templ DatePicker(props ...Props) {
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/datepicker.js") }></script>
@scriptOnce.Once() {
@calendar.Script()
@popover.Script()
@utils.ComponentScript("datepicker")
}
}

View file

@ -1,4 +1,4 @@
// templui component dialog - version: v1.2.0 installed by templui v1.2.0
// templui component dialog - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/dialog
package dialog
@ -11,8 +11,7 @@ import (
type contextKey string
const (
instanceKey contextKey = "dialogInstance"
openKey contextKey = "dialogOpen"
openKey contextKey = "dialogOpen"
)
type Props struct {
@ -28,23 +27,20 @@ type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string // Reference to a specific dialog ID (for external triggers)
For string // Dialog root ID for external triggers
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
HideCloseButton bool
Open bool // Initial open state for standalone usage (when no context)
DisableAutoFocus bool
Class string
Attributes templ.Attributes
HideCloseButton bool
}
type CloseProps struct {
ID string
Class string
Attributes templ.Attributes
For string // ID of the dialog to close (optional, defaults to closest dialog)
For string // Dialog root ID to close (optional, defaults to closest dialog)
}
type HeaderProps struct {
@ -76,25 +72,20 @@ templ Dialog(props ...Props) {
if len(props) > 0 {
{{ p = props[0] }}
}
{{ instanceID := p.ID }}
if instanceID == "" {
{{ instanceID = utils.RandomID() }}
}
{{ ctx = context.WithValue(ctx, instanceKey, instanceID) }}
{{ ctx = context.WithValue(ctx, openKey, p.Open) }}
<div
if p.ID != "" {
id={ p.ID }
}
data-tui-dialog
data-dialog-instance={ instanceID }
if p.DisableClickAway {
data-tui-dialog-disable-click-away="true"
}
if p.DisableESC {
data-tui-dialog-disable-esc="true"
}
class={ utils.TwMerge("", p.Class) }
data-tui-dialog-open={ utils.IfElse(p.Open, "true", "false") }
class={ utils.TwMerge("contents", p.Class) }
{ p.Attributes... }
>
{ children... }
@ -106,19 +97,14 @@ templ Trigger(props ...TriggerProps) {
if len(props) > 0 {
{{ p = props[0] }}
}
{{ instanceID := "" }}
// Explicit For prop takes priority over inherited context
if p.For != "" {
{{ instanceID = p.For }}
} else if val := ctx.Value(instanceKey); val != nil {
{{ instanceID = val.(string) }}
}
<span
if p.ID != "" {
id={ p.ID }
}
data-tui-dialog-trigger={ instanceID }
data-dialog-instance={ instanceID }
data-tui-dialog-trigger
if p.For != "" {
data-tui-dialog-target={ p.For }
}
data-tui-dialog-trigger-open="false"
class={ utils.TwMerge("contents", p.Class) }
{ p.Attributes... }
@ -132,114 +118,67 @@ templ Content(props ...ContentProps) {
if len(props) > 0 {
{{ p = props[0] }}
}
// Start with prop values as defaults
{{ instanceID := p.ID }}
{{ open := p.Open }}
// Override with context values if available
if val := ctx.Value(instanceKey); val != nil {
{{ instanceID = val.(string) }}
}
{{ open := false }}
if val := ctx.Value(openKey); val != nil {
{{ open = val.(bool) }}
}
// Apply defaults if still empty
if instanceID == "" {
{{ instanceID = utils.RandomID() }}
}
<!-- Overlay -->
<div
class={ utils.TwMerge(
"fixed inset-0 z-50 bg-black/50",
"transition-opacity duration-300",
"data-[tui-dialog-open=false]:opacity-0",
"data-[tui-dialog-open=true]:opacity-100",
"data-[tui-dialog-open=false]:pointer-events-none",
"data-[tui-dialog-open=true]:pointer-events-auto",
"data-[tui-dialog-hidden=true]:!hidden",
) }
data-tui-dialog-backdrop
data-dialog-instance={ instanceID }
if open {
data-tui-dialog-open="true"
} else {
data-tui-dialog-open="false"
data-tui-dialog-hidden="true"
}
></div>
<!-- Content -->
<div
<dialog
class={
utils.TwMerge(
// Base positioning
"fixed z-50 left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]",
// Style
"bg-background rounded-lg border shadow-lg",
// Layout
"grid gap-4 p-6",
// Size
"w-full max-w-[calc(100%-2rem)] sm:max-w-lg",
// Transitions
"fixed left-[50%] top-[50%] z-50 m-0 w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-hidden border bg-background text-foreground p-0 shadow-lg outline-none sm:max-w-lg",
"[&:not([open]):not([data-tui-dialog-closing=true])]:hidden",
"rounded-lg",
"[&::backdrop]:transition-all [&::backdrop]:duration-200",
"data-[tui-dialog-open=false]:[&::backdrop]:bg-black/0",
"data-[tui-dialog-open=true]:[&::backdrop]:bg-black/50",
"transition-all duration-200",
// Scale animation
"data-[tui-dialog-open=false]:scale-95",
"data-[tui-dialog-open=true]:scale-100",
// Opacity
"data-[tui-dialog-open=false]:opacity-0",
"data-[tui-dialog-open=true]:opacity-100",
// Pointer events
"data-[tui-dialog-open=false]:pointer-events-none",
"data-[tui-dialog-open=true]:pointer-events-auto",
// Hidden state
"data-[tui-dialog-hidden=true]:!hidden",
p.Class,
),
}
data-tui-dialog-content
data-dialog-instance={ instanceID }
if p.DisableAutoFocus {
data-tui-dialog-disable-autofocus="true"
}
if open {
data-tui-dialog-open="true"
} else {
data-tui-dialog-open="false"
data-tui-dialog-hidden="true"
}
data-tui-dialog-open={ utils.IfElse(open, "true", "false") }
data-tui-dialog-initial-open={ utils.IfElse(open, "true", "false") }
{ p.Attributes... }
>
{ children... }
if !p.HideCloseButton {
<button
class={ utils.TwMerge(
// Positioning
"absolute top-4 right-4",
// Style
"rounded-xs opacity-70",
// Interactions
"transition-opacity hover:opacity-100",
// Focus states
"focus:outline-none focus:ring-2",
"focus:ring-ring focus:ring-offset-2",
"ring-offset-background",
// Hover/Data states
"data-[tui-dialog-open=true]:bg-accent",
"data-[tui-dialog-open=true]:text-muted-foreground",
// Disabled state
"disabled:pointer-events-none",
// Icon styles
"[&_svg]:pointer-events-none",
<div class="relative grid gap-4 p-6" data-tui-dialog-panel>
{ children... }
if !p.HideCloseButton {
<button
class={ utils.TwMerge(
// Positioning
"absolute top-4 right-4",
// Style
"rounded-xs opacity-70",
// Interactions
"transition-opacity hover:opacity-100",
// Focus states
"focus:outline-none focus:ring-2",
"focus:ring-ring focus:ring-offset-2",
"ring-offset-background",
// Hover/Data states
"data-[tui-dialog-open=true]:bg-accent",
"data-[tui-dialog-open=true]:text-muted-foreground",
// Disabled state
"disabled:pointer-events-none",
// Icon styles
"[&_svg]:pointer-events-none",
"[&_svg]:shrink-0",
"[&_svg:not([class*='size-'])]:size-4",
) }
data-tui-dialog-close={ instanceID }
aria-label="Close"
type="button"
>
@icon.X()
<span class="sr-only">Close</span>
</button>
}
</div>
data-tui-dialog-close
aria-label="Close"
type="button"
>
@icon.X()
<span class="sr-only">Close</span>
</button>
}
</div>
</dialog>
}
templ Close(props ...CloseProps) {
@ -251,10 +190,9 @@ templ Close(props ...CloseProps) {
if p.ID != "" {
id={ p.ID }
}
data-tui-dialog-close
if p.For != "" {
data-tui-dialog-close={ p.For }
} else {
data-tui-dialog-close
data-tui-dialog-target={ p.For }
}
class={ utils.TwMerge("contents cursor-pointer", p.Class) }
{ p.Attributes... }
@ -327,6 +265,10 @@ templ Description(props ...DescriptionProps) {
</p>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/dialog.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("dialog")
}
}

View file

@ -1,9 +1,8 @@
// templui component dropdown - version: v1.2.0 installed by templui v1.2.0
// templui component dropdown - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/dropdown
package dropdown
import (
"context"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/popover"
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
)
@ -25,25 +24,16 @@ const (
PlacementLeftEnd = popover.PlacementLeftEnd
)
type contextKey string
var (
contentIDKey contextKey = "contentID"
subContentIDKey contextKey = "subContentID"
)
type Props struct {
ID string
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
Placement Placement
@ -90,13 +80,11 @@ type SubProps struct {
}
type SubTriggerProps struct {
ID string
Class string
Attributes templ.Attributes
}
type SubContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
@ -108,36 +96,25 @@ type PortalProps struct {
}
templ Dropdown(props ...Props) {
{{
var p Props
if len(props) > 0 {
p = props[0]
}
contentID := p.ID
if contentID == "" {
contentID = utils.RandomID()
}
ctx = context.WithValue(ctx, contentIDKey, contentID)
}}
{ children... }
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Root(popover.RootProps{
ID: p.ID,
}) {
{ children... }
}
}
templ Trigger(props ...TriggerProps) {
{{
var p TriggerProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-content-id"
}
}}
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Trigger(popover.TriggerProps{
ID: p.ID,
Class: p.Class,
Attributes: p.Attributes,
For: contentID,
TriggerType: popover.TriggerTypeClick,
}) {
{ children... }
@ -149,10 +126,6 @@ templ Content(props ...ContentProps) {
if len(props) > 0 {
{{ p = props[0] }}
}
{{ contentID, ok := ctx.Value(contentIDKey).(string) }}
if !ok {
{{ contentID = "fallback-content-id" }} // Must match fallback in Trigger
}
{{
placement := p.Placement
if placement == "" {
@ -160,7 +133,6 @@ templ Content(props ...ContentProps) {
}
}}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: placement,
Class: utils.TwMerge(
"z-50 rounded-md bg-popover p-1 shadow-md focus:outline-none overflow-auto",
@ -175,6 +147,15 @@ templ Content(props ...ContentProps) {
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
@scriptOnce.Once() {
@popover.Script()
@utils.ComponentScript("dropdown")
}
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
@ -298,43 +279,25 @@ templ Shortcut(props ...ShortcutProps) {
}
templ Sub(props ...SubProps) {
{{
var p SubProps
if len(props) > 0 {
p = props[0]
}
subContentID := p.ID
if subContentID == "" {
subContentID = utils.RandomID()
}
ctx = context.WithValue(ctx, subContentIDKey, subContentID)
}}
<div
if p.ID != "" {
id={ p.ID }
}
data-tui-dropdown-submenu
class={ utils.TwMerge("relative", p.Class) }
{ p.Attributes... }
>
{{ var p SubProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Root(popover.RootProps{
ID: p.ID,
Class: p.Class,
Attributes: p.Attributes,
}) {
{ children... }
</div>
}
}
templ SubTrigger(props ...SubTriggerProps) {
{{
var p SubTriggerProps
if len(props) > 0 {
p = props[0]
}
subContentID, ok := ctx.Value(subContentIDKey).(string)
if !ok {
subContentID = "fallback-subcontent-id"
}
}}
{{ var p SubTriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Trigger(popover.TriggerProps{
ID: p.ID,
For: subContentID,
TriggerType: popover.TriggerTypeHover,
}) {
<button
@ -360,18 +323,11 @@ templ SubTrigger(props ...SubTriggerProps) {
}
templ SubContent(props ...SubContentProps) {
{{
var p SubContentProps
if len(props) > 0 {
p = props[0]
}
subContentID, ok := ctx.Value(subContentIDKey).(string)
if !ok {
subContentID = "fallback-subcontent-id"
}
}}
{{ var p SubContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Content(popover.ContentProps{
ID: subContentID,
Placement: popover.PlacementRightStart,
Offset: -4, // Adjust as needed
HoverDelay: 100, // ms
@ -385,7 +341,3 @@ templ SubContent(props ...SubContentProps) {
{ children... }
}
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/dropdown.min.js") }></script>
}

View file

@ -1,4 +1,4 @@
// templui component form - version: v1.2.0 installed by templui v1.2.0
// templui component form - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/form
package form

View file

@ -1,4 +1,4 @@
// templui component icon - version: v1.2.0 installed by templui v1.2.0
// templui component icon - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/icon
package icon
@ -20,12 +20,7 @@ var (
// Props defines the properties that can be set for an icon.
type Props struct {
Size int
Color string
Fill string
Stroke string
StrokeWidth string // Stroke Width of Icon, Usage: "2.5"
Class string
Class string
}
// Icon returns a function that generates a templ.Component for the specified icon name.
@ -36,10 +31,8 @@ func Icon(name string) func(...Props) templ.Component {
p = props[0]
}
// Create a unique key for the cache based on icon name and all relevant props.
// This ensures different stylings of the same icon are cached separately.
cacheKey := fmt.Sprintf("%s|s:%d|c:%s|f:%s|sk:%s|sw:%s|cl:%s",
name, p.Size, p.Color, p.Fill, p.Stroke, p.StrokeWidth, p.Class)
// Cache by icon name and class so repeated renders reuse the generated SVG.
cacheKey := fmt.Sprintf("%s|cl:%s", name, p.Class)
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
iconMutex.RLock()
@ -78,33 +71,10 @@ func generateSVG(name string, props Props) (string, error) {
return "", err // Error from getIconContent already includes icon name
}
size := props.Size
if size <= 0 {
size = 24 // Default size
}
fill := props.Fill
if fill == "" {
fill = "none" // Default fill
}
stroke := props.Stroke
if stroke == "" {
stroke = props.Color // Fallback to Color if Stroke is not set
}
if stroke == "" {
stroke = "currentColor" // Default stroke color
}
strokeWidth := props.StrokeWidth
if strokeWidth == "" {
strokeWidth = "2" // Default stroke width
}
// Construct the final SVG string.
// The data-lucide attribute helps identify these as Lucide icons if needed.
return fmt.Sprintf("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\" viewBox=\"0 0 24 24\" fill=\"%s\" stroke=\"%s\" stroke-width=\"%s\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"%s\" data-lucide=\"icon\">%s</svg>",
size, size, fill, stroke, strokeWidth, props.Class, content), nil
return fmt.Sprintf("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"%s\" data-lucide=\"icon\">%s</svg>",
props.Class, content), nil
}
// getIconContent retrieves the raw inner SVG content for a given icon name.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
// templui component input - version: v1.2.0 installed by templui v1.2.0
// templui component input - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/input
package input
@ -38,6 +38,7 @@ type Props struct {
Value string
Disabled bool
Readonly bool
Required bool
FileAccept string
HasError bool
NoTogglePassword bool
@ -75,6 +76,7 @@ templ Input(props ...Props) {
}
disabled?={ p.Disabled }
readonly?={ p.Readonly }
required?={ p.Required }
if p.HasError {
aria-invalid="true"
}
@ -111,20 +113,20 @@ templ Input(props ...Props) {
Attributes: templ.Attributes{"data-tui-input-toggle-password": p.ID},
}) {
<span class="icon-open block">
@icon.Eye(icon.Props{
Size: 18,
})
@icon.Eye(icon.Props{Class: "size-[18px]"})
</span>
<span class="icon-closed hidden">
@icon.EyeOff(icon.Props{
Size: 18,
})
@icon.EyeOff(icon.Props{Class: "size-[18px]"})
</span>
}
}
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/input.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("input")
}
}

View file

@ -1,8 +1,9 @@
// templui component inputotp - version: v1.2.0 installed by templui v1.2.0
// templui component inputotp - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/input-otp
package inputotp
import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
"strconv"
)
@ -176,6 +177,11 @@ templ Separator(props ...SeparatorProps) {
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/inputotp.min.js") }></script>
@scriptOnce.Once() {
@input.Script()
@utils.ComponentScript("inputotp")
}
}

View file

@ -1,4 +1,4 @@
// templui component label - version: v1.2.0 installed by templui v1.2.0
// templui component label - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/label
package label
@ -38,6 +38,10 @@ templ Label(props ...Props) {
</label>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/label.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("label")
}
}

View file

@ -1,4 +1,4 @@
// templui component pagination - version: v1.2.0 installed by templui v1.2.0
// templui component pagination - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/pagination
package pagination
@ -145,7 +145,7 @@ templ Previous(props ...PreviousProps) {
Class: utils.TwMerge("gap-1", p.Class),
Attributes: p.Attributes,
}) {
@icon.ChevronLeft(icon.Props{Size: 16})
@icon.ChevronLeft(icon.Props{Class: "size-4"})
if p.Label != "" {
<span>{ p.Label }</span>
}
@ -168,12 +168,12 @@ templ Next(props ...NextProps) {
if p.Label != "" {
<span>{ p.Label }</span>
}
@icon.ChevronRight(icon.Props{Size: 16})
@icon.ChevronRight(icon.Props{Class: "size-4"})
}
}
templ Ellipsis() {
@icon.Ellipsis(icon.Props{Size: 16})
@icon.Ellipsis(icon.Props{Class: "size-4"})
}
func CreatePagination(currentPage, totalPages, maxVisible int) struct {

View file

@ -1,4 +1,4 @@
// templui component popover - version: v1.2.0 installed by templui v1.2.0
// templui component popover - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/popover
package popover
@ -27,15 +27,21 @@ const (
type TriggerType string
const (
TriggerTypeHover TriggerType = "hover"
TriggerTypeClick TriggerType = "click"
TriggerTypeHover TriggerType = "hover"
TriggerTypeClick TriggerType = "click"
TriggerTypeManual TriggerType = "manual"
)
type RootProps struct {
ID string
Class string
Attributes templ.Attributes
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string
TriggerType TriggerType
}
@ -50,10 +56,26 @@ type ContentProps struct {
ShowArrow bool
HoverDelay int
HoverOutDelay int
MatchWidth bool
Exclusive bool
}
templ Root(props ...RootProps) {
{{ var p RootProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
data-tui-popover-root
class={ utils.TwMerge("contents", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
@ -66,11 +88,8 @@ templ Trigger(props ...TriggerProps) {
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("group cursor-pointer", p.Class) }
if p.For != "" {
data-tui-popover-trigger={ p.For }
}
data-tui-popover-open="false"
class={ utils.TwMerge("contents", p.Class) }
data-tui-popover-trigger
data-tui-popover-type={ string(p.TriggerType) }
{ p.Attributes... }
>
@ -94,8 +113,11 @@ templ Content(props ...ContentProps) {
}
}
<div
id={ p.ID }
data-tui-popover-id={ p.ID }
if p.ID != "" {
id={ p.ID }
}
popover="manual"
data-tui-popover-content
data-tui-popover-open="false"
data-tui-popover-placement={ string(p.Placement) }
data-tui-popover-offset={ strconv.Itoa(p.Offset) }
@ -105,11 +127,8 @@ templ Content(props ...ContentProps) {
data-tui-popover-hover-delay={ strconv.Itoa(p.HoverDelay) }
data-tui-popover-hover-out-delay={ strconv.Itoa(p.HoverOutDelay) }
data-tui-popover-exclusive={ strconv.FormatBool(p.Exclusive) }
if p.MatchWidth {
data-tui-popover-match-width="true"
}
class={ utils.TwMerge(
"bg-popover rounded-lg border text-popover-foreground text-sm shadow-lg pointer-events-auto absolute z-[9999] hidden top-0 left-0",
"bg-popover rounded-lg border text-popover-foreground text-sm shadow-lg pointer-events-auto fixed inset-auto top-0 left-0 m-0 max-w-[min(24rem,calc(100vw-2rem))] overflow-visible outline-none transition-opacity duration-150 ease-out data-[tui-popover-open=false]:opacity-0 data-[tui-popover-open=true]:opacity-100 motion-reduce:transition-none",
p.Class,
) }
{ p.Attributes... }
@ -120,16 +139,16 @@ templ Content(props ...ContentProps) {
if p.ShowArrow {
<div
data-tui-popover-arrow
class="absolute h-2.5 w-2.5 rotate-45 bg-popover border border-border
data-[tui-popover-placement^=top]:-bottom-[5px] data-[tui-popover-placement^=top]:border-t-transparent data-[tui-popover-placement^=top]:border-l-transparent
data-[tui-popover-placement^=bottom]:-top-[5px] data-[tui-popover-placement^=bottom]:border-b-transparent data-[tui-popover-placement^=bottom]:border-r-transparent
data-[tui-popover-placement^=left]:-right-[5px] data-[tui-popover-placement^=left]:border-l-transparent data-[tui-popover-placement^=left]:border-b-transparent
data-[tui-popover-placement^=right]:-left-[5px] data-[tui-popover-placement^=right]:border-r-transparent data-[tui-popover-placement^=right]:border-t-transparent"
class="absolute h-2.5 w-2.5 rotate-45 bg-popover border border-border"
></div>
}
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/popover.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("popover")
}
}

View file

@ -1,4 +1,4 @@
// templui component progress - version: v1.2.0 installed by templui v1.2.0
// templui component progress - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/progress
package progress
@ -122,6 +122,10 @@ func variantClasses(variant Variant) string {
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/progress.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("progress")
}
}

View file

@ -1,4 +1,4 @@
// templui component radio - version: v1.2.0 installed by templui v1.2.0
// templui component radio - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/radio
package radio

View file

@ -1,4 +1,4 @@
// templui component rating - version: v1.2.0 installed by templui v1.2.0
// templui component rating - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/rating
package rating
@ -166,7 +166,7 @@ func ratingIcon(style Style, filled bool, value float64) templ.Component {
}
iconProps := icon.Props{}
if filled {
iconProps.Fill = "currentColor"
iconProps.Class = "fill-current"
}
switch style {
case StyleHeart:
@ -188,6 +188,10 @@ func (p *Props) setDefaults() {
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/rating.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("rating")
}
}

View file

@ -1,10 +1,8 @@
// templui component selectbox - version: v1.2.0 installed by templui v1.2.0
// templui component selectbox - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/select-box
package selectbox
import (
"context"
"fmt"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
@ -13,10 +11,6 @@ import (
"strconv"
)
type contextKey string
var contentIDKey contextKey = "contentID"
type Props struct {
ID string
Class string
@ -80,170 +74,18 @@ templ SelectBox(props ...Props) {
if len(props) > 0 {
p = props[0]
}
wrapperID := p.ID
if wrapperID == "" {
wrapperID = utils.RandomID()
}
contentID := fmt.Sprintf("%s-content", wrapperID)
ctx = context.WithValue(ctx, contentIDKey, contentID)
}}
<div
id={ wrapperID }
class={ utils.TwMerge("select-container w-full relative", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Trigger(props ...TriggerProps) {
{{
var p TriggerProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-select-content-id"
}
if p.ShowPills {
p.Multiple = true
}
}}
@popover.Trigger(popover.TriggerProps{
For: contentID,
TriggerType: popover.TriggerTypeClick,
}) {
@button.Button(button.Props{
ID: p.ID,
Type: "button",
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Required class for JavaScript
"select-trigger",
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"data-tui-selectbox-content-id": contentID,
"data-tui-selectbox-multiple": strconv.FormatBool(p.Multiple),
"data-tui-selectbox-show-pills": strconv.FormatBool(p.ShowPills),
"data-tui-selectbox-selected-count-text": p.SelectedCountText,
"tabindex": "0",
"aria-invalid": utils.If(p.HasError, "true"),
},
),
}) {
<input
type="hidden"
if p.Name != "" {
name={ p.Name }
}
if p.Form != "" {
form={ p.Form }
}
data-tui-selectbox-hidden-input
{ p.Attributes... }
/>
{ children... }
<span class="pointer-events-none ml-1">
@icon.ChevronDown(icon.Props{
Size: 16,
Class: "text-muted-foreground",
})
</span>
}
}
}
templ Value(props ...ValueProps) {
{{ var p ValueProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("block truncate select-value text-muted-foreground", p.Class) }
if p.Placeholder != "" {
data-tui-selectbox-placeholder={ p.Placeholder }
}
class={ utils.TwMerge("select-container w-full relative", p.Class) }
{ p.Attributes... }
>
if p.Placeholder != "" {
{ p.Placeholder }
}
{ children... }
</span>
}
templ Content(props ...ContentProps) {
{{
var p ContentProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-select-content-id"
}
}}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: popover.PlacementBottomStart,
Offset: 4,
MatchWidth: true,
DisableESC: !p.NoSearch,
Class: utils.TwMerge(
"p-1 select-content z-50 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md",
"min-w-[var(--popover-trigger-width)] w-[var(--popover-trigger-width)]",
p.Class,
),
Attributes: utils.MergeAttributes(
templ.Attributes{
"role": "listbox",
"tabindex": "-1",
},
p.Attributes,
),
Exclusive: true,
}) {
if !p.NoSearch {
<div class="sticky top-0 bg-popover p-1">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground z-10 pointer-events-none">
@icon.Search(icon.Props{Size: 16})
</span>
@input.Input(input.Props{
Type: input.TypeSearch,
Class: "pl-8",
Placeholder: utils.IfElse(p.SearchPlaceholder != "", p.SearchPlaceholder, "Search..."),
Attributes: templ.Attributes{
"data-tui-selectbox-search": "",
},
})
</div>
</div>
}
<div class="max-h-[300px] overflow-y-auto">
@popover.Root() {
{ children... }
</div>
}
}
</div>
}
templ Group(props ...GroupProps) {
@ -290,10 +132,10 @@ templ Item(props ...ItemProps) {
}
class={
utils.TwMerge(
"select-item relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-light outline-none",
"select-item group relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-light outline-none",
"hover:bg-accent hover:text-accent-foreground",
"focus:bg-accent focus:text-accent-foreground",
utils.If(p.Selected, "bg-accent text-accent-foreground"),
"focus-visible:bg-accent focus-visible:text-accent-foreground",
"data-[tui-selectbox-selected=true]:bg-accent data-[tui-selectbox-selected=true]:text-accent-foreground",
utils.If(p.Disabled, "pointer-events-none opacity-50"),
p.Class,
),
@ -311,16 +153,163 @@ templ Item(props ...ItemProps) {
<span
class={
utils.TwMerge(
"select-check absolute right-2 flex h-3.5 w-3.5 items-center justify-center",
utils.IfElse(p.Selected, "opacity-100", "opacity-0"),
"select-check absolute right-2 flex h-3.5 w-3.5 items-center justify-center opacity-0",
"group-data-[tui-selectbox-selected=true]:opacity-100",
),
}
>
@icon.Check(icon.Props{Size: 16})
@icon.Check(icon.Props{Class: "size-4"})
</span>
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/selectbox.min.js") }></script>
templ Trigger(props ...TriggerProps) {
{{
var p TriggerProps
if len(props) > 0 {
p = props[0]
}
if p.ShowPills {
p.Multiple = true
}
}}
@popover.Trigger(popover.TriggerProps{
TriggerType: popover.TriggerTypeClick,
}) {
@button.Button(button.Props{
ID: p.ID,
Type: "button",
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Required class for JavaScript
"select-trigger",
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"data-tui-selectbox-multiple": strconv.FormatBool(p.Multiple),
"data-tui-selectbox-show-pills": strconv.FormatBool(p.ShowPills),
"data-tui-selectbox-selected-count-text": p.SelectedCountText,
"tabindex": "0",
"aria-invalid": utils.If(p.HasError, "true"),
},
),
}) {
<input
type="hidden"
if p.Name != "" {
name={ p.Name }
}
if p.Form != "" {
form={ p.Form }
}
data-tui-selectbox-hidden-input
{ p.Attributes... }
/>
{ children... }
<span
class="ml-1 hidden cursor-pointer text-muted-foreground hover:text-foreground"
data-tui-selectbox-clear-trigger
aria-label="Clear selection"
role="button"
>
@icon.CircleX(icon.Props{Class: "size-3.5"})
</span>
<span class="pointer-events-none ml-1" data-tui-selectbox-chevron>
@icon.ChevronDown(icon.Props{Class: "size-4 text-muted-foreground"})
</span>
}
}
}
templ Value(props ...ValueProps) {
{{ var p ValueProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("block truncate select-value text-muted-foreground", p.Class) }
if p.Placeholder != "" {
data-tui-selectbox-placeholder={ p.Placeholder }
}
{ p.Attributes... }
>
if p.Placeholder != "" {
{ p.Placeholder }
}
{ children... }
</span>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Content(popover.ContentProps{
Placement: popover.PlacementBottomStart,
Offset: 4,
DisableESC: !p.NoSearch,
Class: utils.TwMerge(
"p-1 select-content z-50 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md",
p.Class,
),
Attributes: utils.MergeAttributes(
templ.Attributes{
"role": "listbox",
"tabindex": "-1",
"data-tui-selectbox-content": "true",
},
p.Attributes,
),
Exclusive: true,
}) {
if !p.NoSearch {
<div class="sticky top-0 bg-popover p-1">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground z-10 pointer-events-none">
@icon.Search(icon.Props{Class: "size-4"})
</span>
@input.Input(input.Props{
Type: input.TypeSearch,
Class: "pl-8",
Placeholder: utils.IfElse(p.SearchPlaceholder != "", p.SearchPlaceholder, "Search..."),
Attributes: templ.Attributes{
"data-tui-selectbox-search": "",
},
})
</div>
</div>
}
<div class="max-h-[300px] overflow-y-auto">
{ children... }
</div>
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
@scriptOnce.Once() {
@input.Script()
@popover.Script()
@utils.ComponentScript("selectbox")
}
}

View file

@ -1,4 +1,4 @@
// templui component separator - version: v1.2.0 installed by templui v1.2.0
// templui component separator - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/separator
package separator

View file

@ -1,4 +1,4 @@
// templui component sheet - version: v1.2.0 installed by templui v1.2.0
// templui component sheet - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/sheet
package sheet
@ -37,16 +37,14 @@ type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string // Reference to a specific sheet ID (for external triggers)
For string // Sheet root ID for external triggers
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
HideCloseButton bool
Side Side //
Open bool // Initial open state for standalone usage
Side Side
}
type HeaderProps struct {
@ -140,20 +138,16 @@ templ Content(props ...ContentProps) {
}
// Sheet content uses Dialog content with sheet-specific styles
@dialog.Content(dialog.ContentProps{
ID: p.ID,
Open: p.Open,
HideCloseButton: p.HideCloseButton,
Class: utils.TwMerge(
// First apply side-specific positioning and animations
getSideClasses(p.Side),
// Default gap matching shadcn (no padding in content)
"gap-4 !p-0", // Remove Dialog's p-6 padding
// Move panel layout overrides to the inner dialog panel
"[&_[data-tui-dialog-panel]]:gap-4 [&_[data-tui-dialog-panel]]:!p-0",
// Override Dialog styles
"!scale-100", // Reset Dialog's scale animation
"!rounded-none", // Remove dialog rounded corners
"!opacity-100", // Keep fully opaque - no fade, only slide
// Remove pointer-events control during animation
"!pointer-events-auto data-[tui-dialog-hidden=true]:!pointer-events-none",
// User-provided classes last
p.Class,
),
@ -256,26 +250,26 @@ func getSideClasses(side Side) string {
case SideRight:
return baseClasses +
// Positioning
"!inset-y-0 !right-0 !left-auto !top-auto " +
"!top-0 !bottom-0 !right-0 !left-auto " +
// Size
"h-full w-3/4 sm:max-w-sm " +
"!h-dvh !max-h-dvh w-3/4 sm:!max-w-sm " +
// Border
"border-l border-t-0 border-r-0 border-b-0 " +
// Reset Dialog transforms
"!translate-y-0 " +
"!translate-x-0 !translate-y-0 " +
// Slide animation
"data-[tui-dialog-open=false]:!translate-x-full " +
"data-[tui-dialog-open=true]:!translate-x-0"
case SideLeft:
return baseClasses +
// Positioning
"!inset-y-0 !left-0 !right-auto !top-auto " +
"!top-0 !bottom-0 !left-0 !right-auto " +
// Size
"h-full w-3/4 sm:max-w-sm " +
"!h-dvh !max-h-dvh w-3/4 sm:!max-w-sm " +
// Border
"border-r border-t-0 border-l-0 border-b-0 " +
// Reset Dialog transforms
"!translate-y-0 " +
"!translate-x-0 !translate-y-0 " +
// Slide animation
"data-[tui-dialog-open=false]:!-translate-x-full " +
"data-[tui-dialog-open=true]:!translate-x-0"
@ -308,11 +302,19 @@ func getSideClasses(side Side) string {
default:
return baseClasses +
// Default to right side
"!inset-y-0 !right-0 !left-auto !top-auto " +
"h-full w-3/4 " +
"!top-0 !bottom-0 !right-0 !left-auto " +
"!h-dvh !max-h-dvh w-3/4 sm:!max-w-sm " +
"border-l border-t-0 border-r-0 border-b-0 " +
"!translate-y-0 " +
"!translate-x-0 !translate-y-0 " +
"data-[tui-dialog-open=false]:!translate-x-full " +
"data-[tui-dialog-open=true]:!translate-x-0"
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
@scriptOnce.Once() {
@dialog.Script()
}
}

View file

@ -1,4 +1,4 @@
// templui component sidebar - version: v1.2.0 installed by templui v1.2.0
// templui component sidebar - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/sidebar
package sidebar
@ -47,6 +47,113 @@ type Props struct {
KeyboardShortcut string // default: "b"
}
templ Inset(props ...InsetProps) {
{{ var p InsetProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<main
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"relative flex w-full flex-1 flex-col bg-background",
// Add special styling when peer sidebar has variant="inset"
"md:peer-data-[tui-sidebar-variant=inset]:m-2",
"md:peer-data-[tui-sidebar-variant=inset]:ml-0",
"md:peer-data-[tui-sidebar-variant=inset]:rounded-xl",
"md:peer-data-[tui-sidebar-variant=inset]:shadow-sm",
// When sidebar is collapsed (offcanvas mode) and variant is inset, add left margin back
"md:peer-data-[tui-sidebar-variant=inset]:peer-data-[tui-sidebar-state=collapsed]:peer-data-[tui-sidebar-collapsible=offcanvas]:ml-2",
p.Class,
) }
data-tui-sidebar="inset"
{ p.Attributes... }
>
{ children... }
</main>
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("relative flex w-full min-w-0 flex-col p-2", p.Class) }
data-tui-sidebar="group"
{ p.Attributes... }
>
{ children... }
</div>
}
templ GroupLabel(props ...GroupLabelProps) {
{{ var p GroupLabelProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70",
"ring-sidebar-ring outline-none transition-[margin,opacity] duration-200 ease-linear",
"focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:-mt-8 group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:opacity-0",
p.Class,
) }
data-tui-sidebar="group-label"
{ p.Attributes... }
>
{ children... }
</div>
}
templ MenuBadge(props ...MenuBadgeProps) {
{{ var p MenuBadgeProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"ml-auto flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium",
"bg-sidebar-accent text-sidebar-accent-foreground",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:hidden",
p.Class,
) }
data-tui-sidebar="menu-badge"
{ p.Attributes... }
>
{ children... }
</span>
}
templ Separator(props ...SeparatorProps) {
{{ var p SeparatorProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<hr
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"mx-2 my-2 border-t border-sidebar-border",
p.Class,
) }
data-tui-sidebar="separator"
{ p.Attributes... }
/>
}
type TriggerProps struct {
ID string
Class string
@ -453,9 +560,7 @@ templ MenuButton(props ...MenuButtonProps) {
// When collapsed to icon mode - show with tooltip
<div class="group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:block hidden">
@tooltip.Tooltip() {
@tooltip.Trigger(tooltip.TriggerProps{
For: tooltipID,
}) {
@tooltip.Trigger(tooltip.TriggerProps{}) {
@menuButtonContent(p, "") {
{ children... }
}
@ -641,113 +746,12 @@ templ MenuSubButton(props ...MenuSubButtonProps) {
}
}
templ Inset(props ...InsetProps) {
{{ var p InsetProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<main
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"relative flex w-full flex-1 flex-col bg-background",
// Add special styling when peer sidebar has variant="inset"
"md:peer-data-[tui-sidebar-variant=inset]:m-2",
"md:peer-data-[tui-sidebar-variant=inset]:ml-0",
"md:peer-data-[tui-sidebar-variant=inset]:rounded-xl",
"md:peer-data-[tui-sidebar-variant=inset]:shadow-sm",
// When sidebar is collapsed (offcanvas mode) and variant is inset, add left margin back
"md:peer-data-[tui-sidebar-variant=inset]:peer-data-[tui-sidebar-state=collapsed]:peer-data-[tui-sidebar-collapsible=offcanvas]:ml-2",
p.Class,
) }
data-tui-sidebar="inset"
{ p.Attributes... }
>
{ children... }
</main>
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("relative flex w-full min-w-0 flex-col p-2", p.Class) }
data-tui-sidebar="group"
{ p.Attributes... }
>
{ children... }
</div>
}
templ GroupLabel(props ...GroupLabelProps) {
{{ var p GroupLabelProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70",
"ring-sidebar-ring outline-none transition-[margin,opacity] duration-200 ease-linear",
"focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:-mt-8 group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:opacity-0",
p.Class,
) }
data-tui-sidebar="group-label"
{ p.Attributes... }
>
{ children... }
</div>
}
templ MenuBadge(props ...MenuBadgeProps) {
{{ var p MenuBadgeProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"ml-auto flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium",
"bg-sidebar-accent text-sidebar-accent-foreground",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:hidden",
p.Class,
) }
data-tui-sidebar="menu-badge"
{ p.Attributes... }
>
{ children... }
</span>
}
templ Separator(props ...SeparatorProps) {
{{ var p SeparatorProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<hr
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"mx-2 my-2 border-t border-sidebar-border",
p.Class,
) }
data-tui-sidebar="separator"
{ p.Attributes... }
/>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/sidebar.min.js") }></script>
@scriptOnce.Once() {
@sheet.Script()
@tooltip.Script()
@utils.ComponentScript("sidebar")
}
}

View file

@ -1,4 +1,4 @@
// templui component skeleton - version: v1.2.0 installed by templui v1.2.0
// templui component skeleton - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/skeleton
package skeleton

View file

@ -1,4 +1,4 @@
// templui component slider - version: v1.2.0 installed by templui v1.2.0
// templui component slider - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/slider
package slider
@ -116,6 +116,10 @@ templ Value(props ...ValueProps) {
</span>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/slider.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("slider")
}
}

View file

@ -1,4 +1,4 @@
// templui component switch - version: v1.2.0 installed by templui v1.2.0
// templui component switch - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/switch
package switchcomp

View file

@ -1,4 +1,4 @@
// templui component table - version: v1.2.0 installed by templui v1.2.0
// templui component table - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/table
package table

View file

@ -1,4 +1,4 @@
// templui component tabs - version: v1.2.0 installed by templui v1.2.0
// templui component tabs - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/tabs
package tabs
@ -158,6 +158,10 @@ func IDFromContext(ctx context.Context) string {
return ""
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/tabs.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("tabs")
}
}

View file

@ -1,10 +1,11 @@
// templui component tagsinput - version: v1.2.0 installed by templui v1.2.0
// templui component tagsinput - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/tags-input
package tagsinput
import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/popover"
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
)
@ -19,19 +20,26 @@ type Props struct {
Attributes templ.Attributes
Disabled bool
Readonly bool
Suggestions []string
}
templ TagsInput(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{
var p Props
if len(props) > 0 {
p = props[0]
}
if p.ID == "" {
p.ID = utils.RandomID()
}
suggestionsID := p.ID + "-suggestions"
}}
<div
id={ p.ID + "-container" }
class={
utils.TwMerge(
// Base styles
"flex items-center flex-wrap gap-2 p-2 rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
"flex flex-col gap-2 p-2 rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Focus styles
@ -48,9 +56,13 @@ templ TagsInput(props ...Props) {
data-tui-tagsinput
data-tui-tagsinput-name={ p.Name }
data-tui-tagsinput-form={ p.Form }
if len(p.Suggestions) > 0 {
data-tui-tagsinput-suggestions-id={ suggestionsID }
}
{ p.Attributes... }
>
<div class="flex items-center flex-wrap gap-2" data-tui-tagsinput-container>
<!-- Tags row (hidden when empty) -->
<div class="flex flex-wrap gap-2 empty:hidden" data-tui-tagsinput-chips>
for _, tag := range p.Value {
@badge.Badge(badge.Props{
Attributes: templ.Attributes{"data-tui-tagsinput-chip": ""},
@ -69,19 +81,65 @@ templ TagsInput(props ...Props) {
}
}
</div>
@input.Input(input.Props{
ID: p.ID,
Class: "border-0 shadow-none focus-visible:ring-0 h-auto py-0 px-0 bg-transparent rounded-none min-h-0 disabled:opacity-100 dark:bg-transparent",
Type: input.TypeText,
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Readonly: p.Readonly,
Attributes: utils.MergeAttributes(
templ.Attributes{"data-tui-tagsinput-text-input": ""},
p.Attributes,
),
})
<div data-tui-tagsinput-hidden-inputs>
<!-- Input row -->
if len(p.Suggestions) > 0 {
@popover.Root(popover.RootProps{
ID: suggestionsID,
}) {
@popover.Trigger(popover.TriggerProps{
TriggerType: popover.TriggerTypeManual,
}) {
<div class="relative w-full">
@input.Input(input.Props{
ID: p.ID,
Class: "border-0 shadow-none focus-visible:ring-0 h-auto py-0 px-0 bg-transparent rounded-none min-h-0 disabled:opacity-100 dark:bg-transparent w-full",
Type: input.TypeText,
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Readonly: p.Readonly,
Attributes: utils.MergeAttributes(
templ.Attributes{"data-tui-tagsinput-text-input": ""},
p.Attributes,
),
})
</div>
}
@popover.Content(popover.ContentProps{
Placement: popover.PlacementBottomStart,
DisableClickAway: true,
Class: "p-1 max-h-[200px] overflow-y-auto min-w-[var(--trigger-width,12rem)] w-[var(--trigger-width,12rem)]",
Attributes: templ.Attributes{
"data-tui-tagsinput-suggestions-content": "true",
},
}) {
for _, suggestion := range p.Suggestions {
<div
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 px-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
data-tui-tagsinput-suggestion
data-tui-tagsinput-suggestion-value={ suggestion }
>
{ suggestion }
</div>
}
}
}
} else {
<div class="relative w-full">
@input.Input(input.Props{
ID: p.ID,
Class: "border-0 shadow-none focus-visible:ring-0 h-auto py-0 px-0 bg-transparent rounded-none min-h-0 disabled:opacity-100 dark:bg-transparent w-full",
Type: input.TypeText,
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Readonly: p.Readonly,
Attributes: utils.MergeAttributes(
templ.Attributes{"data-tui-tagsinput-text-input": ""},
p.Attributes,
),
})
</div>
}
<div data-tui-tagsinput-hidden-inputs class="hidden">
for _, tag := range p.Value {
<input type="hidden" name={ p.Name } value={ tag }/>
}
@ -89,6 +147,11 @@ templ TagsInput(props ...Props) {
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/tagsinput.min.js") }></script>
@scriptOnce.Once() {
@popover.Script()
@utils.ComponentScript("tagsinput")
}
}

View file

@ -1,4 +1,4 @@
// templui component textarea - version: v1.2.0 installed by templui v1.2.0
// templui component textarea - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/textarea
package textarea
@ -80,6 +80,10 @@ templ Textarea(props ...Props) {
>{ p.Value }</textarea>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/textarea.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("textarea")
}
}

View file

@ -1,4 +1,4 @@
// templui component timepicker - version: v1.2.0 installed by templui v1.2.0
// templui component timepicker - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/time-picker
package timepicker
@ -56,7 +56,6 @@ templ TimePicker(props ...Props) {
p.Step = 1
}
var contentID = p.ID + "-content"
var valueString string
if p.Value != (time.Time{}) {
valueString = p.Value.Format("15:04")
@ -70,181 +69,185 @@ templ TimePicker(props ...Props) {
maxTimeString = p.MaxTime.Format("15:04")
}
}}
<div class="relative inline-block w-full">
<input
type="hidden"
name={ p.Name }
value={ valueString }
if p.Form != "" {
form={ p.Form }
}
id={ p.ID + "-hidden" }
data-tui-timepicker-hidden-input="true"
/>
@popover.Trigger(popover.TriggerProps{For: contentID}) {
@button.Button(button.Props{
ID: p.ID,
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(p.Attributes, templ.Attributes{
"data-tui-timepicker": "true",
"data-tui-timepicker-use12hours": fmt.Sprintf("%t", p.Use12Hours),
"data-tui-timepicker-am-label": p.AMLabel,
"data-tui-timepicker-pm-label": p.PMLabel,
"data-tui-timepicker-placeholder": p.Placeholder,
"data-tui-timepicker-step": fmt.Sprintf("%d", p.Step),
"data-tui-timepicker-min-time": minTimeString,
"data-tui-timepicker-max-time": maxTimeString,
"aria-invalid": utils.If(p.HasError, "true"),
}),
}) {
<span data-tui-timepicker-display class="text-left grow text-muted-foreground">
{ p.Placeholder }
</span>
<span class="text-muted-foreground flex items-center ml-2">
@icon.Clock(icon.Props{Size: 16})
</span>
}
}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: popover.PlacementBottomStart,
Class: "p-0 w-80",
}) {
@card.Card(card.Props{
Class: "border-0 shadow-none",
}) {
@card.Content(card.ContentProps{
Class: "p-4",
<div class="relative inline-block w-full" data-tui-timepicker-root>
@popover.Root() {
<input
type="hidden"
name={ p.Name }
value={ valueString }
if p.Form != "" {
form={ p.Form }
}
data-tui-timepicker-hidden-input="true"
/>
@popover.Trigger() {
@button.Button(button.Props{
ID: p.ID,
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(p.Attributes, templ.Attributes{
"data-tui-timepicker": "true",
"data-tui-timepicker-use12hours": fmt.Sprintf("%t", p.Use12Hours),
"data-tui-timepicker-am-label": p.AMLabel,
"data-tui-timepicker-pm-label": p.PMLabel,
"data-tui-timepicker-placeholder": p.Placeholder,
"data-tui-timepicker-step": fmt.Sprintf("%d", p.Step),
"data-tui-timepicker-min-time": minTimeString,
"data-tui-timepicker-max-time": maxTimeString,
"aria-invalid": utils.If(p.HasError, "true"),
}),
}) {
<div
data-tui-timepicker-popup="true"
data-tui-timepicker-input-name={ p.Name }
data-tui-timepicker-parent-id={ p.ID }
if valueString != "" {
data-tui-timepicker-value={ valueString }
}
>
// Time selection grid
<div class="grid grid-cols-2 gap-3 mb-4">
// Hour selection
<div class="space-y-2">
<label class="text-sm font-medium">Hour</label>
<div class="max-h-32 overflow-y-auto border rounded-md bg-background">
<div data-tui-timepicker-hour-list="true" class="p-1 space-y-0.5">
if p.Use12Hours {
// 12-hour format: 12, 01-11
<button
type="button"
data-tui-timepicker-hour="0"
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
12
</button>
for hour := 1; hour <= 11; hour++ {
<span data-tui-timepicker-display class="text-left grow text-muted-foreground">
{ p.Placeholder }
</span>
<span class="text-muted-foreground flex items-center ml-2">
@icon.Clock(icon.Props{Class: "size-4"})
</span>
}
}
@popover.Content(popover.ContentProps{
Placement: popover.PlacementBottomStart,
Class: "p-0 w-80",
}) {
@card.Card(card.Props{
Class: "border-0 shadow-none",
}) {
@card.Content(card.ContentProps{
Class: "p-4",
}) {
<div
data-tui-timepicker-popup="true"
data-tui-timepicker-input-name={ p.Name }
if valueString != "" {
data-tui-timepicker-value={ valueString }
}
>
// Time selection grid
<div class="grid grid-cols-2 gap-3 mb-4">
// Hour selection
<div class="space-y-2">
<label class="text-sm font-medium">Hour</label>
<div class="max-h-32 overflow-y-auto border rounded-md bg-background">
<div data-tui-timepicker-hour-list="true" class="p-1 space-y-0.5">
if p.Use12Hours {
// 12-hour format: 12, 01-11
<button
type="button"
data-tui-timepicker-hour={ strconv.Itoa(hour) }
data-tui-timepicker-hour="0"
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", hour) }
12
</button>
for hour := 1; hour <= 11; hour++ {
<button
type="button"
data-tui-timepicker-hour={ strconv.Itoa(hour) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", hour) }
</button>
}
} else {
// 24-hour format: 00-23
for hour := 0; hour < 24; hour++ {
<button
type="button"
data-tui-timepicker-hour={ strconv.Itoa(hour) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", hour) }
</button>
}
}
} else {
// 24-hour format: 00-23
for hour := 0; hour < 24; hour++ {
</div>
</div>
</div>
// Minute selection
<div class="space-y-2">
<label class="text-sm font-medium">Minute</label>
<div class="max-h-32 overflow-y-auto border rounded-md bg-background">
<div data-tui-timepicker-minute-list="true" class="p-1 space-y-0.5">
for minute := 0; minute < 60; minute += p.Step {
<button
type="button"
data-tui-timepicker-hour={ strconv.Itoa(hour) }
data-tui-timepicker-minute={ strconv.Itoa(minute) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", hour) }
{ fmt.Sprintf("%02d", minute) }
</button>
}
}
</div>
</div>
</div>
</div>
// Minute selection
<div class="space-y-2">
<label class="text-sm font-medium">Minute</label>
<div class="max-h-32 overflow-y-auto border rounded-md bg-background">
<div data-tui-timepicker-minute-list="true" class="p-1 space-y-0.5">
for minute := 0; minute < 60; minute += p.Step {
<button
type="button"
data-tui-timepicker-minute={ strconv.Itoa(minute) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", minute) }
</button>
}
// AM/PM selector and action buttons
<div class="flex justify-between items-center">
if p.Use12Hours {
<div class="flex gap-1">
<button
type="button"
data-tui-timepicker-period="AM"
data-tui-timepicker-active="false"
class="px-3 py-1 text-sm rounded-md border transition-colors hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-active=true]:bg-primary data-[tui-timepicker-active=true]:text-primary-foreground data-[tui-timepicker-active=true]:hover:bg-primary/90"
>
{ p.AMLabel }
</button>
<button
type="button"
data-tui-timepicker-period="PM"
data-tui-timepicker-active="false"
class="px-3 py-1 text-sm rounded-md border transition-colors hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-active=true]:bg-primary data-[tui-timepicker-active=true]:text-primary-foreground data-[tui-timepicker-active=true]:hover:bg-primary/90"
>
{ p.PMLabel }
</button>
</div>
</div>
} else {
<div></div>
}
@button.Button(button.Props{
Type: "button",
Variant: button.VariantSecondary,
Size: button.SizeSm,
Attributes: templ.Attributes{
"data-tui-timepicker-done": "true",
},
}) {
Done
}
</div>
</div>
// AM/PM selector and action buttons
<div class="flex justify-between items-center">
if p.Use12Hours {
<div class="flex gap-1">
<button
type="button"
data-tui-timepicker-period="AM"
data-tui-timepicker-active="false"
class="px-3 py-1 text-sm rounded-md border transition-colors hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-active=true]:bg-primary data-[tui-timepicker-active=true]:text-primary-foreground data-[tui-timepicker-active=true]:hover:bg-primary/90"
>
{ p.AMLabel }
</button>
<button
type="button"
data-tui-timepicker-period="PM"
data-tui-timepicker-active="false"
class="px-3 py-1 text-sm rounded-md border transition-colors hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-active=true]:bg-primary data-[tui-timepicker-active=true]:text-primary-foreground data-[tui-timepicker-active=true]:hover:bg-primary/90"
>
{ p.PMLabel }
</button>
</div>
} else {
<div></div>
}
@button.Button(button.Props{
Type: "button",
Variant: button.VariantSecondary,
Size: button.SizeSm,
Attributes: templ.Attributes{
"data-tui-timepicker-done": "true",
},
}) {
Done
}
</div>
</div>
}
}
}
}
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/timepicker.min.js") }></script>
@scriptOnce.Once() {
@popover.Script()
@utils.ComponentScript("timepicker")
}
}

View file

@ -1,4 +1,4 @@
// templui component toast - version: v1.2.0 installed by templui v1.2.0
// templui component toast - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/toast
package toast
@ -109,13 +109,13 @@ templ Toast(props ...Props) {
if p.Icon {
switch p.Variant {
case VariantSuccess:
@icon.CircleCheck(icon.Props{Size: 22, Class: "text-green-500 mr-3 flex-shrink-0"})
@icon.CircleCheck(icon.Props{Class: "size-[22px] text-green-500 mr-3 flex-shrink-0"})
case VariantError:
@icon.CircleX(icon.Props{Size: 22, Class: "text-red-500 mr-3 flex-shrink-0"})
@icon.CircleX(icon.Props{Class: "size-[22px] text-red-500 mr-3 flex-shrink-0"})
case VariantWarning:
@icon.TriangleAlert(icon.Props{Size: 22, Class: "text-yellow-500 mr-3 flex-shrink-0"})
@icon.TriangleAlert(icon.Props{Class: "size-[22px] text-yellow-500 mr-3 flex-shrink-0"})
case VariantInfo:
@icon.Info(icon.Props{Size: 22, Class: "text-blue-500 mr-3 flex-shrink-0"})
@icon.Info(icon.Props{Class: "size-[22px] text-blue-500 mr-3 flex-shrink-0"})
}
}
// Content
@ -138,16 +138,17 @@ templ Toast(props ...Props) {
"type": "button",
},
}) {
@icon.X(icon.Props{
Size: 18,
Class: "opacity-75 hover:opacity-100",
})
@icon.X(icon.Props{Class: "size-[18px] opacity-75 hover:opacity-100"})
}
}
</div>
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/toast.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("toast")
}
}

View file

@ -1,4 +1,4 @@
// templui component tooltip - version: v1.2.0 installed by templui v1.2.0
// templui component tooltip - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/tooltip
package tooltip
@ -16,7 +16,6 @@ const (
PositionLeft Position = "left"
)
// Map tooltip positions to popover positions
func mapTooltipPositionToPopover(position Position) popover.Placement {
switch position {
case PositionTop:
@ -42,7 +41,6 @@ type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string
}
type ContentProps struct {
@ -56,7 +54,17 @@ type ContentProps struct {
}
templ Tooltip(props ...Props) {
{ children... }
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Root(popover.RootProps{
ID: p.ID,
Class: p.Class,
Attributes: p.Attributes,
}) {
{ children... }
}
}
templ Trigger(props ...TriggerProps) {
@ -69,7 +77,6 @@ templ Trigger(props ...TriggerProps) {
Class: p.Class,
Attributes: p.Attributes,
TriggerType: popover.TriggerTypeHover,
For: p.For,
}) {
{ children... }
}
@ -92,3 +99,11 @@ templ Content(props ...ContentProps) {
{ children... }
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
@scriptOnce.Once() {
@popover.Script()
}
}

View file

@ -156,7 +156,7 @@ templ AppSidebarDropdown(user *model.User) {
Href: "/app/settings",
}) {
<span class="flex items-center">
@icon.Settings(icon.Props{Size: 16, Class: "mr-2"})
@icon.Settings(icon.Props{Class: "mr-2"})
Settings
</span>
}
@ -168,7 +168,7 @@ templ AppSidebarDropdown(user *model.User) {
},
}) {
<span class="flex items-center">
@icon.LogOut(icon.Props{Size: 16, Class: "mr-2"})
@icon.LogOut(icon.Props{Class: "mr-2"})
Log out
</span>
}

View file

@ -1,13 +1,19 @@
// templui util templui.go - version: v0.101.0 installed by templui v0.101.0
// templui util templui.go - version: v1.9.5 installed by templui v1.9.5
package utils
import (
"context"
"crypto/rand"
"fmt"
"io"
"io/fs"
"net/http"
"path"
"strings"
"time"
"crypto/rand"
"github.com/a-h/templ"
"github.com/templui/templui/components"
twmerge "github.com/Oudwins/tailwind-merge-go"
)
@ -18,9 +24,9 @@ func TwMerge(classes ...string) string {
return twmerge.Merge(classes...)
}
// TwIf returns value if condition is true, otherwise an empty value of type T.
// If returns value if condition is true, otherwise the zero value of T.
// Example: true, "bg-red-500" → "bg-red-500"
func If[T comparable](condition bool, value T) T {
func If[T any](condition bool, value T) T {
var empty T
if condition {
return value
@ -28,7 +34,7 @@ func If[T comparable](condition bool, value T) T {
return empty
}
// TwIfElse returns trueValue if condition is true, otherwise falseValue.
// IfElse returns trueValue if condition is true, otherwise falseValue.
// Example: true, "bg-red-500", "bg-gray-300" → "bg-red-500"
func IfElse[T any](condition bool, trueValue T, falseValue T) T {
if condition {
@ -56,7 +62,7 @@ func RandomID() string {
}
// ScriptVersion is a timestamp generated at app start for cache busting.
// Used in Script() templates to append ?v=<timestamp> to script URLs.
// Used in component script tags to append ?v=<timestamp> to script URLs.
var ScriptVersion = fmt.Sprintf("%d", time.Now().Unix())
// ScriptURL generates cache-busted script URLs.
@ -72,3 +78,85 @@ var ScriptVersion = fmt.Sprintf("%d", time.Now().Unix())
var ScriptURL = func(path string) string {
return path + "?v=" + ScriptVersion
}
// componentScriptBasePath is the base public path for component JavaScript files.
// In the import workflow this stays "/templui/js". The CLI rewrites it to the user's local jsPublicPath.
var componentScriptBasePath = "/assets/js"
// UseUnminifiedScripts switches component script loading to the unminified files.
// Leave this false in normal use and set it to true during app startup for debugging.
var UseUnminifiedScripts = false
// ComponentScript renders a deferred script tag for a component JavaScript file.
// Example: ComponentScript("datepicker") → <script defer src="/templui/js/datepicker.min.js?..."></script>
func ComponentScript(component string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
nonce := templ.GetNonce(ctx)
fileName := component + ".min.js"
if UseUnminifiedScripts {
fileName = component + ".js"
}
src := ScriptURL(componentScriptBasePath + "/" + fileName)
if _, err := io.WriteString(w, `<script type="module"`); err != nil {
return err
}
if nonce != "" {
if _, err := io.WriteString(w, ` nonce="`); err != nil {
return err
}
if _, err := io.WriteString(w, templ.EscapeString(nonce)); err != nil {
return err
}
if _, err := io.WriteString(w, `"`); err != nil {
return err
}
}
if _, err := io.WriteString(w, ` src="`); err != nil {
return err
}
if _, err := io.WriteString(w, templ.EscapeString(src)); err != nil {
return err
}
if _, err := io.WriteString(w, `"></script>`); err != nil {
return err
}
return nil
})
}
// SetupScriptRoutes serves embedded component JavaScript files for the import workflow.
// Example: SetupScriptRoutes(mux, true) mounts /templui/js/*.js with no-store caching in development.
func SetupScriptRoutes(mux *http.ServeMux, isDevelopment bool) {
if mux == nil || componentScriptBasePath != "/templui/js" {
return
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlPath := strings.TrimPrefix(r.URL.Path, "/templui/js/")
if urlPath == r.URL.Path || urlPath == "" || strings.Contains(urlPath, "..") {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/javascript")
if isDevelopment {
w.Header().Set("Cache-Control", "no-store")
} else {
w.Header().Set("Cache-Control", "public, max-age=31536000")
}
fileName := path.Base(urlPath)
component := strings.TrimSuffix(fileName, ".min.js")
component = strings.TrimSuffix(component, ".js")
file, err := fs.ReadFile(components.TemplFiles, path.Join(component, fileName))
if err != nil {
http.NotFound(w, r)
return
}
_, _ = w.Write(file)
})
mux.Handle("GET /templui/js/", handler)
}