Files
Termix/plugins/homepage/src/frontend/widgets/LinkTreeWidget.tsx
T

128 lines
3.4 KiB
TypeScript

import { useTranslation } from "@termix/plugin-sdk/frontend";
import { useRef } from "react";
import { List, ExternalLink } from "lucide-react";
import { registerWidget } from "./WidgetRegistry";
import type {
LinkTreeConfig,
LinkTreeLink,
WidgetComponentProps,
} from "../types.js";
import { GRID_SIZE } from "../types.js";
import { WidgetTitle } from "@termix/plugin-sdk/ui";
function TreeLink({
link,
compact,
isReadOnly,
}: {
link: LinkTreeLink;
compact: boolean;
isReadOnly?: boolean;
}) {
const mouseDownRef = useRef<{ x: number; y: number } | null>(null);
const content = (
<div
className={`group flex items-center gap-1.5 ${compact ? "py-0.5" : "py-1"} pl-2 pr-1 hover:bg-muted/40 transition-colors overflow-hidden`}
>
<ExternalLink
size={9}
className="shrink-0 text-muted-foreground group-hover:text-accent-brand transition-colors"
/>
<div className="flex flex-col min-w-0">
<span className="text-[10px] font-medium text-foreground truncate">
{link.label}
</span>
{!compact && link.description && (
<span className="text-[9px] text-muted-foreground truncate">
{link.description}
</span>
)}
</div>
</div>
);
if (isReadOnly || !link.url) return content;
return (
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="block no-underline"
onMouseDown={(e) => {
mouseDownRef.current = { x: e.clientX, y: e.clientY };
}}
onClick={(e) => {
const s = mouseDownRef.current;
if (
s &&
(Math.abs(e.clientX - s.x) > 4 || Math.abs(e.clientY - s.y) > 4)
)
e.preventDefault();
mouseDownRef.current = null;
}}
>
{content}
</a>
);
}
function LinkTreeWidget({
widget,
config,
isReadOnly,
}: WidgetComponentProps<LinkTreeConfig>) {
const { t } = useTranslation();
const { sections, compact } = config;
if (sections.length === 0) {
return (
<div className="flex items-center justify-center w-full h-full text-xs text-muted-foreground">
{t("homepage.noSections")}
</div>
);
}
return (
<div className="flex flex-col w-full h-full overflow-hidden">
<WidgetTitle title={widget.title} icon={<List size={11} />} />
<div className="flex-1 overflow-auto">
{sections.map((section, si) => (
<div key={si}>
{section.heading && (
<div className="flex items-center gap-1.5 px-2 pt-2 pb-0.5 border-b border-border/30">
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider truncate">
{section.heading}
</span>
</div>
)}
{section.links.map((link, li) => (
<TreeLink
key={li}
link={link}
compact={compact}
isReadOnly={isReadOnly}
/>
))}
</div>
))}
</div>
</div>
);
}
registerWidget<LinkTreeConfig>({
id: "link_tree",
name: "Link Tree",
description: "Grouped links organized into labeled sections",
category: "links",
icon: <List size={14} />,
defaultConfig: { sections: [], compact: false },
defaultSize: { w: GRID_SIZE * 10, h: GRID_SIZE * 12 },
minSize: { w: GRID_SIZE * 4, h: GRID_SIZE * 4 },
component: LinkTreeWidget,
});
export { LinkTreeWidget };