style: format web/src with oxfmt defaults (via vp fmt)
One-time mechanical sweep, isolated from any functional change. - oxfmt defaults: double quotes, semicolons, 2-space indent, LF, printWidth 100, trailing commas, bracket spacing, arrow parens. - vite.config.ts gains an fmt.ignorePatterns block for web/static: vendored/minified assets (htmx.min.js, compiled css) must never be reformatted. - Verified: rebuild after the sweep produces byte-identical JS/CSS bundles; only sourcemaps changed (they embed sourcesContent).
This commit is contained in:
1 parent
8aeecaa0ef
commit
77cd268f4e
37 files changed
+1151
-1853
No files matched your search
+58
-81
@@ -2,10 +2,7 @@
|
||||
|
||||
import { Alpine } from "./alpine";
|
||||
import { showToast } from "./toast";
|
||||
import type {
|
||||
FilterGroupState,
|
||||
FilterRuleState,
|
||||
} from "./filter_builder";
|
||||
import type { FilterGroupState, FilterRuleState } from "./filter_builder";
|
||||
|
||||
function clearFilters(): void {
|
||||
// Clear all form state (reuses helper)
|
||||
@@ -65,12 +62,8 @@ function clearFormWithoutSubmit(): void {
|
||||
cleanupDynamicHiddenInputs();
|
||||
|
||||
// Reset pagination to defaults
|
||||
const limitInput = filterForm.querySelector(
|
||||
'input[name="limit"]',
|
||||
) as HTMLInputElement;
|
||||
const offsetInput = filterForm.querySelector(
|
||||
'input[name="offset"]',
|
||||
) as HTMLInputElement;
|
||||
const limitInput = filterForm.querySelector('input[name="limit"]') as HTMLInputElement;
|
||||
const offsetInput = filterForm.querySelector('input[name="offset"]') as HTMLInputElement;
|
||||
if (limitInput) limitInput.value = "50";
|
||||
if (offsetInput) offsetInput.value = "0";
|
||||
|
||||
@@ -100,22 +93,21 @@ Alpine.data("bookshelf", () => ({
|
||||
// inputs are dropped outright) and re-apply the form. Chips refresh
|
||||
// via the shared htmx:afterRequest hook.
|
||||
removeFilterChip(param: string): void {
|
||||
const filterForm = document.getElementById(
|
||||
"filter-form",
|
||||
) as HTMLFormElement;
|
||||
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
||||
if (!filterForm) return;
|
||||
// Tree chips (qf mode): "rule:<path>" — remove that rule from the
|
||||
// builder tree, resync the wire inputs, re-apply.
|
||||
if (param.startsWith("rule:")) {
|
||||
const builder = builderData();
|
||||
const path = param.slice(5).split("-").map((n) => parseInt(n, 10));
|
||||
const path = param
|
||||
.slice(5)
|
||||
.split("-")
|
||||
.map((n) => parseInt(n, 10));
|
||||
builder?.removeNode(path);
|
||||
window.htmx.trigger(filterForm, "submit");
|
||||
return;
|
||||
}
|
||||
const input = filterForm.querySelector(
|
||||
`[name="${param}"]`,
|
||||
) as HTMLInputElement | null;
|
||||
const input = filterForm.querySelector(`[name="${param}"]`) as HTMLInputElement | null;
|
||||
if (input) {
|
||||
if (input.type === "hidden") {
|
||||
input.remove();
|
||||
@@ -225,14 +217,10 @@ Alpine.data("bookshelf", () => ({
|
||||
|
||||
// Parse filters JSON (string → object)
|
||||
const filterData: Record<string, string> =
|
||||
typeof filter.filters === "string"
|
||||
? JSON.parse(filter.filters)
|
||||
: filter.filters;
|
||||
typeof filter.filters === "string" ? JSON.parse(filter.filters) : filter.filters;
|
||||
|
||||
// Get the hidden filter form
|
||||
const filterForm = document.getElementById(
|
||||
"filter-form",
|
||||
) as HTMLFormElement;
|
||||
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
||||
if (!filterForm) {
|
||||
showToast("Filter form not found", "error");
|
||||
return;
|
||||
@@ -250,9 +238,7 @@ Alpine.data("bookshelf", () => ({
|
||||
// (qf wins over flat, the pinned precedence).
|
||||
Object.entries(filterData).forEach(([key, value]) => {
|
||||
if (!value) return; // Skip empty values
|
||||
const visibleField = filterForm.querySelector(
|
||||
`[name="${key}"]`,
|
||||
) as HTMLInputElement;
|
||||
const visibleField = filterForm.querySelector(`[name="${key}"]`) as HTMLInputElement;
|
||||
if (visibleField) {
|
||||
if (visibleField.type === "checkbox") {
|
||||
visibleField.checked = value === "true";
|
||||
@@ -336,9 +322,7 @@ function applyURLFilters(filterForm: HTMLFormElement | null): boolean {
|
||||
// ignored — the tree IS the query.
|
||||
const qf = params.get("qf");
|
||||
if (qf) {
|
||||
const input = filterForm.querySelector(
|
||||
'[name="qf"]',
|
||||
) as HTMLInputElement | null;
|
||||
const input = filterForm.querySelector('[name="qf"]') as HTMLInputElement | null;
|
||||
if (input) {
|
||||
input.value = qf;
|
||||
} else {
|
||||
@@ -376,9 +360,7 @@ function applyURLFilters(filterForm: HTMLFormElement | null): boolean {
|
||||
for (const name of filterNames) {
|
||||
const value = params.get(name);
|
||||
if (!value) continue;
|
||||
const input = filterForm.querySelector(
|
||||
`[name="${name}"]`,
|
||||
) as HTMLInputElement | null;
|
||||
const input = filterForm.querySelector(`[name="${name}"]`) as HTMLInputElement | null;
|
||||
if (input) {
|
||||
input.value = value;
|
||||
applied = true;
|
||||
@@ -413,14 +395,10 @@ interface FilterChip {
|
||||
function prettifyGenreLabel(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || trimmed !== trimmed.toLowerCase()) return trimmed;
|
||||
const small = new Set([
|
||||
"a", "an", "and", "of", "the", "in", "on", "for", "to", "or",
|
||||
]);
|
||||
const small = new Set(["a", "an", "and", "of", "the", "in", "on", "for", "to", "or"]);
|
||||
return trimmed
|
||||
.split(/\s+/)
|
||||
.map((w, i) =>
|
||||
i > 0 && small.has(w) ? w : w.charAt(0).toUpperCase() + w.slice(1),
|
||||
)
|
||||
.map((w, i) => (i > 0 && small.has(w) ? w : w.charAt(0).toUpperCase() + w.slice(1)))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
@@ -450,8 +428,7 @@ function facetChipLabel(name: string, value: string): string {
|
||||
abridged_filter: "Abridged",
|
||||
};
|
||||
const label = names[name] ?? name;
|
||||
const shown =
|
||||
name === "genre_filter" ? prettifyGenreLabel(value) : value;
|
||||
const shown = name === "genre_filter" ? prettifyGenreLabel(value) : value;
|
||||
return `${label}: ${shown}`;
|
||||
}
|
||||
|
||||
@@ -459,9 +436,7 @@ function facetChipLabel(name: string, value: string): string {
|
||||
// the shareable URL (history.replaceState) — the single sync point for
|
||||
// form applies, URL deep-links, sort changes, and clears.
|
||||
function refreshFilterChips(): void {
|
||||
const filterForm = document.getElementById(
|
||||
"filter-form",
|
||||
) as HTMLFormElement;
|
||||
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
||||
const bookshelfElem = document.querySelector('[x-data*="bookshelf"]');
|
||||
if (!filterForm || !bookshelfElem || !(bookshelfElem as any)._x_dataStack) {
|
||||
return;
|
||||
@@ -470,9 +445,7 @@ function refreshFilterChips(): void {
|
||||
if (typeof data.activeFilters === "undefined") return;
|
||||
|
||||
const chips: FilterChip[] = [];
|
||||
const qfInput = filterForm.querySelector(
|
||||
'[name="qf"]',
|
||||
) as HTMLInputElement | null;
|
||||
const qfInput = filterForm.querySelector('[name="qf"]') as HTMLInputElement | null;
|
||||
|
||||
if (qfInput && qfInput.value) {
|
||||
// Tree mode: one chip per rule (red for negations — the chips
|
||||
@@ -482,24 +455,20 @@ function refreshFilterChips(): void {
|
||||
try {
|
||||
const tree = JSON.parse(qfInput.value) as FilterGroupState;
|
||||
const walk = (group: FilterGroupState, path: number[]) => {
|
||||
(group.children as (FilterRuleState | FilterGroupState)[]).forEach(
|
||||
(child, idx) => {
|
||||
const childPath = [...path, idx];
|
||||
if ("children" in (child as FilterGroupState)) {
|
||||
walk(child as FilterGroupState, childPath);
|
||||
return;
|
||||
}
|
||||
const rule = child as FilterRuleState;
|
||||
const label = builder
|
||||
? builder.chipLabelFor(rule)
|
||||
: `${rule.field} ${rule.op}`;
|
||||
chips.push({
|
||||
param: "rule:" + childPath.join("-"),
|
||||
label,
|
||||
negated: builder?.isNegated(rule) ?? false,
|
||||
});
|
||||
},
|
||||
);
|
||||
(group.children as (FilterRuleState | FilterGroupState)[]).forEach((child, idx) => {
|
||||
const childPath = [...path, idx];
|
||||
if ("children" in (child as FilterGroupState)) {
|
||||
walk(child as FilterGroupState, childPath);
|
||||
return;
|
||||
}
|
||||
const rule = child as FilterRuleState;
|
||||
const label = builder ? builder.chipLabelFor(rule) : `${rule.field} ${rule.op}`;
|
||||
chips.push({
|
||||
param: "rule:" + childPath.join("-"),
|
||||
label,
|
||||
negated: builder?.isNegated(rule) ?? false,
|
||||
});
|
||||
});
|
||||
};
|
||||
walk(tree, []);
|
||||
} catch {
|
||||
@@ -527,12 +496,27 @@ function refreshFilterChips(): void {
|
||||
// shareable (plan: chips bar is URL-synced).
|
||||
const url = new URL(window.location.href);
|
||||
const keep = new Set([
|
||||
"author_filter", "series_filter", "genre_filter", "publisher_filter",
|
||||
"tags_filter", "narrator_filter", "language_filter", "story_arc_filter",
|
||||
"imprint_filter", "edition_filter", "age_rating_filter",
|
||||
"format_group_filter", "manga_type_filter", "reading_direction_filter",
|
||||
"explicit_filter", "abridged_filter", "year_min", "year_max",
|
||||
"has_cover", "library_id", "qf",
|
||||
"author_filter",
|
||||
"series_filter",
|
||||
"genre_filter",
|
||||
"publisher_filter",
|
||||
"tags_filter",
|
||||
"narrator_filter",
|
||||
"language_filter",
|
||||
"story_arc_filter",
|
||||
"imprint_filter",
|
||||
"edition_filter",
|
||||
"age_rating_filter",
|
||||
"format_group_filter",
|
||||
"manga_type_filter",
|
||||
"reading_direction_filter",
|
||||
"explicit_filter",
|
||||
"abridged_filter",
|
||||
"year_min",
|
||||
"year_max",
|
||||
"has_cover",
|
||||
"library_id",
|
||||
"qf",
|
||||
]);
|
||||
const next = new URLSearchParams();
|
||||
url.searchParams.forEach((value, key) => {
|
||||
@@ -551,9 +535,7 @@ function refreshFilterChips(): void {
|
||||
}
|
||||
|
||||
function initBookshelf(): void {
|
||||
const filterForm = document.getElementById(
|
||||
"filter-form",
|
||||
) as HTMLFormElement;
|
||||
const filterForm = document.getElementById("filter-form") as HTMLFormElement;
|
||||
|
||||
// Active-filter chips refresh after every grid refetch — drawer
|
||||
// applies, sort changes, and pagination all include the form's
|
||||
@@ -565,9 +547,7 @@ function initBookshelf(): void {
|
||||
// The live match count is scope-aware: switching the top library
|
||||
// picker re-runs it (the count fetch reads the select at fetch time —
|
||||
// this is what makes it fire).
|
||||
const librarySelect = document.getElementById(
|
||||
"library-select",
|
||||
) as HTMLSelectElement | null;
|
||||
const librarySelect = document.getElementById("library-select") as HTMLSelectElement | null;
|
||||
librarySelect?.addEventListener("change", () => {
|
||||
builderData()?.scheduleMatchCount();
|
||||
});
|
||||
@@ -587,13 +567,10 @@ function initBookshelf(): void {
|
||||
|
||||
// Check if books were already rendered server-side
|
||||
const booksGrid = document.getElementById("books-grid");
|
||||
const hasServerBooks =
|
||||
booksGrid && booksGrid.querySelector("[data-book-id]") !== null;
|
||||
const hasServerBooks = booksGrid && booksGrid.querySelector("[data-book-id]") !== null;
|
||||
|
||||
if (!hasServerBooks || urlApplied) {
|
||||
const librarySelect = document.getElementById(
|
||||
"library-select",
|
||||
) as HTMLSelectElement;
|
||||
const librarySelect = document.getElementById("library-select") as HTMLSelectElement;
|
||||
librarySelect?.addEventListener("change", () => {
|
||||
localStorage.setItem("selectedLibrary", librarySelect.value);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user