fix(types): clear web/src type and lint debt with real types
tsc --noEmit and oxlint web/src are now both clean; vp check passes.
- htmx: one canonical Htmx interface in shims.d.ts (ajax/process/trigger
per the htmx 2.x API). alpine.ts had a second, conflicting Window.htmx
declaration (TS2717) — removed; bookPicker/collections lose their
(window as any) escapes now that Window.htmx is properly typed.
- tsconfig lib ES2020 -> ES2021 + DOM.Iterable: replaceAll exists
(escapeHtml) and for..of over querySelectorAll results is legal.
Type-checking only — vite emits at esnext regardless.
- custom-section-builder: selectedBooks holds identity fields only;
typed as Pick<BookInfo, ...> instead of forcing partial API shapes.
The API-critical BookInfo contract is untouched.
- toast: real bug found by the type system — setup.ts passes 'warning'
but getToastConfig had no entry, so the warning toast threw TypeError
at runtime instead of displaying. Added the type + config. Uses
bg-yellow-500 (verified present in scanned templates => generated by
Tailwind); the one intentional behavior change in this commit.
- book-detail: instanceof narrowing replaces an as-cast; unused
AnnotationSource import removed.
- password_validation: explicit HTMLElement | null for the checklist
list (querySelector('ul') infers HTMLUListElement, conflicting with
the container-as-list branch).
- stats: tooltip parsed value typed via annotation (bar=scalar,
line={x,y}); dead getActiveRange() removed (was already tree-shaken
out of every bundle).
- library-switcher: onSwitch honestly typed void | Promise<void>
(series.ts passes a sync navigator).
- 20 unused catch bindings converted to optional catch binding.
Verification: fresh build is byte-reproducible; every chunk is
identical to the pre-fix build except main.js (instanceof narrowing,
regex fix, stats refactor), reader.js (destructure), and the shared
storage chunk (renamed — carries toast.ts). reader/main diffs audited
hunk-by-hunk against the intended edits.
This commit is contained in:
1 parent
77cd268f4e
commit
d7d1495753
14 files changed
+70
-60
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"module": "ES2020",
|
"module": "ES2020",
|
||||||
"lib": ["ES2020", "DOM"],
|
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||||
"outDir": "./web/static",
|
"outDir": "./web/static",
|
||||||
"rootDir": "./web/src",
|
"rootDir": "./web/src",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
|||||||
+3
-4
@@ -1,12 +1,11 @@
|
|||||||
import Alpine from "alpinejs";
|
import Alpine from "alpinejs";
|
||||||
|
|
||||||
// Extend Window interface to include Alpine
|
// Extend Window interface to include Alpine. The `htmx` member is declared
|
||||||
|
// once in shims.d.ts — redeclaring it here with a different shape is a
|
||||||
|
// compile error (TS2717) and split-brained typing.
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
Alpine: typeof Alpine;
|
Alpine: typeof Alpine;
|
||||||
htmx: {
|
|
||||||
trigger: (element: HTMLElement | string, event: string) => void;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
triggerDownload,
|
triggerDownload,
|
||||||
bannerBookTitle,
|
bannerBookTitle,
|
||||||
type AnnotationRow,
|
type AnnotationRow,
|
||||||
type AnnotationSource,
|
|
||||||
} from "./annotations-banner";
|
} from "./annotations-banner";
|
||||||
|
|
||||||
function getMediaId(): string {
|
function getMediaId(): string {
|
||||||
@@ -69,8 +68,8 @@ function collectFormData(editorTags: string[]): Record<string, unknown> {
|
|||||||
if (el.type === "number") {
|
if (el.type === "number") {
|
||||||
const val = parseFloat(el.value);
|
const val = parseFloat(el.value);
|
||||||
data[el.name] = isNaN(val) ? 0 : val;
|
data[el.name] = isNaN(val) ? 0 : val;
|
||||||
} else if (el.type === "checkbox") {
|
} else if (el instanceof HTMLInputElement && el.type === "checkbox") {
|
||||||
data[el.name] = (el as HTMLInputElement).checked;
|
data[el.name] = el.checked;
|
||||||
} else {
|
} else {
|
||||||
data[el.name] = el.value;
|
data[el.name] = el.value;
|
||||||
}
|
}
|
||||||
@@ -369,7 +368,7 @@ Alpine.data("bookDetail", () => {
|
|||||||
}
|
}
|
||||||
await this.fetchBannerAnnotations();
|
await this.fetchBannerAnnotations();
|
||||||
showToast("Note saved", "success");
|
showToast("Note saved", "success");
|
||||||
} catch (e) {
|
} catch {
|
||||||
showToast("Could not save the note", "error");
|
showToast("Could not save the note", "error");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -386,7 +385,7 @@ Alpine.data("bookDetail", () => {
|
|||||||
// Tombstoned server-side: restorable from the Deleted modal.
|
// Tombstoned server-side: restorable from the Deleted modal.
|
||||||
showToast("Note deleted — restorable from Deleted", "success");
|
showToast("Note deleted — restorable from Deleted", "success");
|
||||||
await this.fetchBannerAnnotations();
|
await this.fetchBannerAnnotations();
|
||||||
} catch (_e) {
|
} catch {
|
||||||
showToast("Could not delete the note", "error");
|
showToast("Could not delete the note", "error");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -403,7 +402,7 @@ Alpine.data("bookDetail", () => {
|
|||||||
// Tombstoned server-side: restorable from the Deleted modal.
|
// Tombstoned server-side: restorable from the Deleted modal.
|
||||||
showToast("Bookmark deleted — restorable from Deleted", "success");
|
showToast("Bookmark deleted — restorable from Deleted", "success");
|
||||||
await this.fetchBannerAnnotations();
|
await this.fetchBannerAnnotations();
|
||||||
} catch (_e) {
|
} catch {
|
||||||
showToast("Could not delete the bookmark", "error");
|
showToast("Could not delete the bookmark", "error");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -420,7 +419,7 @@ Alpine.data("bookDetail", () => {
|
|||||||
// Tombstoned server-side: restorable from the Deleted modal.
|
// Tombstoned server-side: restorable from the Deleted modal.
|
||||||
showToast("Highlight deleted — restorable from Deleted", "success");
|
showToast("Highlight deleted — restorable from Deleted", "success");
|
||||||
await this.fetchBannerAnnotations();
|
await this.fetchBannerAnnotations();
|
||||||
} catch (_e) {
|
} catch {
|
||||||
showToast("Could not delete the highlight", "error");
|
showToast("Could not delete the highlight", "error");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -455,7 +454,7 @@ Alpine.data("bookDetail", () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
showToast("Annotations exported", "success");
|
showToast("Annotations exported", "success");
|
||||||
} catch (_e) {
|
} catch {
|
||||||
showToast("Export failed", "error");
|
showToast("Export failed", "error");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -105,32 +105,30 @@ Alpine.store("bookPicker", {
|
|||||||
// Provides methods for template to access store
|
// Provides methods for template to access store
|
||||||
Alpine.data("bookPicker", () => ({
|
Alpine.data("bookPicker", () => ({
|
||||||
get isOpen() {
|
get isOpen() {
|
||||||
return (window as any).Alpine?.$store.bookPicker.isOpen;
|
return window.Alpine?.$store.bookPicker.isOpen;
|
||||||
},
|
},
|
||||||
|
|
||||||
get selectedCount() {
|
get selectedCount() {
|
||||||
return (window as any).Alpine?.$store.bookPicker.selectedCount;
|
return window.Alpine?.$store.bookPicker.selectedCount;
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleBook(bookId: string) {
|
toggleBook(bookId: string) {
|
||||||
(window as any).Alpine?.$store.bookPicker.toggleBook(bookId);
|
window.Alpine?.$store.bookPicker.toggleBook(bookId);
|
||||||
},
|
},
|
||||||
|
|
||||||
isSelected(bookId: string): boolean {
|
isSelected(bookId: string): boolean {
|
||||||
return (window as any).Alpine?.$store.bookPicker.isSelected(bookId);
|
return window.Alpine?.$store.bookPicker.isSelected(bookId);
|
||||||
},
|
},
|
||||||
|
|
||||||
clearFilters() {
|
clearFilters() {
|
||||||
(window as any).Alpine?.$store.bookPicker.clearFilters();
|
window.Alpine?.$store.bookPicker.clearFilters();
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
(window as any).Alpine?.$store.bookPicker.close();
|
window.Alpine?.$store.bookPicker.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
(window as any).Alpine?.$store.bookPicker.submit();
|
window.Alpine?.$store.bookPicker.submit();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -587,8 +587,8 @@ async function moveCollectionBook(this: any, mediaId: string, direction: number)
|
|||||||
showToast("Failed to save the new order", "error");
|
showToast("Failed to save the new order", "error");
|
||||||
// Rendering ownership: the templ partial re-renders the true order.
|
// Rendering ownership: the templ partial re-renders the true order.
|
||||||
const gridTarget = document.getElementById("books-container");
|
const gridTarget = document.getElementById("books-container");
|
||||||
if (gridTarget && (window as any).htmx) {
|
if (gridTarget && window.htmx) {
|
||||||
(window as any).htmx.ajax(
|
window.htmx.ajax(
|
||||||
"GET",
|
"GET",
|
||||||
`/collections/${getCollectionId()}/books-grid${window.location.search.startsWith("?") ? window.location.search : ""}`,
|
`/collections/${getCollectionId()}/books-grid${window.location.search.startsWith("?") ? window.location.search : ""}`,
|
||||||
{ target: "#books-container", swap: "innerHTML" },
|
{ target: "#books-container", swap: "innerHTML" },
|
||||||
|
|||||||
@@ -210,7 +210,11 @@ const FILTER_FIELDS: FilterField[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
let ruleCounter = 0;
|
let ruleCounter = 0;
|
||||||
let selectedBooks: Map<string, BookInfo> = new Map();
|
// Books picked in the builder only ever carry identity fields; the API's
|
||||||
|
// BookInfo conflict/format/badge metadata is attached server-side later.
|
||||||
|
type SelectedBook = Pick<BookInfo, "media_item_id" | "title" | "author" | "cover_image_path">;
|
||||||
|
|
||||||
|
let selectedBooks: Map<string, SelectedBook> = new Map();
|
||||||
let customSectionTimeout: number | null = null;
|
let customSectionTimeout: number | null = null;
|
||||||
|
|
||||||
function initCustomSectionBuilder(): void {
|
function initCustomSectionBuilder(): void {
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ async function loadShelfMappings(deviceId: string): Promise<void> {
|
|||||||
container.innerHTML =
|
container.innerHTML =
|
||||||
'<p style="color: var(--text-secondary)">No shelf mappings configured. Click "Add Mapping" to create one.</p>';
|
'<p style="color: var(--text-secondary)">No shelf mappings configured. Click "Add Mapping" to create one.</p>';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
container.innerHTML = '<p style="color: var(--error)">Failed to load mappings</p>';
|
container.innerHTML = '<p style="color: var(--error)">Failed to load mappings</p>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ export async function switchWithTransition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LibrarySwitcherOptions {
|
export interface LibrarySwitcherOptions {
|
||||||
onSwitch: (libraryId: string) => Promise<void>;
|
// Async switchers (SSR navigation awaits nothing, API-backed switchers
|
||||||
|
// await their fetch) — hence void | Promise<void>.
|
||||||
|
onSwitch: (libraryId: string) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initLibrarySwitcher(options: LibrarySwitcherOptions): void {
|
export function initLibrarySwitcher(options: LibrarySwitcherOptions): void {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function hasNumber(password: string): boolean {
|
|||||||
return /[0-9]/.test(password);
|
return /[0-9]/.test(password);
|
||||||
}
|
}
|
||||||
function hasSpecialChar(password: string): boolean {
|
function hasSpecialChar(password: string): boolean {
|
||||||
return /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
|
return /[!@#$%^&*()_+\-=[]{};':"\\|,.<>?]/.test(password);
|
||||||
}
|
}
|
||||||
function passwordsMatch(password: string, confirm: string): boolean {
|
function passwordsMatch(password: string, confirm: string): boolean {
|
||||||
return password.length > 0 && password === confirm;
|
return password.length > 0 && password === confirm;
|
||||||
@@ -107,7 +107,7 @@ function requirementRow(id: string, label: string): string {
|
|||||||
function ensureChecklist(containerId: string, rules: PasswordRules, hasConfirm: boolean): void {
|
function ensureChecklist(containerId: string, rules: PasswordRules, hasConfirm: boolean): void {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
let list = container.querySelector("ul");
|
let list: HTMLElement | null = container.querySelector("ul");
|
||||||
if (container.tagName === "UL") list = container; // container IS the list
|
if (container.tagName === "UL") list = container; // container IS the list
|
||||||
if (!list) {
|
if (!list) {
|
||||||
container.insertAdjacentHTML(
|
container.insertAdjacentHTML(
|
||||||
|
|||||||
+16
-16
@@ -187,7 +187,7 @@ async function loadFontBlobUrls(): Promise<void> {
|
|||||||
const blob = await resp.blob();
|
const blob = await resp.blob();
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
return { key: f.url, blobUrl };
|
return { key: f.url, blobUrl };
|
||||||
} catch (_e) {
|
} catch {
|
||||||
return { key: f.url, blobUrl: f.url };
|
return { key: f.url, blobUrl: f.url };
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -1533,7 +1533,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
async adjustFinalize() {
|
async adjustFinalize() {
|
||||||
const ctx = this.adjustContext;
|
const ctx = this.adjustContext;
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
const { index, doc, range, id } = ctx;
|
const { index, range, id } = ctx;
|
||||||
const text = range.toString().replace(/\s+/g, " ").trim();
|
const text = range.toString().replace(/\s+/g, " ").trim();
|
||||||
this.cancelAdjust();
|
this.cancelAdjust();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -2452,7 +2452,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
positionLabel: r.position ?? "",
|
positionLabel: r.position ?? "",
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch {
|
||||||
/* annotations are non-critical; leave lists as-is */
|
/* annotations are non-critical; leave lists as-is */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2523,7 +2523,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.hideSelectionPopover();
|
this.hideSelectionPopover();
|
||||||
} catch (_e) {
|
} catch {
|
||||||
/* ignore highlight errors */
|
/* ignore highlight errors */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2590,7 +2590,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
// Explicit saves (the Save button) close the whole popover; color
|
// Explicit saves (the Save button) close the whole popover; color
|
||||||
// tweaks made while the note editor is open keep everything open.
|
// tweaks made while the note editor is open keep everything open.
|
||||||
if (!keepNoteOpen) this.hideSelectionPopover();
|
if (!keepNoteOpen) this.hideSelectionPopover();
|
||||||
} catch (_e) {
|
} catch {
|
||||||
/* ignore highlight errors */
|
/* ignore highlight errors */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2615,7 +2615,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.hideSelectionPopover();
|
this.hideSelectionPopover();
|
||||||
} catch (_e) {
|
} catch {
|
||||||
/* ignore highlight errors */
|
/* ignore highlight errors */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2839,7 +2839,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
});
|
});
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
await this.refreshAnnotations();
|
await this.refreshAnnotations();
|
||||||
} catch (_e) {
|
} catch {
|
||||||
/* ignore note errors */
|
/* ignore note errors */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2882,7 +2882,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
note.positionLabel = row.position ?? note.positionLabel;
|
note.positionLabel = row.position ?? note.positionLabel;
|
||||||
note.editOpen = false;
|
note.editOpen = false;
|
||||||
note.editText = "";
|
note.editText = "";
|
||||||
} catch (_e) {
|
} catch {
|
||||||
/* note errors are non-critical */
|
/* note errors are non-critical */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2897,7 +2897,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
if (resp.ok || resp.status === 204) {
|
if (resp.ok || resp.status === 204) {
|
||||||
this.noteItems = this.noteItems.filter((n) => n.id !== id);
|
this.noteItems = this.noteItems.filter((n) => n.id !== id);
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch {
|
||||||
/* ignore note errors */
|
/* ignore note errors */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2928,7 +2928,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
page: typeof page === "number" ? page : 0,
|
page: typeof page === "number" ? page : 0,
|
||||||
percentage: typeof pct === "number" ? pct : 0,
|
percentage: typeof pct === "number" ? pct : 0,
|
||||||
};
|
};
|
||||||
} catch (_e) {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3017,7 +3017,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
});
|
});
|
||||||
this.lastSyncedCfi = cfi || "";
|
this.lastSyncedCfi = cfi || "";
|
||||||
this.lastSyncedFraction = fraction;
|
this.lastSyncedFraction = fraction;
|
||||||
} catch (_e) {
|
} catch {
|
||||||
// silent fail — progress save is non-critical
|
// silent fail — progress save is non-critical
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3658,7 +3658,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
cfi: r.cfi_position?.String ?? r.cfi_position ?? "",
|
cfi: r.cfi_position?.String ?? r.cfi_position ?? "",
|
||||||
page: r.page_number != null ? (r.page_number?.Int32 ?? r.page_number) : null,
|
page: r.page_number != null ? (r.page_number?.Int32 ?? r.page_number) : null,
|
||||||
}));
|
}));
|
||||||
} catch (_e) {
|
} catch {
|
||||||
/* leave the existing list on fetch failure */
|
/* leave the existing list on fetch failure */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3696,7 +3696,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
await this.refreshBookmarks();
|
await this.refreshBookmarks();
|
||||||
showToast(`Bookmark added — ${this.progressText || "current position"}`, "success", 2500);
|
showToast(`Bookmark added — ${this.progressText || "current position"}`, "success", 2500);
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch {
|
||||||
/* network failures surface via the toast fetch interceptor */
|
/* network failures surface via the toast fetch interceptor */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3711,7 +3711,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
if (resp.ok || resp.status === 204) {
|
if (resp.ok || resp.status === 204) {
|
||||||
this.bookmarkItems = this.bookmarkItems.filter((b) => b.id !== id);
|
this.bookmarkItems = this.bookmarkItems.filter((b) => b.id !== id);
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch {
|
||||||
/* ignore bookmark errors for now */
|
/* ignore bookmark errors for now */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3740,7 +3740,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
bookmark.title = title;
|
bookmark.title = title;
|
||||||
}
|
}
|
||||||
bookmark.renameOpen = false;
|
bookmark.renameOpen = false;
|
||||||
} catch (_e) {
|
} catch {
|
||||||
bookmark.renameOpen = false;
|
bookmark.renameOpen = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3905,7 +3905,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
this.readingSpeedPpm = data.pages_per_minute || 0;
|
this.readingSpeedPpm = data.pages_per_minute || 0;
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch {
|
||||||
this.readingSpeedPpm = 0;
|
this.readingSpeedPpm = 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function loadSettings(): Promise<ReaderSettings> {
|
|||||||
const settings = await response.json();
|
const settings = await response.json();
|
||||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(settings));
|
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(settings));
|
||||||
return settings;
|
return settings;
|
||||||
} catch (error) {
|
} catch {
|
||||||
const local = localStorage.getItem(LOCALSTORAGE_KEY);
|
const local = localStorage.getItem(LOCALSTORAGE_KEY);
|
||||||
return local ? JSON.parse(local) : getDefaultSettings();
|
return local ? JSON.parse(local) : getDefaultSettings();
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ export async function saveSettings(settings: Partial<ReaderSettings>): Promise<v
|
|||||||
body: JSON.stringify(settings),
|
body: JSON.stringify(settings),
|
||||||
});
|
});
|
||||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
|
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
|
||||||
} catch (error) {
|
} catch {
|
||||||
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
|
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+9
-4
@@ -3,6 +3,8 @@ declare module "turndown-plugin-gfm";
|
|||||||
|
|
||||||
// htmx is loaded as a classic script on every page and used from TS for
|
// htmx is loaded as a classic script on every page and used from TS for
|
||||||
// partial swaps (rendering-ownership rule: templ renders, JS swaps).
|
// partial swaps (rendering-ownership rule: templ renders, JS swaps).
|
||||||
|
// Members mirror the htmx 2.x public API that bookhoard actually calls;
|
||||||
|
// `trigger`'s element accepts a selector string, per the htmx docs.
|
||||||
interface HtmxAjaxOptions {
|
interface HtmxAjaxOptions {
|
||||||
target?: string;
|
target?: string;
|
||||||
swap?: string;
|
swap?: string;
|
||||||
@@ -10,9 +12,12 @@ interface HtmxAjaxOptions {
|
|||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Htmx {
|
||||||
|
ajax(method: string, url: string, options?: HtmxAjaxOptions): Promise<boolean>;
|
||||||
|
process(element: Element): void;
|
||||||
|
trigger(element: Element | string, eventName: string, detail?: Record<string, unknown>): void;
|
||||||
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
htmx: {
|
htmx: Htmx;
|
||||||
ajax(method: string, url: string, options?: HtmxAjaxOptions): Promise<boolean>;
|
|
||||||
process(element: Element): void;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
+3
-6
@@ -151,11 +151,6 @@ function text(id: string, value: string): void {
|
|||||||
if (el) el.textContent = value;
|
if (el) el.textContent = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveRange(): string {
|
|
||||||
const btn = document.querySelector(".stats-range-btn.btn-active") as HTMLElement | null;
|
|
||||||
return btn?.dataset.range || "7";
|
|
||||||
}
|
|
||||||
|
|
||||||
function markRangeButtons(active: string): void {
|
function markRangeButtons(active: string): void {
|
||||||
document.querySelectorAll<HTMLElement>(".stats-range-btn").forEach((btn) => {
|
document.querySelectorAll<HTMLElement>(".stats-range-btn").forEach((btn) => {
|
||||||
const isActive = btn.dataset.range === active;
|
const isActive = btn.dataset.range === active;
|
||||||
@@ -359,7 +354,9 @@ async function renderDailyCharts(daily: DailyEntry[]): Promise<void> {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (ctx) => {
|
label: (ctx) => {
|
||||||
const v = typeof ctx.parsed === "number" ? ctx.parsed : ctx.parsed.y;
|
// Scalar for bar charts, {x, y} points for line charts.
|
||||||
|
const parsed: number | { y: number } = ctx.parsed;
|
||||||
|
const v = typeof parsed === "number" ? parsed : parsed.y;
|
||||||
return `${Math.round(v * 10) / 10} ${unit}`;
|
return `${Math.round(v * 10) / 10} ${unit}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+9
-3
@@ -1,7 +1,7 @@
|
|||||||
// Toast notification system for backend errors
|
// Toast notification system for backend errors
|
||||||
// Displays toast notifications at the top of the page
|
// Displays toast notifications at the top of the page
|
||||||
|
|
||||||
type ToastType = "error" | "success" | "info";
|
type ToastType = "error" | "success" | "info" | "warning";
|
||||||
|
|
||||||
const TOAST_DEFAULT_DURATION = 5000;
|
const TOAST_DEFAULT_DURATION = 5000;
|
||||||
|
|
||||||
@@ -39,6 +39,12 @@ const getToastConfig = (type: ToastType) => {
|
|||||||
bgClass: "bg-blue-500/90",
|
bgClass: "bg-blue-500/90",
|
||||||
icon: "ℹ️",
|
icon: "ℹ️",
|
||||||
},
|
},
|
||||||
|
warning: {
|
||||||
|
// bg-yellow-500 (no opacity modifier): it must already appear in the
|
||||||
|
// templates for the Tailwind build to emit the class.
|
||||||
|
bgClass: "bg-yellow-500",
|
||||||
|
icon: "⚠️",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return configs[type];
|
return configs[type];
|
||||||
};
|
};
|
||||||
@@ -109,7 +115,7 @@ const parseXHRError = (xhr: XMLHttpRequest): string => {
|
|||||||
try {
|
try {
|
||||||
const response = JSON.parse(xhr.responseText);
|
const response = JSON.parse(xhr.responseText);
|
||||||
errorMessage = response.error || response.message || errorMessage;
|
errorMessage = response.error || response.message || errorMessage;
|
||||||
} catch (e) {
|
} catch {
|
||||||
errorMessage = xhr.responseText || errorMessage;
|
errorMessage = xhr.responseText || errorMessage;
|
||||||
}
|
}
|
||||||
return errorMessage;
|
return errorMessage;
|
||||||
@@ -154,7 +160,7 @@ const setupHTMXListeners = (): void => {
|
|||||||
customEvent.detail.target.innerHTML = "";
|
customEvent.detail.target.innerHTML = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Not valid JSON — ignore
|
// Not valid JSON — ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user