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/repository/` — data access with sqlx, interface-based
|
||||||
- `internal/model/` — data structs with `db:` tags
|
- `internal/model/` — data structs with `db:` tags
|
||||||
- `internal/middleware/` — ordered chain: Config → Logging → NoCache → CSRF → Auth → URLPath
|
- `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/routes/routes.go` — all route definitions with middleware wrapping
|
||||||
- `internal/ui/` — templ templates organized as pages/, components/, layouts/, blocks/
|
- `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`
|
- `assets/` — static files (CSS, JS, fonts) embedded in binary via `go:embed`
|
||||||
|
|
||||||
## Key Patterns
|
## Key Patterns
|
||||||
|
|
@ -67,3 +69,78 @@ App reads from `.env` file via `godotenv`. Key vars: `APP_ENV`, `APP_URL`, `DB_D
|
||||||
## Database
|
## 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.
|
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
|
// Utility functions
|
||||||
function parseISODate(isoString) {
|
function parseISODate(isoString) {
|
||||||
if (!isoString) return null;
|
if (!isoString) return null;
|
||||||
|
|
@ -103,32 +78,45 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find elements
|
function findRoot(element) {
|
||||||
function findElements(trigger) {
|
return element?.closest("[data-tui-datepicker-root]") || null;
|
||||||
const calendarId = trigger.id + "-calendar-instance";
|
}
|
||||||
const calendar = document.getElementById(calendarId);
|
|
||||||
const hiddenInput =
|
function findElements(root) {
|
||||||
document.getElementById(trigger.id + "-hidden") ||
|
const trigger = root?.querySelector("[data-tui-datepicker='true']");
|
||||||
trigger.parentElement?.querySelector(
|
const calendar = root?.querySelector("[data-tui-calendar-container]");
|
||||||
|
const hiddenInput = root?.querySelector(
|
||||||
"[data-tui-datepicker-hidden-input]",
|
"[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
|
// Update display
|
||||||
function updateDisplay(trigger) {
|
function updateDisplay(root) {
|
||||||
const elements = findElements(trigger);
|
const elements = findElements(root);
|
||||||
if (!elements.display || !elements.hiddenInput) return;
|
if (!elements.trigger || !elements.display || !elements.hiddenInput) return;
|
||||||
|
|
||||||
const format =
|
const format =
|
||||||
trigger.getAttribute("data-tui-datepicker-display-format") ||
|
elements.trigger.getAttribute("data-tui-datepicker-display-format") ||
|
||||||
"locale-medium";
|
"locale-medium";
|
||||||
const locale =
|
const locale =
|
||||||
trigger.getAttribute("data-tui-datepicker-locale-tag") || "en-US";
|
elements.trigger.getAttribute("data-tui-datepicker-locale-tag") ||
|
||||||
|
"en-US";
|
||||||
const placeholder =
|
const placeholder =
|
||||||
trigger.getAttribute("data-tui-datepicker-placeholder") ||
|
elements.trigger.getAttribute("data-tui-datepicker-placeholder") ||
|
||||||
"Select a date";
|
"Select a date";
|
||||||
|
|
||||||
if (elements.hiddenInput.value) {
|
if (elements.hiddenInput.value) {
|
||||||
|
|
@ -144,43 +132,30 @@
|
||||||
elements.display.classList.add("text-muted-foreground");
|
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
|
// Handle calendar date selection
|
||||||
document.addEventListener("calendar-date-selected", (e) => {
|
document.addEventListener("calendar-date-selected", (e) => {
|
||||||
// Find the datepicker trigger associated with this calendar
|
|
||||||
const calendar = e.target;
|
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", "");
|
elements.hiddenInput.value = toISODate(e.detail.date);
|
||||||
const trigger = document.getElementById(triggerId);
|
updateDisplay(root);
|
||||||
if (!trigger || !trigger.hasAttribute("data-tui-datepicker")) return;
|
closePopover(root);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle hidden input value changes (for reactive frameworks)
|
// Handle hidden input value changes (for reactive frameworks)
|
||||||
document.addEventListener("input", (e) => {
|
document.addEventListener("input", (e) => {
|
||||||
if (!e.target.matches("[data-tui-datepicker-hidden-input]")) return;
|
if (!e.target.matches("[data-tui-datepicker-hidden-input]")) return;
|
||||||
|
|
||||||
const trigger = document.getElementById(e.target.id.replace("-hidden", ""));
|
const root = findRoot(e.target);
|
||||||
if (trigger) {
|
if (root) {
|
||||||
updateDisplay(trigger);
|
updateDisplay(root);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -188,51 +163,24 @@
|
||||||
document.addEventListener("reset", (e) => {
|
document.addEventListener("reset", (e) => {
|
||||||
if (!e.target.matches("form")) return;
|
if (!e.target.matches("form")) return;
|
||||||
|
|
||||||
e.target
|
e.target.querySelectorAll("[data-tui-datepicker-root]").forEach((root) => {
|
||||||
.querySelectorAll('[data-tui-datepicker="true"]')
|
const elements = findElements(root);
|
||||||
.forEach((trigger) => {
|
|
||||||
const elements = findElements(trigger);
|
|
||||||
if (elements.hiddenInput) {
|
if (elements.hiddenInput) {
|
||||||
elements.hiddenInput.value = "";
|
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
|
// Initialize datepickers
|
||||||
function initializeDatePickers() {
|
function initializeDatePickers() {
|
||||||
document
|
document.querySelectorAll("[data-tui-datepicker-root]").forEach((root) => {
|
||||||
.querySelectorAll('[data-tui-datepicker="true"]')
|
const elements = findElements(root);
|
||||||
.forEach((trigger) => {
|
|
||||||
const elements = findElements(trigger);
|
|
||||||
if (!elements.hiddenInput || elements.hiddenInput._tui) return;
|
if (!elements.hiddenInput || elements.hiddenInput._tui) return;
|
||||||
|
|
||||||
// Enable reactive binding for hidden input
|
// Enable reactive binding for hidden input
|
||||||
enableReactiveBinding(elements.hiddenInput);
|
enableReactiveBinding(elements.hiddenInput);
|
||||||
// If required, set default date to today
|
updateDisplay(root);
|
||||||
if (trigger.dataset.tuiDatepickerRequired === "true") {
|
|
||||||
setDefaultToday(elements.hiddenInput);
|
|
||||||
}
|
|
||||||
updateDisplay(trigger);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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=`
|
(()=>{(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()}}})})();})();
|
||||||
<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="")})})})();})();
|
|
||||||
|
|
|
||||||
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",
|
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json",
|
||||||
"packages": [
|
"packages": [
|
||||||
"go@1.25.5",
|
"go@1.25.5",
|
||||||
"templ@0.3.960",
|
|
||||||
"go-task@3.45.5",
|
"go-task@3.45.5",
|
||||||
"tailwindcss_4@4.2.1",
|
"tailwindcss_4@4.2.1",
|
||||||
"nodejs@24",
|
"nodejs@24",
|
||||||
"goose@latest"
|
"goose@latest",
|
||||||
|
"templ@0.3.1001"
|
||||||
],
|
],
|
||||||
"shell": {
|
"shell": {
|
||||||
"init_hook": [
|
"init_hook": [
|
||||||
|
|
|
||||||
24
devbox.lock
24
devbox.lock
|
|
@ -278,51 +278,51 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"templ@0.3.960": {
|
"templ@0.3.1001": {
|
||||||
"last_modified": "2025-12-31T03:27:36Z",
|
"last_modified": "2026-03-21T07:29:51Z",
|
||||||
"resolved": "github:NixOS/nixpkgs/f665af0cdb70ed27e1bd8f9fdfecaf451260fc55#templ",
|
"resolved": "github:NixOS/nixpkgs/09061f748ee21f68a089cd5d91ec1859cd93d0be#templ",
|
||||||
"source": "devbox-search",
|
"source": "devbox-search",
|
||||||
"version": "0.3.960",
|
"version": "0.3.1001",
|
||||||
"systems": {
|
"systems": {
|
||||||
"aarch64-darwin": {
|
"aarch64-darwin": {
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "out",
|
"name": "out",
|
||||||
"path": "/nix/store/g2b8z0ssdj1qxfkcvw4h47rzhdr91wwf-templ-0.3.960",
|
"path": "/nix/store/x9y3fnv2mb8j70r8k0i3n96qflrqq8d5-templ-0.3.1001",
|
||||||
"default": true
|
"default": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"store_path": "/nix/store/g2b8z0ssdj1qxfkcvw4h47rzhdr91wwf-templ-0.3.960"
|
"store_path": "/nix/store/x9y3fnv2mb8j70r8k0i3n96qflrqq8d5-templ-0.3.1001"
|
||||||
},
|
},
|
||||||
"aarch64-linux": {
|
"aarch64-linux": {
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "out",
|
"name": "out",
|
||||||
"path": "/nix/store/ws4nkq75id3wp1i741zzjqs6casbd0cw-templ-0.3.960",
|
"path": "/nix/store/3nhjivm3l1bk9gc98qrmafdk5n18lrdd-templ-0.3.1001",
|
||||||
"default": true
|
"default": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"store_path": "/nix/store/ws4nkq75id3wp1i741zzjqs6casbd0cw-templ-0.3.960"
|
"store_path": "/nix/store/3nhjivm3l1bk9gc98qrmafdk5n18lrdd-templ-0.3.1001"
|
||||||
},
|
},
|
||||||
"x86_64-darwin": {
|
"x86_64-darwin": {
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "out",
|
"name": "out",
|
||||||
"path": "/nix/store/a1a7qklai2fjn6ika5a8q25mapi5xz77-templ-0.3.960",
|
"path": "/nix/store/7wjjyfwhkhavm8xyba699w3r1rhrkyci-templ-0.3.1001",
|
||||||
"default": true
|
"default": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"store_path": "/nix/store/a1a7qklai2fjn6ika5a8q25mapi5xz77-templ-0.3.960"
|
"store_path": "/nix/store/7wjjyfwhkhavm8xyba699w3r1rhrkyci-templ-0.3.1001"
|
||||||
},
|
},
|
||||||
"x86_64-linux": {
|
"x86_64-linux": {
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "out",
|
"name": "out",
|
||||||
"path": "/nix/store/033s4gq6nqywzxhzd8gr9l05xcvln4ld-templ-0.3.960",
|
"path": "/nix/store/v4qj3mml7lyps2nicavbys97w56dxvjy-templ-0.3.1001",
|
||||||
"default": true
|
"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 (
|
require (
|
||||||
github.com/Oudwins/tailwind-merge-go v0.2.1
|
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/alexedwards/argon2id v1.0.0
|
||||||
github.com/emersion/go-imap v1.2.1
|
github.com/emersion/go-imap v1.2.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
github.com/sethvargo/go-retry v0.3.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/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect
|
||||||
github.com/vertica/vertica-sql-go v1.3.3 // indirect
|
github.com/vertica/vertica-sql-go v1.3.3 // indirect
|
||||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 // 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/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 h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
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.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY=
|
||||||
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
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 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
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/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.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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.9.5 h1:qJyELi+xHCVYNgheAGkRP9EKjsufJgfpQOD+Cb4+Y+M=
|
||||||
github.com/templui/templui v1.0.0/go.mod h1:SnKmOIs7t/ngsdWUws97CVodbz89ne9kQv3ivgdhiHo=
|
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/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 h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
|
||||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
|
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",
|
"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
|
// 📚 Documentation: https://templui.io/docs/components/accordion
|
||||||
package accordion
|
package accordion
|
||||||
|
|
||||||
|
|
@ -97,10 +97,7 @@ templ Trigger(props ...TriggerProps) {
|
||||||
{ p.Attributes... }
|
{ p.Attributes... }
|
||||||
>
|
>
|
||||||
{ children... }
|
{ children... }
|
||||||
@icon.ChevronDown(icon.Props{
|
@icon.ChevronDown(icon.Props{Class: "size-4 shrink-0 translate-y-0.5 transition-transform duration-200 text-muted-foreground pointer-events-none"})
|
||||||
Size: 16,
|
|
||||||
Class: "size-4 shrink-0 translate-y-0.5 transition-transform duration-200 text-muted-foreground pointer-events-none",
|
|
||||||
})
|
|
||||||
</summary>
|
</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
|
// 📚 Documentation: https://templui.io/docs/components/alert
|
||||||
package 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
|
// 📚 Documentation: https://templui.io/docs/components/aspect-ratio
|
||||||
package aspectratio
|
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
|
// 📚 Documentation: https://templui.io/docs/components/avatar
|
||||||
package avatar
|
package avatar
|
||||||
|
|
||||||
|
|
@ -92,6 +92,10 @@ templ Fallback(props ...FallbackProps) {
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/badge
|
||||||
package 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
|
// 📚 Documentation: https://templui.io/docs/components/breadcrumb
|
||||||
package breadcrumb
|
package breadcrumb
|
||||||
|
|
||||||
|
|
@ -148,7 +148,7 @@ templ Separator(props ...SeparatorProps) {
|
||||||
if p.UseCustom {
|
if p.UseCustom {
|
||||||
{ children... }
|
{ children... }
|
||||||
} else {
|
} else {
|
||||||
@icon.ChevronRight(icon.Props{Size: 14, Class: "text-muted-foreground"})
|
@icon.ChevronRight(icon.Props{Class: "size-3.5 text-muted-foreground"})
|
||||||
}
|
}
|
||||||
</span>
|
</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
|
// 📚 Documentation: https://templui.io/docs/components/button
|
||||||
package 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
|
// 📚 Documentation: https://templui.io/docs/components/calendar
|
||||||
package 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.
|
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.
|
InitialYear int // Optional: (Default: current or from Value). Controls the initially displayed year view.
|
||||||
StartOfWeek *Day // Optional: 0-6 [Sun-Sat] (Default: 1).
|
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) {
|
templ Calendar(props ...Props) {
|
||||||
|
|
@ -62,13 +62,6 @@ templ Calendar(props ...Props) {
|
||||||
if p.LocaleTag == "" {
|
if p.LocaleTag == "" {
|
||||||
p.LocaleTag = LocaleDefaultTag
|
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
|
initialStartOfWeek := Monday
|
||||||
if p.StartOfWeek != nil {
|
if p.StartOfWeek != nil {
|
||||||
|
|
@ -106,8 +99,12 @@ templ Calendar(props ...Props) {
|
||||||
// Generate short month names (English only, JS will update with localized)
|
// 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"}
|
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">
|
<div
|
||||||
if p.RenderHiddenInput {
|
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
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name={ p.Name }
|
name={ p.Name }
|
||||||
|
|
@ -118,6 +115,7 @@ templ Calendar(props ...Props) {
|
||||||
}
|
}
|
||||||
<div
|
<div
|
||||||
id={ p.ID }
|
id={ p.ID }
|
||||||
|
class="inline-flex flex-col"
|
||||||
data-tui-calendar-container="true"
|
data-tui-calendar-container="true"
|
||||||
data-tui-calendar-locale-tag={ string(p.LocaleTag) }
|
data-tui-calendar-locale-tag={ string(p.LocaleTag) }
|
||||||
data-tui-calendar-initial-month={ strconv.Itoa(initialMonth) }
|
data-tui-calendar-initial-month={ strconv.Itoa(initialMonth) }
|
||||||
|
|
@ -126,7 +124,7 @@ templ Calendar(props ...Props) {
|
||||||
data-tui-calendar-start-of-week={ int(initialStartOfWeek) }
|
data-tui-calendar-start-of-week={ int(initialStartOfWeek) }
|
||||||
>
|
>
|
||||||
<!-- Calendar Header -->
|
<!-- Calendar Header -->
|
||||||
<div class="flex items-center gap-2 mb-4">
|
<div class="flex w-full items-center gap-2 mb-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-tui-calendar-prev
|
data-tui-calendar-prev
|
||||||
|
|
@ -152,7 +150,7 @@ templ Calendar(props ...Props) {
|
||||||
</select>
|
</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 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>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Year Select -->
|
<!-- Year Select -->
|
||||||
|
|
@ -172,7 +170,7 @@ templ Calendar(props ...Props) {
|
||||||
</select>
|
</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 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>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -185,13 +183,17 @@ templ Calendar(props ...Props) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Weekday Headers -->
|
<!-- 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 -->
|
<!-- 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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/card
|
||||||
package 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
|
// 📚 Documentation: https://templui.io/docs/components/carousel
|
||||||
package carousel
|
package carousel
|
||||||
|
|
||||||
|
|
@ -206,6 +206,10 @@ templ Indicators(props ...IndicatorsProps) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/charts
|
||||||
package chart
|
package chart
|
||||||
|
|
||||||
|
|
@ -53,11 +53,17 @@ type Config struct {
|
||||||
BeginAtZero *bool `json:"beginAtZero,omitempty"`
|
BeginAtZero *bool `json:"beginAtZero,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScriptConfig struct {
|
||||||
|
RawConfig map[string]any `json:"rawConfig,omitempty"`
|
||||||
|
GeneratedConfig *Config `json:"generatedConfig,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type Props struct {
|
type Props struct {
|
||||||
ID string
|
ID string
|
||||||
Variant Variant
|
Variant Variant
|
||||||
Data Data
|
Data Data
|
||||||
Options Options
|
Options Options
|
||||||
|
RawConfig map[string]any
|
||||||
ShowLegend bool
|
ShowLegend bool
|
||||||
ShowXAxis bool
|
ShowXAxis bool
|
||||||
ShowYAxis bool
|
ShowYAxis bool
|
||||||
|
|
@ -96,7 +102,11 @@ templ Chart(props ...Props) {
|
||||||
<canvas id={ canvasId } data-tui-chart-id={ dataId }></canvas>
|
<canvas id={ canvasId } data-tui-chart-id={ dataId }></canvas>
|
||||||
</div>
|
</div>
|
||||||
{{
|
{{
|
||||||
chartConfig := Config{
|
scriptConfig := ScriptConfig{
|
||||||
|
RawConfig: p.RawConfig,
|
||||||
|
}
|
||||||
|
if p.RawConfig == nil {
|
||||||
|
generatedConfig := Config{
|
||||||
Type: p.Variant,
|
Type: p.Variant,
|
||||||
Data: p.Data,
|
Data: p.Data,
|
||||||
Options: p.Options,
|
Options: p.Options,
|
||||||
|
|
@ -113,10 +123,16 @@ templ Chart(props ...Props) {
|
||||||
YMax: p.YMax,
|
YMax: p.YMax,
|
||||||
BeginAtZero: p.BeginAtZero,
|
BeginAtZero: p.BeginAtZero,
|
||||||
}
|
}
|
||||||
|
scriptConfig.GeneratedConfig = &generatedConfig
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
@templ.JSONScript(dataId, chartConfig)
|
@templ.JSONScript(dataId, scriptConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/checkbox
|
||||||
package checkbox
|
package checkbox
|
||||||
|
|
||||||
|
|
@ -71,17 +71,21 @@ templ Checkbox(props ...Props) {
|
||||||
if p.Icon != nil {
|
if p.Icon != nil {
|
||||||
@p.Icon
|
@p.Icon
|
||||||
} else {
|
} else {
|
||||||
@icon.Check(icon.Props{Size: 14})
|
@icon.Check(icon.Props{Class: "size-3.5"})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 pointer-events-none flex items-center justify-center text-primary-foreground opacity-0 peer-indeterminate:opacity-100"
|
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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/collapsible
|
||||||
package collapsible
|
package collapsible
|
||||||
|
|
||||||
|
|
@ -81,6 +81,10 @@ templ Content(props ...ContentProps) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/copy-button
|
||||||
package copybutton
|
package copybutton
|
||||||
|
|
||||||
|
|
@ -34,15 +34,19 @@ templ CopyButton(props Props) {
|
||||||
Type: button.TypeButton,
|
Type: button.TypeButton,
|
||||||
}) {
|
}) {
|
||||||
<span data-copy-icon-clipboard>
|
<span data-copy-icon-clipboard>
|
||||||
@icon.Clipboard(icon.Props{Size: 16})
|
@icon.Clipboard(icon.Props{Class: "size-4"})
|
||||||
</span>
|
</span>
|
||||||
<span data-copy-icon-check class="hidden">
|
<span data-copy-icon-check class="hidden">
|
||||||
@icon.Check(icon.Props{Size: 16})
|
@icon.Check(icon.Props{Class: "size-4"})
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/date-picker
|
||||||
package datepicker
|
package datepicker
|
||||||
|
|
||||||
|
|
@ -47,8 +47,6 @@ type Props struct {
|
||||||
Placeholder string
|
Placeholder string
|
||||||
Disabled bool
|
Disabled bool
|
||||||
HasError bool
|
HasError bool
|
||||||
Required bool
|
|
||||||
Clearable bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
templ DatePicker(props ...Props) {
|
templ DatePicker(props ...Props) {
|
||||||
|
|
@ -73,20 +71,15 @@ templ DatePicker(props ...Props) {
|
||||||
p.Format = FormatLOCALE_MEDIUM
|
p.Format = FormatLOCALE_MEDIUM
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentID = p.ID + "-content"
|
|
||||||
var valuePtr *time.Time
|
var valuePtr *time.Time
|
||||||
var initialSelectedISO string
|
var initialSelectedISO string
|
||||||
if !p.Value.IsZero() {
|
if !p.Value.IsZero() {
|
||||||
valuePtr = &p.Value
|
valuePtr = &p.Value
|
||||||
initialSelectedISO = p.Value.Format("2006-01-02")
|
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
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name={ p.Name }
|
name={ p.Name }
|
||||||
|
|
@ -94,10 +87,9 @@ templ DatePicker(props ...Props) {
|
||||||
if p.Form != "" {
|
if p.Form != "" {
|
||||||
form={ p.Form }
|
form={ p.Form }
|
||||||
}
|
}
|
||||||
id={ p.ID + "-hidden" }
|
|
||||||
data-tui-datepicker-hidden-input
|
data-tui-datepicker-hidden-input
|
||||||
/>
|
/>
|
||||||
@popover.Trigger(popover.TriggerProps{For: contentID}) {
|
@popover.Trigger() {
|
||||||
@button.Button(button.Props{
|
@button.Button(button.Props{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
Variant: button.VariantOutline,
|
Variant: button.VariantOutline,
|
||||||
|
|
@ -123,7 +115,6 @@ templ DatePicker(props ...Props) {
|
||||||
"data-tui-datepicker-display-format": string(p.Format),
|
"data-tui-datepicker-display-format": string(p.Format),
|
||||||
"data-tui-datepicker-locale-tag": string(p.LocaleTag),
|
"data-tui-datepicker-locale-tag": string(p.LocaleTag),
|
||||||
"data-tui-datepicker-placeholder": p.Placeholder,
|
"data-tui-datepicker-placeholder": p.Placeholder,
|
||||||
"data-tui-datepicker-required": required,
|
|
||||||
"aria-invalid": utils.If(p.HasError, "true"),
|
"aria-invalid": utils.If(p.HasError, "true"),
|
||||||
}),
|
}),
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -133,12 +124,11 @@ templ DatePicker(props ...Props) {
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
<span class="text-muted-foreground flex items-center ml-2">
|
<span class="text-muted-foreground flex items-center ml-2">
|
||||||
@icon.Calendar(icon.Props{Size: 16})
|
@icon.Calendar(icon.Props{Class: "size-4"})
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@popover.Content(popover.ContentProps{
|
@popover.Content(popover.ContentProps{
|
||||||
ID: contentID,
|
|
||||||
Placement: popover.PlacementBottomStart,
|
Placement: popover.PlacementBottomStart,
|
||||||
Class: "p-0",
|
Class: "p-0",
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -149,23 +139,11 @@ templ DatePicker(props ...Props) {
|
||||||
Class: "p-3",
|
Class: "p-3",
|
||||||
}) {
|
}) {
|
||||||
@calendar.Calendar(calendar.Props{
|
@calendar.Calendar(calendar.Props{
|
||||||
ID: p.ID + "-calendar-instance", // Pass ID for calendar instance
|
LocaleTag: calendar.LocaleTag(p.LocaleTag),
|
||||||
LocaleTag: calendar.LocaleTag(p.LocaleTag), // Pass locale tag to calendar
|
StartOfWeek: p.StartOfWeek,
|
||||||
StartOfWeek: p.StartOfWeek, // Pass start of week to calendar
|
Value: valuePtr,
|
||||||
Value: valuePtr, // Pass pointer to value
|
HideHiddenInput: true,
|
||||||
RenderHiddenInput: false, // Don't render hidden input inside popover
|
|
||||||
})
|
})
|
||||||
if p.Clearable {
|
|
||||||
@button.Button(button.Props{
|
|
||||||
ID: p.ID + "-clear-button",
|
|
||||||
Class: "mt-4 w-full",
|
|
||||||
Variant: button.VariantOutline,
|
|
||||||
Attributes: templ.Attributes{
|
|
||||||
"data-tui-datepicker-clear": "true",
|
|
||||||
},
|
|
||||||
}) {
|
|
||||||
Clear
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,6 +151,12 @@ templ DatePicker(props ...Props) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/dialog
|
||||||
package dialog
|
package dialog
|
||||||
|
|
||||||
|
|
@ -11,7 +11,6 @@ import (
|
||||||
type contextKey string
|
type contextKey string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
instanceKey contextKey = "dialogInstance"
|
|
||||||
openKey contextKey = "dialogOpen"
|
openKey contextKey = "dialogOpen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -28,23 +27,20 @@ type TriggerProps struct {
|
||||||
ID string
|
ID string
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
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 {
|
type ContentProps struct {
|
||||||
ID string
|
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
Attributes templ.Attributes
|
||||||
HideCloseButton bool
|
HideCloseButton bool
|
||||||
Open bool // Initial open state for standalone usage (when no context)
|
|
||||||
DisableAutoFocus bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CloseProps struct {
|
type CloseProps struct {
|
||||||
ID string
|
ID string
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
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 {
|
type HeaderProps struct {
|
||||||
|
|
@ -76,25 +72,20 @@ templ Dialog(props ...Props) {
|
||||||
if len(props) > 0 {
|
if len(props) > 0 {
|
||||||
{{ p = 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) }}
|
{{ ctx = context.WithValue(ctx, openKey, p.Open) }}
|
||||||
<div
|
<div
|
||||||
if p.ID != "" {
|
if p.ID != "" {
|
||||||
id={ p.ID }
|
id={ p.ID }
|
||||||
}
|
}
|
||||||
data-tui-dialog
|
data-tui-dialog
|
||||||
data-dialog-instance={ instanceID }
|
|
||||||
if p.DisableClickAway {
|
if p.DisableClickAway {
|
||||||
data-tui-dialog-disable-click-away="true"
|
data-tui-dialog-disable-click-away="true"
|
||||||
}
|
}
|
||||||
if p.DisableESC {
|
if p.DisableESC {
|
||||||
data-tui-dialog-disable-esc="true"
|
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... }
|
{ p.Attributes... }
|
||||||
>
|
>
|
||||||
{ children... }
|
{ children... }
|
||||||
|
|
@ -106,19 +97,14 @@ templ Trigger(props ...TriggerProps) {
|
||||||
if len(props) > 0 {
|
if len(props) > 0 {
|
||||||
{{ p = 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
|
<span
|
||||||
if p.ID != "" {
|
if p.ID != "" {
|
||||||
id={ p.ID }
|
id={ p.ID }
|
||||||
}
|
}
|
||||||
data-tui-dialog-trigger={ instanceID }
|
data-tui-dialog-trigger
|
||||||
data-dialog-instance={ instanceID }
|
if p.For != "" {
|
||||||
|
data-tui-dialog-target={ p.For }
|
||||||
|
}
|
||||||
data-tui-dialog-trigger-open="false"
|
data-tui-dialog-trigger-open="false"
|
||||||
class={ utils.TwMerge("contents", p.Class) }
|
class={ utils.TwMerge("contents", p.Class) }
|
||||||
{ p.Attributes... }
|
{ p.Attributes... }
|
||||||
|
|
@ -132,81 +118,33 @@ templ Content(props ...ContentProps) {
|
||||||
if len(props) > 0 {
|
if len(props) > 0 {
|
||||||
{{ p = props[0] }}
|
{{ p = props[0] }}
|
||||||
}
|
}
|
||||||
// Start with prop values as defaults
|
{{ open := false }}
|
||||||
{{ instanceID := p.ID }}
|
|
||||||
{{ open := p.Open }}
|
|
||||||
// Override with context values if available
|
|
||||||
if val := ctx.Value(instanceKey); val != nil {
|
|
||||||
{{ instanceID = val.(string) }}
|
|
||||||
}
|
|
||||||
if val := ctx.Value(openKey); val != nil {
|
if val := ctx.Value(openKey); val != nil {
|
||||||
{{ open = val.(bool) }}
|
{{ open = val.(bool) }}
|
||||||
}
|
}
|
||||||
// Apply defaults if still empty
|
<dialog
|
||||||
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
|
|
||||||
class={
|
class={
|
||||||
utils.TwMerge(
|
utils.TwMerge(
|
||||||
// Base positioning
|
"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",
|
||||||
"fixed z-50 left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]",
|
"[&:not([open]):not([data-tui-dialog-closing=true])]:hidden",
|
||||||
// Style
|
"rounded-lg",
|
||||||
"bg-background rounded-lg border shadow-lg",
|
"[&::backdrop]:transition-all [&::backdrop]:duration-200",
|
||||||
// Layout
|
"data-[tui-dialog-open=false]:[&::backdrop]:bg-black/0",
|
||||||
"grid gap-4 p-6",
|
"data-[tui-dialog-open=true]:[&::backdrop]:bg-black/50",
|
||||||
// Size
|
|
||||||
"w-full max-w-[calc(100%-2rem)] sm:max-w-lg",
|
|
||||||
// Transitions
|
|
||||||
"transition-all duration-200",
|
"transition-all duration-200",
|
||||||
// Scale animation
|
|
||||||
"data-[tui-dialog-open=false]:scale-95",
|
"data-[tui-dialog-open=false]:scale-95",
|
||||||
"data-[tui-dialog-open=true]:scale-100",
|
"data-[tui-dialog-open=true]:scale-100",
|
||||||
// Opacity
|
|
||||||
"data-[tui-dialog-open=false]:opacity-0",
|
"data-[tui-dialog-open=false]:opacity-0",
|
||||||
"data-[tui-dialog-open=true]:opacity-100",
|
"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,
|
p.Class,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
data-tui-dialog-content
|
data-tui-dialog-content
|
||||||
data-dialog-instance={ instanceID }
|
data-tui-dialog-open={ utils.IfElse(open, "true", "false") }
|
||||||
if p.DisableAutoFocus {
|
data-tui-dialog-initial-open={ utils.IfElse(open, "true", "false") }
|
||||||
data-tui-dialog-disable-autofocus="true"
|
|
||||||
}
|
|
||||||
if open {
|
|
||||||
data-tui-dialog-open="true"
|
|
||||||
} else {
|
|
||||||
data-tui-dialog-open="false"
|
|
||||||
data-tui-dialog-hidden="true"
|
|
||||||
}
|
|
||||||
{ p.Attributes... }
|
{ p.Attributes... }
|
||||||
>
|
>
|
||||||
|
<div class="relative grid gap-4 p-6" data-tui-dialog-panel>
|
||||||
{ children... }
|
{ children... }
|
||||||
if !p.HideCloseButton {
|
if !p.HideCloseButton {
|
||||||
<button
|
<button
|
||||||
|
|
@ -231,7 +169,7 @@ templ Content(props ...ContentProps) {
|
||||||
"[&_svg]:shrink-0",
|
"[&_svg]:shrink-0",
|
||||||
"[&_svg:not([class*='size-'])]:size-4",
|
"[&_svg:not([class*='size-'])]:size-4",
|
||||||
) }
|
) }
|
||||||
data-tui-dialog-close={ instanceID }
|
data-tui-dialog-close
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
@ -240,6 +178,7 @@ templ Content(props ...ContentProps) {
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</dialog>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Close(props ...CloseProps) {
|
templ Close(props ...CloseProps) {
|
||||||
|
|
@ -251,10 +190,9 @@ templ Close(props ...CloseProps) {
|
||||||
if p.ID != "" {
|
if p.ID != "" {
|
||||||
id={ p.ID }
|
id={ p.ID }
|
||||||
}
|
}
|
||||||
if p.For != "" {
|
|
||||||
data-tui-dialog-close={ p.For }
|
|
||||||
} else {
|
|
||||||
data-tui-dialog-close
|
data-tui-dialog-close
|
||||||
|
if p.For != "" {
|
||||||
|
data-tui-dialog-target={ p.For }
|
||||||
}
|
}
|
||||||
class={ utils.TwMerge("contents cursor-pointer", p.Class) }
|
class={ utils.TwMerge("contents cursor-pointer", p.Class) }
|
||||||
{ p.Attributes... }
|
{ p.Attributes... }
|
||||||
|
|
@ -327,6 +265,10 @@ templ Description(props ...DescriptionProps) {
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/dropdown
|
||||||
package dropdown
|
package dropdown
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/popover"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/popover"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -25,25 +24,16 @@ const (
|
||||||
PlacementLeftEnd = popover.PlacementLeftEnd
|
PlacementLeftEnd = popover.PlacementLeftEnd
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey string
|
|
||||||
|
|
||||||
var (
|
|
||||||
contentIDKey contextKey = "contentID"
|
|
||||||
subContentIDKey contextKey = "subContentID"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Props struct {
|
type Props struct {
|
||||||
ID string
|
ID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TriggerProps struct {
|
type TriggerProps struct {
|
||||||
ID string
|
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
Attributes templ.Attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentProps struct {
|
type ContentProps struct {
|
||||||
ID string
|
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
Attributes templ.Attributes
|
||||||
Placement Placement
|
Placement Placement
|
||||||
|
|
@ -90,13 +80,11 @@ type SubProps struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubTriggerProps struct {
|
type SubTriggerProps struct {
|
||||||
ID string
|
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
Attributes templ.Attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubContentProps struct {
|
type SubContentProps struct {
|
||||||
ID string
|
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
Attributes templ.Attributes
|
||||||
}
|
}
|
||||||
|
|
@ -108,36 +96,25 @@ type PortalProps struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Dropdown(props ...Props) {
|
templ Dropdown(props ...Props) {
|
||||||
{{
|
{{ var p Props }}
|
||||||
var p Props
|
|
||||||
if len(props) > 0 {
|
if len(props) > 0 {
|
||||||
p = props[0]
|
{{ p = props[0] }}
|
||||||
}
|
}
|
||||||
contentID := p.ID
|
@popover.Root(popover.RootProps{
|
||||||
if contentID == "" {
|
ID: p.ID,
|
||||||
contentID = utils.RandomID()
|
}) {
|
||||||
}
|
|
||||||
ctx = context.WithValue(ctx, contentIDKey, contentID)
|
|
||||||
}}
|
|
||||||
{ children... }
|
{ children... }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Trigger(props ...TriggerProps) {
|
templ Trigger(props ...TriggerProps) {
|
||||||
{{
|
{{ var p TriggerProps }}
|
||||||
var p TriggerProps
|
|
||||||
if len(props) > 0 {
|
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{
|
@popover.Trigger(popover.TriggerProps{
|
||||||
ID: p.ID,
|
|
||||||
Class: p.Class,
|
Class: p.Class,
|
||||||
Attributes: p.Attributes,
|
Attributes: p.Attributes,
|
||||||
For: contentID,
|
|
||||||
TriggerType: popover.TriggerTypeClick,
|
TriggerType: popover.TriggerTypeClick,
|
||||||
}) {
|
}) {
|
||||||
{ children... }
|
{ children... }
|
||||||
|
|
@ -149,10 +126,6 @@ templ Content(props ...ContentProps) {
|
||||||
if len(props) > 0 {
|
if len(props) > 0 {
|
||||||
{{ p = 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
|
placement := p.Placement
|
||||||
if placement == "" {
|
if placement == "" {
|
||||||
|
|
@ -160,7 +133,6 @@ templ Content(props ...ContentProps) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@popover.Content(popover.ContentProps{
|
@popover.Content(popover.ContentProps{
|
||||||
ID: contentID,
|
|
||||||
Placement: placement,
|
Placement: placement,
|
||||||
Class: utils.TwMerge(
|
Class: utils.TwMerge(
|
||||||
"z-50 rounded-md bg-popover p-1 shadow-md focus:outline-none overflow-auto",
|
"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) {
|
templ Group(props ...GroupProps) {
|
||||||
{{ var p GroupProps }}
|
{{ var p GroupProps }}
|
||||||
if len(props) > 0 {
|
if len(props) > 0 {
|
||||||
|
|
@ -298,43 +279,25 @@ templ Shortcut(props ...ShortcutProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Sub(props ...SubProps) {
|
templ Sub(props ...SubProps) {
|
||||||
{{
|
{{ var p SubProps }}
|
||||||
var p SubProps
|
|
||||||
if len(props) > 0 {
|
if len(props) > 0 {
|
||||||
p = props[0]
|
{{ p = props[0] }}
|
||||||
}
|
}
|
||||||
subContentID := p.ID
|
@popover.Root(popover.RootProps{
|
||||||
if subContentID == "" {
|
ID: p.ID,
|
||||||
subContentID = utils.RandomID()
|
Class: p.Class,
|
||||||
}
|
Attributes: p.Attributes,
|
||||||
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... }
|
|
||||||
>
|
|
||||||
{ children... }
|
{ children... }
|
||||||
</div>
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ SubTrigger(props ...SubTriggerProps) {
|
templ SubTrigger(props ...SubTriggerProps) {
|
||||||
{{
|
{{ var p SubTriggerProps }}
|
||||||
var p SubTriggerProps
|
|
||||||
if len(props) > 0 {
|
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{
|
@popover.Trigger(popover.TriggerProps{
|
||||||
ID: p.ID,
|
|
||||||
For: subContentID,
|
|
||||||
TriggerType: popover.TriggerTypeHover,
|
TriggerType: popover.TriggerTypeHover,
|
||||||
}) {
|
}) {
|
||||||
<button
|
<button
|
||||||
|
|
@ -360,18 +323,11 @@ templ SubTrigger(props ...SubTriggerProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
templ SubContent(props ...SubContentProps) {
|
templ SubContent(props ...SubContentProps) {
|
||||||
{{
|
{{ var p SubContentProps }}
|
||||||
var p SubContentProps
|
|
||||||
if len(props) > 0 {
|
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{
|
@popover.Content(popover.ContentProps{
|
||||||
ID: subContentID,
|
|
||||||
Placement: popover.PlacementRightStart,
|
Placement: popover.PlacementRightStart,
|
||||||
Offset: -4, // Adjust as needed
|
Offset: -4, // Adjust as needed
|
||||||
HoverDelay: 100, // ms
|
HoverDelay: 100, // ms
|
||||||
|
|
@ -385,7 +341,3 @@ templ SubContent(props ...SubContentProps) {
|
||||||
{ children... }
|
{ 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
|
// 📚 Documentation: https://templui.io/docs/components/form
|
||||||
package 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
|
// 📚 Documentation: https://templui.io/docs/components/icon
|
||||||
package icon
|
package icon
|
||||||
|
|
||||||
|
|
@ -20,11 +20,6 @@ var (
|
||||||
|
|
||||||
// Props defines the properties that can be set for an icon.
|
// Props defines the properties that can be set for an icon.
|
||||||
type Props struct {
|
type Props struct {
|
||||||
Size int
|
|
||||||
Color string
|
|
||||||
Fill string
|
|
||||||
Stroke string
|
|
||||||
StrokeWidth string // Stroke Width of Icon, Usage: "2.5"
|
|
||||||
Class string
|
Class string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,10 +31,8 @@ func Icon(name string) func(...Props) templ.Component {
|
||||||
p = props[0]
|
p = props[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a unique key for the cache based on icon name and all relevant props.
|
// Cache by icon name and class so repeated renders reuse the generated SVG.
|
||||||
// This ensures different stylings of the same icon are cached separately.
|
cacheKey := fmt.Sprintf("%s|cl:%s", name, p.Class)
|
||||||
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)
|
|
||||||
|
|
||||||
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
|
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
|
||||||
iconMutex.RLock()
|
iconMutex.RLock()
|
||||||
|
|
@ -78,33 +71,10 @@ func generateSVG(name string, props Props) (string, error) {
|
||||||
return "", err // Error from getIconContent already includes icon name
|
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.
|
// Construct the final SVG string.
|
||||||
// The data-lucide attribute helps identify these as Lucide icons if needed.
|
// 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>",
|
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>",
|
||||||
size, size, fill, stroke, strokeWidth, props.Class, content), nil
|
props.Class, content), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getIconContent retrieves the raw inner SVG content for a given icon name.
|
// 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
|
// 📚 Documentation: https://templui.io/docs/components/input
|
||||||
package input
|
package input
|
||||||
|
|
||||||
|
|
@ -38,6 +38,7 @@ type Props struct {
|
||||||
Value string
|
Value string
|
||||||
Disabled bool
|
Disabled bool
|
||||||
Readonly bool
|
Readonly bool
|
||||||
|
Required bool
|
||||||
FileAccept string
|
FileAccept string
|
||||||
HasError bool
|
HasError bool
|
||||||
NoTogglePassword bool
|
NoTogglePassword bool
|
||||||
|
|
@ -75,6 +76,7 @@ templ Input(props ...Props) {
|
||||||
}
|
}
|
||||||
disabled?={ p.Disabled }
|
disabled?={ p.Disabled }
|
||||||
readonly?={ p.Readonly }
|
readonly?={ p.Readonly }
|
||||||
|
required?={ p.Required }
|
||||||
if p.HasError {
|
if p.HasError {
|
||||||
aria-invalid="true"
|
aria-invalid="true"
|
||||||
}
|
}
|
||||||
|
|
@ -111,20 +113,20 @@ templ Input(props ...Props) {
|
||||||
Attributes: templ.Attributes{"data-tui-input-toggle-password": p.ID},
|
Attributes: templ.Attributes{"data-tui-input-toggle-password": p.ID},
|
||||||
}) {
|
}) {
|
||||||
<span class="icon-open block">
|
<span class="icon-open block">
|
||||||
@icon.Eye(icon.Props{
|
@icon.Eye(icon.Props{Class: "size-[18px]"})
|
||||||
Size: 18,
|
|
||||||
})
|
|
||||||
</span>
|
</span>
|
||||||
<span class="icon-closed hidden">
|
<span class="icon-closed hidden">
|
||||||
@icon.EyeOff(icon.Props{
|
@icon.EyeOff(icon.Props{Class: "size-[18px]"})
|
||||||
Size: 18,
|
|
||||||
})
|
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/input-otp
|
||||||
package inputotp
|
package inputotp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
@ -176,6 +177,11 @@ templ Separator(props ...SeparatorProps) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/label
|
||||||
package label
|
package label
|
||||||
|
|
||||||
|
|
@ -38,6 +38,10 @@ templ Label(props ...Props) {
|
||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/pagination
|
||||||
package pagination
|
package pagination
|
||||||
|
|
||||||
|
|
@ -145,7 +145,7 @@ templ Previous(props ...PreviousProps) {
|
||||||
Class: utils.TwMerge("gap-1", p.Class),
|
Class: utils.TwMerge("gap-1", p.Class),
|
||||||
Attributes: p.Attributes,
|
Attributes: p.Attributes,
|
||||||
}) {
|
}) {
|
||||||
@icon.ChevronLeft(icon.Props{Size: 16})
|
@icon.ChevronLeft(icon.Props{Class: "size-4"})
|
||||||
if p.Label != "" {
|
if p.Label != "" {
|
||||||
<span>{ p.Label }</span>
|
<span>{ p.Label }</span>
|
||||||
}
|
}
|
||||||
|
|
@ -168,12 +168,12 @@ templ Next(props ...NextProps) {
|
||||||
if p.Label != "" {
|
if p.Label != "" {
|
||||||
<span>{ p.Label }</span>
|
<span>{ p.Label }</span>
|
||||||
}
|
}
|
||||||
@icon.ChevronRight(icon.Props{Size: 16})
|
@icon.ChevronRight(icon.Props{Class: "size-4"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Ellipsis() {
|
templ Ellipsis() {
|
||||||
@icon.Ellipsis(icon.Props{Size: 16})
|
@icon.Ellipsis(icon.Props{Class: "size-4"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreatePagination(currentPage, totalPages, maxVisible int) struct {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/popover
|
||||||
package popover
|
package popover
|
||||||
|
|
||||||
|
|
@ -29,13 +29,19 @@ type TriggerType string
|
||||||
const (
|
const (
|
||||||
TriggerTypeHover TriggerType = "hover"
|
TriggerTypeHover TriggerType = "hover"
|
||||||
TriggerTypeClick TriggerType = "click"
|
TriggerTypeClick TriggerType = "click"
|
||||||
|
TriggerTypeManual TriggerType = "manual"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RootProps struct {
|
||||||
|
ID string
|
||||||
|
Class string
|
||||||
|
Attributes templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
type TriggerProps struct {
|
type TriggerProps struct {
|
||||||
ID string
|
ID string
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
Attributes templ.Attributes
|
||||||
For string
|
|
||||||
TriggerType TriggerType
|
TriggerType TriggerType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,10 +56,26 @@ type ContentProps struct {
|
||||||
ShowArrow bool
|
ShowArrow bool
|
||||||
HoverDelay int
|
HoverDelay int
|
||||||
HoverOutDelay int
|
HoverOutDelay int
|
||||||
MatchWidth bool
|
|
||||||
Exclusive 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) {
|
templ Trigger(props ...TriggerProps) {
|
||||||
{{ var p TriggerProps }}
|
{{ var p TriggerProps }}
|
||||||
if len(props) > 0 {
|
if len(props) > 0 {
|
||||||
|
|
@ -66,11 +88,8 @@ templ Trigger(props ...TriggerProps) {
|
||||||
if p.ID != "" {
|
if p.ID != "" {
|
||||||
id={ p.ID }
|
id={ p.ID }
|
||||||
}
|
}
|
||||||
class={ utils.TwMerge("group cursor-pointer", p.Class) }
|
class={ utils.TwMerge("contents", p.Class) }
|
||||||
if p.For != "" {
|
data-tui-popover-trigger
|
||||||
data-tui-popover-trigger={ p.For }
|
|
||||||
}
|
|
||||||
data-tui-popover-open="false"
|
|
||||||
data-tui-popover-type={ string(p.TriggerType) }
|
data-tui-popover-type={ string(p.TriggerType) }
|
||||||
{ p.Attributes... }
|
{ p.Attributes... }
|
||||||
>
|
>
|
||||||
|
|
@ -94,8 +113,11 @@ templ Content(props ...ContentProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<div
|
<div
|
||||||
|
if p.ID != "" {
|
||||||
id={ 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-open="false"
|
||||||
data-tui-popover-placement={ string(p.Placement) }
|
data-tui-popover-placement={ string(p.Placement) }
|
||||||
data-tui-popover-offset={ strconv.Itoa(p.Offset) }
|
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-delay={ strconv.Itoa(p.HoverDelay) }
|
||||||
data-tui-popover-hover-out-delay={ strconv.Itoa(p.HoverOutDelay) }
|
data-tui-popover-hover-out-delay={ strconv.Itoa(p.HoverOutDelay) }
|
||||||
data-tui-popover-exclusive={ strconv.FormatBool(p.Exclusive) }
|
data-tui-popover-exclusive={ strconv.FormatBool(p.Exclusive) }
|
||||||
if p.MatchWidth {
|
|
||||||
data-tui-popover-match-width="true"
|
|
||||||
}
|
|
||||||
class={ utils.TwMerge(
|
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.Class,
|
||||||
) }
|
) }
|
||||||
{ p.Attributes... }
|
{ p.Attributes... }
|
||||||
|
|
@ -120,16 +139,16 @@ templ Content(props ...ContentProps) {
|
||||||
if p.ShowArrow {
|
if p.ShowArrow {
|
||||||
<div
|
<div
|
||||||
data-tui-popover-arrow
|
data-tui-popover-arrow
|
||||||
class="absolute h-2.5 w-2.5 rotate-45 bg-popover border border-border
|
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"
|
|
||||||
></div>
|
></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/progress
|
||||||
package progress
|
package progress
|
||||||
|
|
||||||
|
|
@ -122,6 +122,10 @@ func variantClasses(variant Variant) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/radio
|
||||||
package 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
|
// 📚 Documentation: https://templui.io/docs/components/rating
|
||||||
package rating
|
package rating
|
||||||
|
|
||||||
|
|
@ -166,7 +166,7 @@ func ratingIcon(style Style, filled bool, value float64) templ.Component {
|
||||||
}
|
}
|
||||||
iconProps := icon.Props{}
|
iconProps := icon.Props{}
|
||||||
if filled {
|
if filled {
|
||||||
iconProps.Fill = "currentColor"
|
iconProps.Class = "fill-current"
|
||||||
}
|
}
|
||||||
switch style {
|
switch style {
|
||||||
case StyleHeart:
|
case StyleHeart:
|
||||||
|
|
@ -188,6 +188,10 @@ func (p *Props) setDefaults() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/select-box
|
||||||
package selectbox
|
package selectbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
"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/icon"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
|
||||||
|
|
@ -13,10 +11,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey string
|
|
||||||
|
|
||||||
var contentIDKey contextKey = "contentID"
|
|
||||||
|
|
||||||
type Props struct {
|
type Props struct {
|
||||||
ID string
|
ID string
|
||||||
Class string
|
Class string
|
||||||
|
|
@ -80,170 +74,18 @@ templ SelectBox(props ...Props) {
|
||||||
if len(props) > 0 {
|
if len(props) > 0 {
|
||||||
p = 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
|
<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 != "" {
|
if p.ID != "" {
|
||||||
id={ p.ID }
|
id={ p.ID }
|
||||||
}
|
}
|
||||||
class={ utils.TwMerge("block truncate select-value text-muted-foreground", p.Class) }
|
class={ utils.TwMerge("select-container w-full relative", p.Class) }
|
||||||
if p.Placeholder != "" {
|
|
||||||
data-tui-selectbox-placeholder={ p.Placeholder }
|
|
||||||
}
|
|
||||||
{ p.Attributes... }
|
{ p.Attributes... }
|
||||||
>
|
>
|
||||||
if p.Placeholder != "" {
|
@popover.Root() {
|
||||||
{ p.Placeholder }
|
|
||||||
}
|
|
||||||
{ children... }
|
{ 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>
|
|
||||||
}
|
|
||||||
<div class="max-h-[300px] overflow-y-auto">
|
|
||||||
{ children... }
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Group(props ...GroupProps) {
|
templ Group(props ...GroupProps) {
|
||||||
|
|
@ -290,10 +132,10 @@ templ Item(props ...ItemProps) {
|
||||||
}
|
}
|
||||||
class={
|
class={
|
||||||
utils.TwMerge(
|
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",
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
"focus:bg-accent focus:text-accent-foreground",
|
"focus-visible:bg-accent focus-visible:text-accent-foreground",
|
||||||
utils.If(p.Selected, "bg-accent 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"),
|
utils.If(p.Disabled, "pointer-events-none opacity-50"),
|
||||||
p.Class,
|
p.Class,
|
||||||
),
|
),
|
||||||
|
|
@ -311,16 +153,163 @@ templ Item(props ...ItemProps) {
|
||||||
<span
|
<span
|
||||||
class={
|
class={
|
||||||
utils.TwMerge(
|
utils.TwMerge(
|
||||||
"select-check absolute right-2 flex h-3.5 w-3.5 items-center justify-center",
|
"select-check absolute right-2 flex h-3.5 w-3.5 items-center justify-center opacity-0",
|
||||||
utils.IfElse(p.Selected, "opacity-100", "opacity-0"),
|
"group-data-[tui-selectbox-selected=true]:opacity-100",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@icon.Check(icon.Props{Size: 16})
|
@icon.Check(icon.Props{Class: "size-4"})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Script() {
|
templ Trigger(props ...TriggerProps) {
|
||||||
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/selectbox.min.js") }></script>
|
{{
|
||||||
|
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
|
// 📚 Documentation: https://templui.io/docs/components/separator
|
||||||
package 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
|
// 📚 Documentation: https://templui.io/docs/components/sheet
|
||||||
package sheet
|
package sheet
|
||||||
|
|
||||||
|
|
@ -37,16 +37,14 @@ type TriggerProps struct {
|
||||||
ID string
|
ID string
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
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 {
|
type ContentProps struct {
|
||||||
ID string
|
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
Attributes templ.Attributes
|
||||||
HideCloseButton bool
|
HideCloseButton bool
|
||||||
Side Side //
|
Side Side
|
||||||
Open bool // Initial open state for standalone usage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeaderProps struct {
|
type HeaderProps struct {
|
||||||
|
|
@ -140,20 +138,16 @@ templ Content(props ...ContentProps) {
|
||||||
}
|
}
|
||||||
// Sheet content uses Dialog content with sheet-specific styles
|
// Sheet content uses Dialog content with sheet-specific styles
|
||||||
@dialog.Content(dialog.ContentProps{
|
@dialog.Content(dialog.ContentProps{
|
||||||
ID: p.ID,
|
|
||||||
Open: p.Open,
|
|
||||||
HideCloseButton: p.HideCloseButton,
|
HideCloseButton: p.HideCloseButton,
|
||||||
Class: utils.TwMerge(
|
Class: utils.TwMerge(
|
||||||
// First apply side-specific positioning and animations
|
// First apply side-specific positioning and animations
|
||||||
getSideClasses(p.Side),
|
getSideClasses(p.Side),
|
||||||
// Default gap matching shadcn (no padding in content)
|
// Move panel layout overrides to the inner dialog panel
|
||||||
"gap-4 !p-0", // Remove Dialog's p-6 padding
|
"[&_[data-tui-dialog-panel]]:gap-4 [&_[data-tui-dialog-panel]]:!p-0",
|
||||||
// Override Dialog styles
|
// Override Dialog styles
|
||||||
"!scale-100", // Reset Dialog's scale animation
|
"!scale-100", // Reset Dialog's scale animation
|
||||||
"!rounded-none", // Remove dialog rounded corners
|
"!rounded-none", // Remove dialog rounded corners
|
||||||
"!opacity-100", // Keep fully opaque - no fade, only slide
|
"!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
|
// User-provided classes last
|
||||||
p.Class,
|
p.Class,
|
||||||
),
|
),
|
||||||
|
|
@ -256,26 +250,26 @@ func getSideClasses(side Side) string {
|
||||||
case SideRight:
|
case SideRight:
|
||||||
return baseClasses +
|
return baseClasses +
|
||||||
// Positioning
|
// Positioning
|
||||||
"!inset-y-0 !right-0 !left-auto !top-auto " +
|
"!top-0 !bottom-0 !right-0 !left-auto " +
|
||||||
// Size
|
// Size
|
||||||
"h-full w-3/4 sm:max-w-sm " +
|
"!h-dvh !max-h-dvh w-3/4 sm:!max-w-sm " +
|
||||||
// Border
|
// Border
|
||||||
"border-l border-t-0 border-r-0 border-b-0 " +
|
"border-l border-t-0 border-r-0 border-b-0 " +
|
||||||
// Reset Dialog transforms
|
// Reset Dialog transforms
|
||||||
"!translate-y-0 " +
|
"!translate-x-0 !translate-y-0 " +
|
||||||
// Slide animation
|
// Slide animation
|
||||||
"data-[tui-dialog-open=false]:!translate-x-full " +
|
"data-[tui-dialog-open=false]:!translate-x-full " +
|
||||||
"data-[tui-dialog-open=true]:!translate-x-0"
|
"data-[tui-dialog-open=true]:!translate-x-0"
|
||||||
case SideLeft:
|
case SideLeft:
|
||||||
return baseClasses +
|
return baseClasses +
|
||||||
// Positioning
|
// Positioning
|
||||||
"!inset-y-0 !left-0 !right-auto !top-auto " +
|
"!top-0 !bottom-0 !left-0 !right-auto " +
|
||||||
// Size
|
// Size
|
||||||
"h-full w-3/4 sm:max-w-sm " +
|
"!h-dvh !max-h-dvh w-3/4 sm:!max-w-sm " +
|
||||||
// Border
|
// Border
|
||||||
"border-r border-t-0 border-l-0 border-b-0 " +
|
"border-r border-t-0 border-l-0 border-b-0 " +
|
||||||
// Reset Dialog transforms
|
// Reset Dialog transforms
|
||||||
"!translate-y-0 " +
|
"!translate-x-0 !translate-y-0 " +
|
||||||
// Slide animation
|
// Slide animation
|
||||||
"data-[tui-dialog-open=false]:!-translate-x-full " +
|
"data-[tui-dialog-open=false]:!-translate-x-full " +
|
||||||
"data-[tui-dialog-open=true]:!translate-x-0"
|
"data-[tui-dialog-open=true]:!translate-x-0"
|
||||||
|
|
@ -308,11 +302,19 @@ func getSideClasses(side Side) string {
|
||||||
default:
|
default:
|
||||||
return baseClasses +
|
return baseClasses +
|
||||||
// Default to right side
|
// Default to right side
|
||||||
"!inset-y-0 !right-0 !left-auto !top-auto " +
|
"!top-0 !bottom-0 !right-0 !left-auto " +
|
||||||
"h-full w-3/4 " +
|
"!h-dvh !max-h-dvh w-3/4 sm:!max-w-sm " +
|
||||||
"border-l border-t-0 border-r-0 border-b-0 " +
|
"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=false]:!translate-x-full " +
|
||||||
"data-[tui-dialog-open=true]:!translate-x-0"
|
"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
|
// 📚 Documentation: https://templui.io/docs/components/sidebar
|
||||||
package sidebar
|
package sidebar
|
||||||
|
|
||||||
|
|
@ -47,6 +47,113 @@ type Props struct {
|
||||||
KeyboardShortcut string // default: "b"
|
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 {
|
type TriggerProps struct {
|
||||||
ID string
|
ID string
|
||||||
Class string
|
Class string
|
||||||
|
|
@ -453,9 +560,7 @@ templ MenuButton(props ...MenuButtonProps) {
|
||||||
// When collapsed to icon mode - show with tooltip
|
// When collapsed to icon mode - show with tooltip
|
||||||
<div class="group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:block hidden">
|
<div class="group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:block hidden">
|
||||||
@tooltip.Tooltip() {
|
@tooltip.Tooltip() {
|
||||||
@tooltip.Trigger(tooltip.TriggerProps{
|
@tooltip.Trigger(tooltip.TriggerProps{}) {
|
||||||
For: tooltipID,
|
|
||||||
}) {
|
|
||||||
@menuButtonContent(p, "") {
|
@menuButtonContent(p, "") {
|
||||||
{ children... }
|
{ children... }
|
||||||
}
|
}
|
||||||
|
|
@ -641,113 +746,12 @@ templ MenuSubButton(props ...MenuSubButtonProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Inset(props ...InsetProps) {
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
{{ 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... }
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/skeleton
|
||||||
package 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
|
// 📚 Documentation: https://templui.io/docs/components/slider
|
||||||
package slider
|
package slider
|
||||||
|
|
||||||
|
|
@ -116,6 +116,10 @@ templ Value(props ...ValueProps) {
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/switch
|
||||||
package switchcomp
|
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
|
// 📚 Documentation: https://templui.io/docs/components/table
|
||||||
package 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
|
// 📚 Documentation: https://templui.io/docs/components/tabs
|
||||||
package tabs
|
package tabs
|
||||||
|
|
||||||
|
|
@ -158,6 +158,10 @@ func IDFromContext(ctx context.Context) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/tags-input
|
||||||
package tagsinput
|
package tagsinput
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
|
"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/input"
|
||||||
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/popover"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -19,19 +20,26 @@ type Props struct {
|
||||||
Attributes templ.Attributes
|
Attributes templ.Attributes
|
||||||
Disabled bool
|
Disabled bool
|
||||||
Readonly bool
|
Readonly bool
|
||||||
|
Suggestions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
templ TagsInput(props ...Props) {
|
templ TagsInput(props ...Props) {
|
||||||
{{ var p Props }}
|
{{
|
||||||
|
var p Props
|
||||||
if len(props) > 0 {
|
if len(props) > 0 {
|
||||||
{{ p = props[0] }}
|
p = props[0]
|
||||||
}
|
}
|
||||||
|
if p.ID == "" {
|
||||||
|
p.ID = utils.RandomID()
|
||||||
|
}
|
||||||
|
suggestionsID := p.ID + "-suggestions"
|
||||||
|
}}
|
||||||
<div
|
<div
|
||||||
id={ p.ID + "-container" }
|
id={ p.ID + "-container" }
|
||||||
class={
|
class={
|
||||||
utils.TwMerge(
|
utils.TwMerge(
|
||||||
// Base styles
|
// 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 mode background
|
||||||
"dark:bg-input/30",
|
"dark:bg-input/30",
|
||||||
// Focus styles
|
// Focus styles
|
||||||
|
|
@ -48,9 +56,13 @@ templ TagsInput(props ...Props) {
|
||||||
data-tui-tagsinput
|
data-tui-tagsinput
|
||||||
data-tui-tagsinput-name={ p.Name }
|
data-tui-tagsinput-name={ p.Name }
|
||||||
data-tui-tagsinput-form={ p.Form }
|
data-tui-tagsinput-form={ p.Form }
|
||||||
|
if len(p.Suggestions) > 0 {
|
||||||
|
data-tui-tagsinput-suggestions-id={ suggestionsID }
|
||||||
|
}
|
||||||
{ p.Attributes... }
|
{ 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 {
|
for _, tag := range p.Value {
|
||||||
@badge.Badge(badge.Props{
|
@badge.Badge(badge.Props{
|
||||||
Attributes: templ.Attributes{"data-tui-tagsinput-chip": ""},
|
Attributes: templ.Attributes{"data-tui-tagsinput-chip": ""},
|
||||||
|
|
@ -69,9 +81,18 @@ templ TagsInput(props ...Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</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{
|
@input.Input(input.Props{
|
||||||
ID: p.ID,
|
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,
|
Type: input.TypeText,
|
||||||
Placeholder: p.Placeholder,
|
Placeholder: p.Placeholder,
|
||||||
Disabled: p.Disabled,
|
Disabled: p.Disabled,
|
||||||
|
|
@ -81,7 +102,44 @@ templ TagsInput(props ...Props) {
|
||||||
p.Attributes,
|
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 {
|
for _, tag := range p.Value {
|
||||||
<input type="hidden" name={ p.Name } value={ tag }/>
|
<input type="hidden" name={ p.Name } value={ tag }/>
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +147,11 @@ templ TagsInput(props ...Props) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/textarea
|
||||||
package textarea
|
package textarea
|
||||||
|
|
||||||
|
|
@ -80,6 +80,10 @@ templ Textarea(props ...Props) {
|
||||||
>{ p.Value }</textarea>
|
>{ p.Value }</textarea>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/time-picker
|
||||||
package timepicker
|
package timepicker
|
||||||
|
|
||||||
|
|
@ -56,7 +56,6 @@ templ TimePicker(props ...Props) {
|
||||||
p.Step = 1
|
p.Step = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentID = p.ID + "-content"
|
|
||||||
var valueString string
|
var valueString string
|
||||||
if p.Value != (time.Time{}) {
|
if p.Value != (time.Time{}) {
|
||||||
valueString = p.Value.Format("15:04")
|
valueString = p.Value.Format("15:04")
|
||||||
|
|
@ -70,7 +69,8 @@ templ TimePicker(props ...Props) {
|
||||||
maxTimeString = p.MaxTime.Format("15:04")
|
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
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name={ p.Name }
|
name={ p.Name }
|
||||||
|
|
@ -78,10 +78,9 @@ templ TimePicker(props ...Props) {
|
||||||
if p.Form != "" {
|
if p.Form != "" {
|
||||||
form={ p.Form }
|
form={ p.Form }
|
||||||
}
|
}
|
||||||
id={ p.ID + "-hidden" }
|
|
||||||
data-tui-timepicker-hidden-input="true"
|
data-tui-timepicker-hidden-input="true"
|
||||||
/>
|
/>
|
||||||
@popover.Trigger(popover.TriggerProps{For: contentID}) {
|
@popover.Trigger() {
|
||||||
@button.Button(button.Props{
|
@button.Button(button.Props{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
Variant: button.VariantOutline,
|
Variant: button.VariantOutline,
|
||||||
|
|
@ -118,12 +117,11 @@ templ TimePicker(props ...Props) {
|
||||||
{ p.Placeholder }
|
{ p.Placeholder }
|
||||||
</span>
|
</span>
|
||||||
<span class="text-muted-foreground flex items-center ml-2">
|
<span class="text-muted-foreground flex items-center ml-2">
|
||||||
@icon.Clock(icon.Props{Size: 16})
|
@icon.Clock(icon.Props{Class: "size-4"})
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@popover.Content(popover.ContentProps{
|
@popover.Content(popover.ContentProps{
|
||||||
ID: contentID,
|
|
||||||
Placement: popover.PlacementBottomStart,
|
Placement: popover.PlacementBottomStart,
|
||||||
Class: "p-0 w-80",
|
Class: "p-0 w-80",
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -136,7 +134,6 @@ templ TimePicker(props ...Props) {
|
||||||
<div
|
<div
|
||||||
data-tui-timepicker-popup="true"
|
data-tui-timepicker-popup="true"
|
||||||
data-tui-timepicker-input-name={ p.Name }
|
data-tui-timepicker-input-name={ p.Name }
|
||||||
data-tui-timepicker-parent-id={ p.ID }
|
|
||||||
if valueString != "" {
|
if valueString != "" {
|
||||||
data-tui-timepicker-value={ valueString }
|
data-tui-timepicker-value={ valueString }
|
||||||
}
|
}
|
||||||
|
|
@ -242,9 +239,15 @@ templ TimePicker(props ...Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/toast
|
||||||
package toast
|
package toast
|
||||||
|
|
||||||
|
|
@ -109,13 +109,13 @@ templ Toast(props ...Props) {
|
||||||
if p.Icon {
|
if p.Icon {
|
||||||
switch p.Variant {
|
switch p.Variant {
|
||||||
case VariantSuccess:
|
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:
|
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:
|
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:
|
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
|
// Content
|
||||||
|
|
@ -138,16 +138,17 @@ templ Toast(props ...Props) {
|
||||||
"type": "button",
|
"type": "button",
|
||||||
},
|
},
|
||||||
}) {
|
}) {
|
||||||
@icon.X(icon.Props{
|
@icon.X(icon.Props{Class: "size-[18px] opacity-75 hover:opacity-100"})
|
||||||
Size: 18,
|
|
||||||
Class: "opacity-75 hover:opacity-100",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
templ Script() {
|
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
|
// 📚 Documentation: https://templui.io/docs/components/tooltip
|
||||||
package tooltip
|
package tooltip
|
||||||
|
|
||||||
|
|
@ -16,7 +16,6 @@ const (
|
||||||
PositionLeft Position = "left"
|
PositionLeft Position = "left"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Map tooltip positions to popover positions
|
|
||||||
func mapTooltipPositionToPopover(position Position) popover.Placement {
|
func mapTooltipPositionToPopover(position Position) popover.Placement {
|
||||||
switch position {
|
switch position {
|
||||||
case PositionTop:
|
case PositionTop:
|
||||||
|
|
@ -42,7 +41,6 @@ type TriggerProps struct {
|
||||||
ID string
|
ID string
|
||||||
Class string
|
Class string
|
||||||
Attributes templ.Attributes
|
Attributes templ.Attributes
|
||||||
For string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentProps struct {
|
type ContentProps struct {
|
||||||
|
|
@ -56,7 +54,17 @@ type ContentProps struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Tooltip(props ...Props) {
|
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... }
|
{ children... }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Trigger(props ...TriggerProps) {
|
templ Trigger(props ...TriggerProps) {
|
||||||
|
|
@ -69,7 +77,6 @@ templ Trigger(props ...TriggerProps) {
|
||||||
Class: p.Class,
|
Class: p.Class,
|
||||||
Attributes: p.Attributes,
|
Attributes: p.Attributes,
|
||||||
TriggerType: popover.TriggerTypeHover,
|
TriggerType: popover.TriggerTypeHover,
|
||||||
For: p.For,
|
|
||||||
}) {
|
}) {
|
||||||
{ children... }
|
{ children... }
|
||||||
}
|
}
|
||||||
|
|
@ -92,3 +99,11 @@ templ Content(props ...ContentProps) {
|
||||||
{ children... }
|
{ children... }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scriptOnce = templ.NewOnceHandle()
|
||||||
|
|
||||||
|
templ Script() {
|
||||||
|
@scriptOnce.Once() {
|
||||||
|
@popover.Script()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ templ AppSidebarDropdown(user *model.User) {
|
||||||
Href: "/app/settings",
|
Href: "/app/settings",
|
||||||
}) {
|
}) {
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
@icon.Settings(icon.Props{Size: 16, Class: "mr-2"})
|
@icon.Settings(icon.Props{Class: "mr-2"})
|
||||||
Settings
|
Settings
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +168,7 @@ templ AppSidebarDropdown(user *model.User) {
|
||||||
},
|
},
|
||||||
}) {
|
}) {
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
@icon.LogOut(icon.Props{Size: 16, Class: "mr-2"})
|
@icon.LogOut(icon.Props{Class: "mr-2"})
|
||||||
Log out
|
Log out
|
||||||
</span>
|
</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
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crypto/rand"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
|
"github.com/templui/templui/components"
|
||||||
|
|
||||||
twmerge "github.com/Oudwins/tailwind-merge-go"
|
twmerge "github.com/Oudwins/tailwind-merge-go"
|
||||||
)
|
)
|
||||||
|
|
@ -18,9 +24,9 @@ func TwMerge(classes ...string) string {
|
||||||
return twmerge.Merge(classes...)
|
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"
|
// 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
|
var empty T
|
||||||
if condition {
|
if condition {
|
||||||
return value
|
return value
|
||||||
|
|
@ -28,7 +34,7 @@ func If[T comparable](condition bool, value T) T {
|
||||||
return empty
|
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"
|
// Example: true, "bg-red-500", "bg-gray-300" → "bg-red-500"
|
||||||
func IfElse[T any](condition bool, trueValue T, falseValue T) T {
|
func IfElse[T any](condition bool, trueValue T, falseValue T) T {
|
||||||
if condition {
|
if condition {
|
||||||
|
|
@ -56,7 +62,7 @@ func RandomID() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScriptVersion is a timestamp generated at app start for cache busting.
|
// 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())
|
var ScriptVersion = fmt.Sprintf("%d", time.Now().Unix())
|
||||||
|
|
||||||
// ScriptURL generates cache-busted script URLs.
|
// ScriptURL generates cache-busted script URLs.
|
||||||
|
|
@ -72,3 +78,85 @@ var ScriptVersion = fmt.Sprintf("%d", time.Now().Unix())
|
||||||
var ScriptURL = func(path string) string {
|
var ScriptURL = func(path string) string {
|
||||||
return path + "?v=" + ScriptVersion
|
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