chore: update templ and templui
This commit is contained in:
parent
b5d195baea
commit
61eaa268ab
89 changed files with 25776 additions and 8231 deletions
77
CLAUDE.md
77
CLAUDE.md
|
|
@ -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
63
assets/js/avatar.js
Normal 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
414
assets/js/calendar.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
2
assets/js/calendar.min.js
vendored
2
assets/js/calendar.min.js
vendored
File diff suppressed because one or more lines are too long
236
assets/js/carousel.js
Normal file
236
assets/js/carousel.js
Normal 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
214
assets/js/chart.js
Normal 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 });
|
||||
}
|
||||
})();
|
||||
4
assets/js/chart.min.js
vendored
4
assets/js/chart.min.js
vendored
File diff suppressed because one or more lines are too long
11735
assets/js/chartjs.js
Normal file
11735
assets/js/chartjs.js
Normal file
File diff suppressed because it is too large
Load diff
82
assets/js/checkbox.js
Normal file
82
assets/js/checkbox.js
Normal 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
70
assets/js/collapsible.js
Normal 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
101
assets/js/copybutton.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
|
|
@ -33,31 +33,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
function todayISO() {
|
||||
var d = new Date();
|
||||
return (
|
||||
d.getFullYear() +
|
||||
"-" +
|
||||
String(d.getMonth() + 1).padStart(2, "0") +
|
||||
"-" +
|
||||
String(d.getDate()).padStart(2, "0")
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function parseISODate(isoString) {
|
||||
if (!isoString) return null;
|
||||
|
|
@ -103,32 +78,45 @@
|
|||
}
|
||||
}
|
||||
|
||||
// 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(
|
||||
function findRoot(element) {
|
||||
return element?.closest("[data-tui-datepicker-root]") || null;
|
||||
}
|
||||
|
||||
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]");
|
||||
const display = trigger?.querySelector("[data-tui-datepicker-display]");
|
||||
|
||||
return { calendar, hiddenInput, display };
|
||||
return { trigger, calendar, hiddenInput, display };
|
||||
}
|
||||
|
||||
function closePopover(root) {
|
||||
const popoverContent = root?.querySelector("[data-tui-popover-content]");
|
||||
if (!popoverContent?.matches(":popover-open")) return;
|
||||
|
||||
try {
|
||||
popoverContent.hidePopover();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Update display
|
||||
function updateDisplay(trigger) {
|
||||
const elements = findElements(trigger);
|
||||
if (!elements.display || !elements.hiddenInput) return;
|
||||
function updateDisplay(root) {
|
||||
const elements = findElements(root);
|
||||
if (!elements.trigger || !elements.display || !elements.hiddenInput) return;
|
||||
|
||||
const format =
|
||||
trigger.getAttribute("data-tui-datepicker-display-format") ||
|
||||
elements.trigger.getAttribute("data-tui-datepicker-display-format") ||
|
||||
"locale-medium";
|
||||
const locale =
|
||||
trigger.getAttribute("data-tui-datepicker-locale-tag") || "en-US";
|
||||
elements.trigger.getAttribute("data-tui-datepicker-locale-tag") ||
|
||||
"en-US";
|
||||
const placeholder =
|
||||
trigger.getAttribute("data-tui-datepicker-placeholder") ||
|
||||
elements.trigger.getAttribute("data-tui-datepicker-placeholder") ||
|
||||
"Select a date";
|
||||
|
||||
if (elements.hiddenInput.value) {
|
||||
|
|
@ -144,43 +132,30 @@
|
|||
elements.display.classList.add("text-muted-foreground");
|
||||
}
|
||||
|
||||
function toISODate(date) {
|
||||
if (!date || isNaN(date.getTime())) return "";
|
||||
return date.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
// 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;
|
||||
const root = findRoot(calendar);
|
||||
const elements = findElements(root);
|
||||
if (!elements.hiddenInput || !e.detail?.date) return;
|
||||
|
||||
const triggerId = calendar.id.replace("-calendar-instance", "");
|
||||
const trigger = document.getElementById(triggerId);
|
||||
if (!trigger || !trigger.hasAttribute("data-tui-datepicker")) return;
|
||||
|
||||
const elements = findElements(trigger);
|
||||
if (!elements.display || !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.display.textContent = formatDate(e.detail.date, format, locale);
|
||||
elements.display.classList.remove("text-muted-foreground");
|
||||
|
||||
// Close the popover
|
||||
if (window.closePopover) {
|
||||
const popoverId =
|
||||
trigger.getAttribute("aria-controls") || trigger.id + "-content";
|
||||
window.closePopover(popoverId);
|
||||
}
|
||||
elements.hiddenInput.value = toISODate(e.detail.date);
|
||||
updateDisplay(root);
|
||||
closePopover(root);
|
||||
});
|
||||
|
||||
// Handle hidden input value changes (for reactive frameworks)
|
||||
document.addEventListener("input", (e) => {
|
||||
if (!e.target.matches("[data-tui-datepicker-hidden-input]")) return;
|
||||
|
||||
const trigger = document.getElementById(e.target.id.replace("-hidden", ""));
|
||||
if (trigger) {
|
||||
updateDisplay(trigger);
|
||||
const root = findRoot(e.target);
|
||||
if (root) {
|
||||
updateDisplay(root);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -188,51 +163,24 @@
|
|||
document.addEventListener("reset", (e) => {
|
||||
if (!e.target.matches("form")) return;
|
||||
|
||||
e.target
|
||||
.querySelectorAll('[data-tui-datepicker="true"]')
|
||||
.forEach((trigger) => {
|
||||
const elements = findElements(trigger);
|
||||
e.target.querySelectorAll("[data-tui-datepicker-root]").forEach((root) => {
|
||||
const elements = findElements(root);
|
||||
if (elements.hiddenInput) {
|
||||
elements.hiddenInput.value = "";
|
||||
}
|
||||
updateDisplay(trigger);
|
||||
updateDisplay(root);
|
||||
});
|
||||
});
|
||||
|
||||
// Clear button handling
|
||||
document.addEventListener("click", (e) => {
|
||||
const trigger = e.target.closest('[data-tui-datepicker-clear="true"]');
|
||||
if (!trigger) return;
|
||||
|
||||
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);
|
||||
document.querySelectorAll("[data-tui-datepicker-root]").forEach((root) => {
|
||||
const elements = findElements(root);
|
||||
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);
|
||||
updateDisplay(root);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
2
assets/js/datepicker.min.js
vendored
2
assets/js/datepicker.min.js
vendored
|
|
@ -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
215
assets/js/dialog.js
Normal 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,
|
||||
};
|
||||
})();
|
||||
2
assets/js/dialog.min.js
vendored
2
assets/js/dialog.min.js
vendored
|
|
@ -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
20
assets/js/dropdown.js
Normal 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
|
||||
}
|
||||
});
|
||||
})();
|
||||
2
assets/js/dropdown.min.js
vendored
2
assets/js/dropdown.min.js
vendored
|
|
@ -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{}})})();})();
|
||||
|
|
|
|||
858
assets/js/floating_ui_core.js
Normal file
858
assets/js/floating_ui_core.js
Normal 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 } }
|
||||
: {};
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
604
assets/js/floating_ui_dom.js
Normal file
604
assets/js/floating_ui_dom.js
Normal 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
25
assets/js/input.js
Normal 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
207
assets/js/inputotp.js
Normal 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 });
|
||||
})();
|
||||
2
assets/js/inputotp.min.js
vendored
2
assets/js/inputotp.min.js
vendored
|
|
@ -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
58
assets/js/label.js
Normal 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
447
assets/js/popover.js
Normal 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,
|
||||
};
|
||||
})();
|
||||
7
assets/js/popover.min.js
vendored
7
assets/js/popover.min.js
vendored
File diff suppressed because one or more lines are too long
49
assets/js/progress.js
Normal file
49
assets/js/progress.js
Normal 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
216
assets/js/rating.js
Normal 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
507
assets/js/selectbox.js
Normal 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 });
|
||||
})();
|
||||
2
assets/js/selectbox.min.js
vendored
2
assets/js/selectbox.min.js
vendored
File diff suppressed because one or more lines are too long
129
assets/js/sidebar.js
Normal file
129
assets/js/sidebar.js
Normal 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
26
assets/js/slider.js
Normal 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
72
assets/js/tabs.js
Normal 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
213
assets/js/tagsinput.js
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
12
assets/js/tagsinput.min.js
vendored
12
assets/js/tagsinput.min.js
vendored
|
|
@ -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
22
assets/js/textarea.js
Normal 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
379
assets/js/timepicker.js
Normal 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 });
|
||||
})();
|
||||
2
assets/js/timepicker.min.js
vendored
2
assets/js/timepicker.min.js
vendored
File diff suppressed because one or more lines are too long
128
assets/js/toast.js
Normal file
128
assets/js/toast.js
Normal 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 });
|
||||
})();
|
||||
2
assets/js/toast.min.js
vendored
2
assets/js/toast.min.js
vendored
|
|
@ -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})})();})();
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
"$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"
|
||||
"goose@latest",
|
||||
"templ@0.3.1001"
|
||||
],
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
|
|
|
|||
24
devbox.lock
24
devbox.lock
|
|
@ -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
4
go.mod
|
|
@ -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
12
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@ templ ThemeSwitcher(props ...ThemeSwitcherProps) {
|
|||
"aria-label": "Toggle theme",
|
||||
},
|
||||
}) {
|
||||
@icon.Eclipse(icon.Props{Size: 20})
|
||||
@icon.Eclipse(icon.Props{Class: ""})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ type Props struct {
|
|||
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.
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +102,11 @@ templ Chart(props ...Props) {
|
|||
<canvas id={ canvasId } data-tui-chart-id={ dataId }></canvas>
|
||||
</div>
|
||||
{{
|
||||
chartConfig := Config{
|
||||
scriptConfig := ScriptConfig{
|
||||
RawConfig: p.RawConfig,
|
||||
}
|
||||
if p.RawConfig == nil {
|
||||
generatedConfig := Config{
|
||||
Type: p.Variant,
|
||||
Data: p.Data,
|
||||
Options: p.Options,
|
||||
|
|
@ -113,10 +123,16 @@ templ Chart(props ...Props) {
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,20 +71,15 @@ 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">
|
||||
<div class="relative inline-block w-full" data-tui-datepicker-root>
|
||||
@popover.Root() {
|
||||
<input
|
||||
type="hidden"
|
||||
name={ p.Name }
|
||||
|
|
@ -94,10 +87,9 @@ templ DatePicker(props ...Props) {
|
|||
if p.Form != "" {
|
||||
form={ p.Form }
|
||||
}
|
||||
id={ p.ID + "-hidden" }
|
||||
data-tui-datepicker-hidden-input
|
||||
/>
|
||||
@popover.Trigger(popover.TriggerProps{For: contentID}) {
|
||||
@popover.Trigger() {
|
||||
@button.Button(button.Props{
|
||||
ID: p.ID,
|
||||
Variant: button.VariantOutline,
|
||||
|
|
@ -123,7 +115,6 @@ templ DatePicker(props ...Props) {
|
|||
"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"),
|
||||
}),
|
||||
}) {
|
||||
|
|
@ -133,12 +124,11 @@ templ DatePicker(props ...Props) {
|
|||
</span>
|
||||
}
|
||||
<span class="text-muted-foreground flex items-center ml-2">
|
||||
@icon.Calendar(icon.Props{Size: 16})
|
||||
@icon.Calendar(icon.Props{Class: "size-4"})
|
||||
</span>
|
||||
}
|
||||
}
|
||||
@popover.Content(popover.ContentProps{
|
||||
ID: contentID,
|
||||
Placement: popover.PlacementBottomStart,
|
||||
Class: "p-0",
|
||||
}) {
|
||||
|
|
@ -149,23 +139,11 @@ templ DatePicker(props ...Props) {
|
|||
Class: "p-3",
|
||||
}) {
|
||||
@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
|
||||
LocaleTag: calendar.LocaleTag(p.LocaleTag),
|
||||
StartOfWeek: p.StartOfWeek,
|
||||
Value: valuePtr,
|
||||
HideHiddenInput: true,
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +11,6 @@ import (
|
|||
type contextKey string
|
||||
|
||||
const (
|
||||
instanceKey contextKey = "dialogInstance"
|
||||
openKey contextKey = "dialogOpen"
|
||||
)
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
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,81 +118,33 @@ 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... }
|
||||
>
|
||||
<div class="relative grid gap-4 p-6" data-tui-dialog-panel>
|
||||
{ children... }
|
||||
if !p.HideCloseButton {
|
||||
<button
|
||||
|
|
@ -231,7 +169,7 @@ templ Content(props ...ContentProps) {
|
|||
"[&_svg]:shrink-0",
|
||||
"[&_svg:not([class*='size-'])]:size-4",
|
||||
) }
|
||||
data-tui-dialog-close={ instanceID }
|
||||
data-tui-dialog-close
|
||||
aria-label="Close"
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -240,6 +178,7 @@ templ Content(props ...ContentProps) {
|
|||
</button>
|
||||
}
|
||||
</div>
|
||||
</dialog>
|
||||
}
|
||||
|
||||
templ Close(props ...CloseProps) {
|
||||
|
|
@ -251,10 +190,9 @@ templ Close(props ...CloseProps) {
|
|||
if p.ID != "" {
|
||||
id={ p.ID }
|
||||
}
|
||||
if p.For != "" {
|
||||
data-tui-dialog-close={ p.For }
|
||||
} else {
|
||||
data-tui-dialog-close
|
||||
if p.For != "" {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{{ var p Props }}
|
||||
if len(props) > 0 {
|
||||
p = props[0]
|
||||
{{ p = props[0] }}
|
||||
}
|
||||
contentID := p.ID
|
||||
if contentID == "" {
|
||||
contentID = utils.RandomID()
|
||||
}
|
||||
ctx = context.WithValue(ctx, contentIDKey, contentID)
|
||||
}}
|
||||
@popover.Root(popover.RootProps{
|
||||
ID: p.ID,
|
||||
}) {
|
||||
{ children... }
|
||||
}
|
||||
}
|
||||
|
||||
templ Trigger(props ...TriggerProps) {
|
||||
{{
|
||||
var p TriggerProps
|
||||
{{ var p TriggerProps }}
|
||||
if len(props) > 0 {
|
||||
p = props[0]
|
||||
{{ p = props[0] }}
|
||||
}
|
||||
contentID, ok := ctx.Value(contentIDKey).(string)
|
||||
if !ok {
|
||||
contentID = "fallback-content-id"
|
||||
}
|
||||
}}
|
||||
@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
|
||||
{{ var p SubProps }}
|
||||
if len(props) > 0 {
|
||||
p = 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... }
|
||||
>
|
||||
@popover.Root(popover.RootProps{
|
||||
ID: p.ID,
|
||||
Class: p.Class,
|
||||
Attributes: p.Attributes,
|
||||
}) {
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ SubTrigger(props ...SubTriggerProps) {
|
||||
{{
|
||||
var p SubTriggerProps
|
||||
{{ var p SubTriggerProps }}
|
||||
if len(props) > 0 {
|
||||
p = props[0]
|
||||
{{ p = props[0] }}
|
||||
}
|
||||
subContentID, ok := ctx.Value(subContentIDKey).(string)
|
||||
if !ok {
|
||||
subContentID = "fallback-subcontent-id"
|
||||
}
|
||||
}}
|
||||
@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
|
||||
{{ var p SubContentProps }}
|
||||
if len(props) > 0 {
|
||||
p = props[0]
|
||||
{{ p = props[0] }}
|
||||
}
|
||||
subContentID, ok := ctx.Value(subContentIDKey).(string)
|
||||
if !ok {
|
||||
subContentID = "fallback-subcontent-id"
|
||||
}
|
||||
}}
|
||||
@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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,11 +20,6 @@ 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
|
||||
}
|
||||
|
||||
|
|
@ -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
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -29,13 +29,19 @@ type TriggerType string
|
|||
const (
|
||||
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
|
||||
if p.ID != "" {
|
||||
id={ p.ID }
|
||||
data-tui-popover-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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
@popover.Root() {
|
||||
{ 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">
|
||||
{ children... }
|
||||
</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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
{{
|
||||
var p Props
|
||||
if len(props) > 0 {
|
||||
{{ p = 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,9 +81,18 @@ templ TagsInput(props ...Props) {
|
|||
}
|
||||
}
|
||||
</div>
|
||||
<!-- 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",
|
||||
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,
|
||||
|
|
@ -81,7 +102,44 @@ templ TagsInput(props ...Props) {
|
|||
p.Attributes,
|
||||
),
|
||||
})
|
||||
<div data-tui-tagsinput-hidden-inputs>
|
||||
</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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +69,8 @@ templ TimePicker(props ...Props) {
|
|||
maxTimeString = p.MaxTime.Format("15:04")
|
||||
}
|
||||
}}
|
||||
<div class="relative inline-block w-full">
|
||||
<div class="relative inline-block w-full" data-tui-timepicker-root>
|
||||
@popover.Root() {
|
||||
<input
|
||||
type="hidden"
|
||||
name={ p.Name }
|
||||
|
|
@ -78,10 +78,9 @@ templ TimePicker(props ...Props) {
|
|||
if p.Form != "" {
|
||||
form={ p.Form }
|
||||
}
|
||||
id={ p.ID + "-hidden" }
|
||||
data-tui-timepicker-hidden-input="true"
|
||||
/>
|
||||
@popover.Trigger(popover.TriggerProps{For: contentID}) {
|
||||
@popover.Trigger() {
|
||||
@button.Button(button.Props{
|
||||
ID: p.ID,
|
||||
Variant: button.VariantOutline,
|
||||
|
|
@ -118,12 +117,11 @@ templ TimePicker(props ...Props) {
|
|||
{ p.Placeholder }
|
||||
</span>
|
||||
<span class="text-muted-foreground flex items-center ml-2">
|
||||
@icon.Clock(icon.Props{Size: 16})
|
||||
@icon.Clock(icon.Props{Class: "size-4"})
|
||||
</span>
|
||||
}
|
||||
}
|
||||
@popover.Content(popover.ContentProps{
|
||||
ID: contentID,
|
||||
Placement: popover.PlacementBottomStart,
|
||||
Class: "p-0 w-80",
|
||||
}) {
|
||||
|
|
@ -136,7 +134,6 @@ templ TimePicker(props ...Props) {
|
|||
<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 }
|
||||
}
|
||||
|
|
@ -242,9 +239,15 @@ templ TimePicker(props ...Props) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,8 +54,18 @@ type ContentProps struct {
|
|||
}
|
||||
|
||||
templ Tooltip(props ...Props) {
|
||||
{{ 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) {
|
||||
{{ var p 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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue