mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-03 21:02:40 +02:00
feat: introduce citation components from tool-ui with hover popover functionality and schema validation for enhanced citation management
This commit is contained in:
parent
0e3f5d804c
commit
9eab427b56
14 changed files with 1168 additions and 0 deletions
|
|
@ -0,0 +1,27 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const AspectRatioSchema = z
|
||||
.enum(["auto", "1:1", "4:3", "16:9", "9:16"])
|
||||
.default("auto");
|
||||
|
||||
export type AspectRatio = z.infer<typeof AspectRatioSchema>;
|
||||
|
||||
export const MediaFitSchema = z.enum(["cover", "contain"]).default("cover");
|
||||
|
||||
export type MediaFit = z.infer<typeof MediaFitSchema>;
|
||||
|
||||
export const RATIO_CLASS_MAP: Record<AspectRatio, string> = {
|
||||
auto: "",
|
||||
"1:1": "aspect-square",
|
||||
"4:3": "aspect-[4/3]",
|
||||
"16:9": "aspect-video",
|
||||
"9:16": "aspect-[9/16]",
|
||||
};
|
||||
|
||||
export function getRatioClass(ratio: AspectRatio): string {
|
||||
return RATIO_CLASS_MAP[ratio];
|
||||
}
|
||||
|
||||
export function getFitClass(fit: MediaFit): string {
|
||||
return fit === "cover" ? "object-cover" : "object-contain";
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
export function formatDuration(durationMs: number): string {
|
||||
const totalSeconds = Math.round(durationMs / 1000);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
}
|
||||
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size in bytes to human-readable string.
|
||||
* @example formatFileSize(1024) => "1 KB"
|
||||
* @example formatFileSize(1536000) => "1.5 MB"
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
const units = ["KB", "MB", "GB"];
|
||||
let size = bytes / 1024;
|
||||
let unit = 0;
|
||||
while (size >= 1024 && unit < units.length - 1) {
|
||||
size /= 1024;
|
||||
unit += 1;
|
||||
}
|
||||
return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;
|
||||
}
|
||||
19
surfsense_web/components/tool-ui/shared/media/index.ts
Normal file
19
surfsense_web/components/tool-ui/shared/media/index.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
export {
|
||||
AspectRatioSchema,
|
||||
MediaFitSchema,
|
||||
RATIO_CLASS_MAP,
|
||||
getRatioClass,
|
||||
getFitClass,
|
||||
type AspectRatio,
|
||||
type MediaFit,
|
||||
} from "./aspect-ratio";
|
||||
|
||||
export { OVERLAY_GRADIENT } from "./overlay-gradient";
|
||||
|
||||
export { formatDuration, formatFileSize } from "./format-utils";
|
||||
|
||||
export { sanitizeHref } from "./sanitize-href";
|
||||
export {
|
||||
resolveSafeNavigationHref,
|
||||
openSafeNavigationHref,
|
||||
} from "./safe-navigation";
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
export const OVERLAY_GRADIENT = `linear-gradient(
|
||||
to bottom,
|
||||
hsl(0, 0%, 0%) 0%,
|
||||
hsla(0, 0%, 0%, 0.987) 8.3%,
|
||||
hsla(0, 0%, 0%, 0.951) 16.6%,
|
||||
hsla(0, 0%, 0%, 0.896) 24.6%,
|
||||
hsla(0, 0%, 0%, 0.825) 32.5%,
|
||||
hsla(0, 0%, 0%, 0.741) 40.1%,
|
||||
hsla(0, 0%, 0%, 0.648) 47.6%,
|
||||
hsla(0, 0%, 0%, 0.55) 54.8%,
|
||||
hsla(0, 0%, 0%, 0.45) 61.7%,
|
||||
hsla(0, 0%, 0%, 0.352) 68.3%,
|
||||
hsla(0, 0%, 0%, 0.259) 74.5%,
|
||||
hsla(0, 0%, 0%, 0.175) 80.4%,
|
||||
hsla(0, 0%, 0%, 0.104) 86%,
|
||||
hsla(0, 0%, 0%, 0.049) 91.1%,
|
||||
hsla(0, 0%, 0%, 0.013) 95.8%,
|
||||
hsla(0, 0%, 0%, 0) 100%
|
||||
)` as const;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { sanitizeHref } from "./sanitize-href";
|
||||
|
||||
export function resolveSafeNavigationHref(
|
||||
...candidates: Array<string | null | undefined>
|
||||
): string | undefined {
|
||||
for (const candidate of candidates) {
|
||||
const safeHref = sanitizeHref(candidate ?? undefined);
|
||||
if (safeHref) {
|
||||
return safeHref;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function openSafeNavigationHref(href: string | undefined): boolean {
|
||||
if (!href || typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
window.open(href, "_blank", "noopener,noreferrer");
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
export function sanitizeHref(href?: string): string | undefined {
|
||||
if (!href) return undefined;
|
||||
const candidate = href.trim();
|
||||
if (!candidate) return undefined;
|
||||
|
||||
if (
|
||||
candidate.startsWith("/") ||
|
||||
candidate.startsWith("./") ||
|
||||
candidate.startsWith("../") ||
|
||||
candidate.startsWith("?") ||
|
||||
candidate.startsWith("#")
|
||||
) {
|
||||
if (candidate.startsWith("//")) return undefined;
|
||||
// eslint-disable-next-line no-control-regex -- intentionally matching control characters
|
||||
if (/[\u0000-\u001F\u007F]/.test(candidate)) return undefined;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(candidate);
|
||||
if (url.protocol === "http:" || url.protocol === "https:") {
|
||||
return url.toString();
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue