Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

@@ -0,0 +1,12 @@
import { useTranslation } from "react-i18next";
import BreadcrumbSimple from "~/components/ui/breadcrumbs/BreadcrumbSimple";
interface Props {
items: { title: string; routePath: string }[];
}
export default function DocBreadcrumb({ items }: Props) {
const { t } = useTranslation();
return <BreadcrumbSimple menu={[{ title: t("docs.title"), routePath: "/docs" }, ...items]} />;
}
@@ -0,0 +1,13 @@
import CustomCarousel from "../images/CustomCarousel";
interface Props {
items: {
type: string;
title: string;
src: string;
}[];
}
export default function DocCarousel({ items }: Props) {
return <CustomCarousel items={items} />;
}
@@ -0,0 +1,60 @@
import { t as i18nT } from "i18next";
import CheckIcon from "../icons/CheckIcon";
import XIcon from "../icons/XIcon";
interface Props {
items: { route: string; c: boolean; r: boolean; u: boolean; d: boolean }[];
}
export default function DocLifeCycle({ items }: Props) {
return (
<div className="not-prose flex flex-col">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div className="overflow-hidden shadow-xs ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-surface-neutral-secondary">
<tr>
<th scope="col" className="text-text-neutral-primary w-full px-2 py-1.5 text-left text-sm font-semibold sm:pl-6">
{i18nT("components.docs.lifecycle.route")}
</th>
<th scope="col" className="text-text-neutral-primary px-2 py-1.5 text-center text-sm font-semibold">
{i18nT("components.docs.lifecycle.create")}
</th>
<th scope="col" className="text-text-neutral-primary px-2 py-1.5 text-center text-sm font-semibold">
{i18nT("components.docs.lifecycle.read")}
</th>
<th scope="col" className="text-text-neutral-primary px-2 py-1.5 text-center text-sm font-semibold">
{i18nT("components.docs.lifecycle.update")}
</th>
<th scope="col" className="text-text-neutral-primary px-2 py-1.5 text-center text-sm font-semibold">
{i18nT("components.docs.lifecycle.delete")}
</th>
</tr>
</thead>
<tbody className="divide-border bg-surface-neutral-primary divide-y">
{items.map((item) => (
<tr key={item.route}>
<td className="text-text-neutral-primary whitespace-nowrap px-2 py-1.5 text-sm font-medium sm:pl-6">{item.route}</td>
<td className="text-text-neutral-tertiary whitespace-nowrap px-2 py-1.5 text-center text-sm">
{item.c ? <CheckIcon className="text-theme-500 mx-auto h-4 w-4" /> : <XIcon className="mx-auto h-4 w-4 text-gray-300" />}
</td>
<td className="text-text-neutral-tertiary whitespace-nowrap px-2 py-1.5 text-center text-sm">
{item.r ? <CheckIcon className="text-theme-500 mx-auto h-4 w-4" /> : <XIcon className="mx-auto h-4 w-4 text-gray-300" />}
</td>
<td className="text-text-neutral-tertiary whitespace-nowrap px-2 py-1.5 text-center text-sm">
{item.u ? <CheckIcon className="text-theme-500 mx-auto h-4 w-4" /> : <XIcon className="mx-auto h-4 w-4 text-gray-300" />}
</td>
<td className="text-text-neutral-tertiary whitespace-nowrap px-2 py-1.5 text-center text-sm">
{item.d ? <CheckIcon className="text-theme-500 mx-auto h-4 w-4" /> : <XIcon className="mx-auto h-4 w-4 text-gray-300" />}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,71 @@
import { t as i18nT } from "i18next";
import { Link } from "react-router";
interface Props {
items: { name: string; title: string; type?: ModelPropertyType; customType?: { name: string; route?: string }; description?: string; required: boolean }[];
}
export enum ModelPropertyType {
String,
DateTime,
Int,
Decimal,
Boolean,
UUID,
Cuid,
}
export default function DocPropertiesTable({ items }: Props) {
return (
<div className="not-prose flex flex-col">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div className="overflow-hidden shadow-xs ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-surface-neutral-secondary">
<tr>
<th scope="col" className="text-text-neutral-primary px-2 py-1.5 text-left text-sm font-semibold sm:pl-6">
{i18nT("components.docs.properties.name")}
</th>
<th scope="col" className="text-text-neutral-primary px-2 py-1.5 text-left text-sm font-semibold">
{i18nT("components.docs.properties.title")}
</th>
<th scope="col" className="text-text-neutral-primary px-2 py-1.5 text-left text-sm font-semibold">
{i18nT("components.docs.properties.type")}
</th>
<th scope="col" className="text-text-neutral-primary w-full px-2 py-1.5 text-left text-sm font-semibold">
{i18nT("components.docs.properties.description")}
</th>
</tr>
</thead>
<tbody className="divide-border bg-surface-neutral-primary divide-y">
{items.map((item) => (
<tr key={item.name}>
<td className="text-text-neutral-primary whitespace-nowrap px-2 py-1.5 text-sm font-medium sm:pl-6">
{item.name} {item.required && <span className="ml-1 text-red-500">*</span>}
</td>
<td className="text-text-neutral-tertiary whitespace-nowrap px-2 py-1.5 text-sm">{item.title}</td>
<td className="text-text-neutral-tertiary whitespace-nowrap px-2 py-1.5 text-sm">
{item.customType && (
<span className="font-bold">
{item.customType.route ? (
<Link className="text-theme-600 underline" to={item.customType.route}>
{item.customType.name}
</Link>
) : (
item.customType.name
)}
</span>
)}
{item.type !== undefined && ModelPropertyType[item.type]}
</td>
<td className="text-text-neutral-tertiary whitespace-nowrap px-2 py-1.5 text-sm">{item.description}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,12 @@
interface Props {
alt: string;
src: string;
}
export default function DocTutorialImage({ alt, src }: Props) {
return (
<div className="border-border-default border-2 border-dashed">
<img className="bg-surface-neutral-secondary/90 mx-auto rounded-lg object-cover shadow-2xl" alt={alt} src={src} />
</div>
);
}
@@ -0,0 +1,19 @@
interface Props {
title: string;
src: string;
}
export default function DocVideo({ title, src }: Props) {
return (
<div className="border-border-default h-full border-2 border-dashed">
<iframe
src={src}
title={title ?? ""}
frameBorder="0"
loading="lazy"
className="min-h-full w-full object-cover"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
></iframe>
</div>
);
}
@@ -0,0 +1,102 @@
import { Fragment, useState } from "react";
import { t as i18nT } from "i18next";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "../sheet";
import { Button } from "../button";
import Logo from "~/components/brand/Logo";
import { ScrollArea } from "../scroll-area";
import { Link } from "react-router";
import clsx from "clsx";
import { SideBarItem } from "~/application/sidebar/SidebarItem";
import { getUiIdAttributes, UI_ACTION_IDS } from "~/application/ui/uiRegistry";
import { MenuIcon } from "~/components/ui/icons/vendor";
export function MobileNav({ items }: { items: SideBarItem[] }) {
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
data-ui-id={`${UI_ACTION_IDS.sidebarTrigger}.docsMobile`}
className="px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden"
>
<MenuIcon className="text-text-neutral-tertiary h-6 w-6" />
<span className="sr-only">{i18nT("components.docsMobileNav.toggleMenu")}</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="pr-0">
<SheetHeader className="sr-only">
<SheetTitle>{i18nT("docs.mobileNav.title")}</SheetTitle>
<SheetDescription>{i18nT("docs.mobileNav.description")}</SheetDescription>
</SheetHeader>
<Logo className="mr-2" />
<ScrollArea className="my-4 h-[calc(100vh-100px)] pb-10 pl-3">
<div className="flex flex-col space-y-2">
{items.map((item, index) => (
<div key={index} className="flex flex-col space-y-3 pt-4" {...(!item.path ? getUiIdAttributes(item) : {})}>
{item.items && item.items.length > 0 ? (
<button type="button" aria-expanded="true" className="text-left font-medium" {...getUiIdAttributes(item)}>
{item.title}
</button>
) : item.path ? (
<Link to={item.path} {...getUiIdAttributes(item)}>
<h4 className="font-medium">{item.title}</h4>
</Link>
) : (
<h4 className="font-medium">{item.title}</h4>
)}
{item.items?.map((item) => (
<Fragment key={item.path}>
{!item.disabled &&
(item.path ? (
<MobileLink href={item.path} onOpenChange={setOpen} className="text-text-neutral-tertiary" uiItem={item}>
{item.title}
{item.side && (
<span className="ml-2 rounded-md bg-[#adfa1d] px-1.5 py-0.5 text-xs leading-none text-[#000000] no-underline group-hover:no-underline">
{item.side}
</span>
)}
</MobileLink>
) : (
item.title
))}
</Fragment>
))}
</div>
))}
</div>
</ScrollArea>
</SheetContent>
</Sheet>
);
}
function MobileLink({
href,
onOpenChange,
className,
children,
uiItem,
...props
}: {
href: string;
onOpenChange: (open: boolean) => void;
className: string;
children: React.ReactNode;
uiItem?: SideBarItem;
}) {
return (
<Link
to={href}
onClick={() => {
onOpenChange?.(false);
}}
className={clsx(className)}
{...(uiItem ? getUiIdAttributes(uiItem) : {})}
{...props}
>
{children}
</Link>
);
}
@@ -0,0 +1,103 @@
import "@testing-library/jest-dom";
import fs from "node:fs";
import path from "node:path";
import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router";
import { describe, expect, it, vi } from "vitest";
import { resolveRepoPath } from "~/utils/testing/repoPaths";
import { MobileNav } from "./DocsMobileNav";
import { DocsSidebarNav } from "./DocsSidebarNav";
vi.mock("~/utils/data/useRootData", () => ({
useRootData: () => ({
appConfiguration: {
branding: {},
},
}),
}));
describe("docs navigation", () => {
it("renders parents with children as expand-only controls", () => {
render(
<MemoryRouter>
<DocsSidebarNav
items={[
{
title: "Settings",
path: "/settings",
items: [{ title: "Overview", path: "/settings" }],
},
]}
/>
</MemoryRouter>
);
expect(screen.getByRole("button", { name: "Settings" })).toHaveAttribute("aria-expanded", "true");
expect(screen.getByRole("link", { name: "Overview" })).toHaveAttribute("href", "/settings");
expect(screen.queryByRole("link", { name: "Settings" })).not.toBeInTheDocument();
});
it("does not render disabled docs items as links", () => {
render(
<MemoryRouter>
<DocsSidebarNav
items={[
{
title: "Settings",
items: [{ title: "Disabled Plugin", path: "/settings/plugin", disabled: true }],
},
]}
/>
</MemoryRouter>
);
expect(screen.queryByRole("link", { name: "Disabled Plugin" })).not.toBeInTheDocument();
expect(screen.getByText("Disabled Plugin")).toHaveAttribute("aria-disabled", "true");
});
it("renders an accessible mobile docs sheet title and description", () => {
render(
<MemoryRouter>
<MobileNav items={[]} />
</MemoryRouter>
);
fireEvent.click(screen.getByRole("button", { name: "Toggle menu" }));
const dialog = screen.getByRole("dialog", { name: "Docs navigation" });
const heading = screen.getByRole("heading", { name: "Docs navigation" });
const description = screen.getByText("Displays the mobile docs navigation.");
expect(dialog).toHaveAccessibleDescription("Displays the mobile docs navigation.");
expect(heading.closest('[data-slot="sheet-header"]')).toHaveClass("sr-only");
expect(description.closest('[data-slot="sheet-header"]')).toHaveClass("sr-only");
});
it("keeps mobile docs parents as non-link controls", () => {
render(
<MemoryRouter>
<MobileNav
items={[
{
title: "Settings",
path: "/settings",
items: [{ title: "Overview", path: "/settings" }],
},
]}
/>
</MemoryRouter>
);
fireEvent.click(screen.getByRole("button", { name: "Toggle menu" }));
expect(screen.getByRole("button", { name: "Settings" })).toHaveAttribute("aria-expanded", "true");
expect(screen.getByRole("link", { name: "Overview" })).toHaveAttribute("href", "/settings");
expect(screen.queryByRole("link", { name: "Settings" })).not.toBeInTheDocument();
});
it("adds an explicit overview child for knowledge-base category paths", () => {
const source = fs.readFileSync(resolveRepoPath(path.join("apps/web/app/modules/knowledgeBase/components/layouts/KbDocsLayout.tsx")), "utf8");
expect(source).toContain('items: [{ title: t("shared.overview"), path: category.href }]');
});
});
@@ -0,0 +1,82 @@
import { Link, useLocation } from "react-router";
import clsx from "clsx";
import { SideBarItem } from "~/application/sidebar/SidebarItem";
import { getUiIdAttributes } from "~/application/ui/uiRegistry";
export function DocsSidebarNav({ items }: { items: SideBarItem[] }) {
return (
<div>
{items.length > 0 ? (
<div className="w-full">
{items.map((item, index) => (
<div key={index} className={clsx(item.items && item.items.length > 0 && "pb-4")} {...(!item.path ? getUiIdAttributes(item) : {})}>
{item.items && item.items.length > 0 ? (
<button type="button" aria-expanded="true" className="mb-1 rounded-md px-2 py-1 text-left font-semibold" {...getUiIdAttributes(item)}>
{item.title}
</button>
) : item.path ? (
<Link to={item.path} className="hover:underline" {...getUiIdAttributes(item)}>
<h4 className="mb-1 rounded-md px-2 py-1 font-semibold">{item.title}</h4>
</Link>
) : (
<h4 className="mb-1 rounded-md px-2 py-1 font-semibold">{item.title}</h4>
)}
{item.items && item.items.length > 0 && <DocsSidebarNavItems items={item.items} />}
</div>
))}
</div>
) : null}
</div>
);
}
function DocsSidebarNavItems({ items }: { items: SideBarItem[] }) {
const location = useLocation();
const pathname = location.pathname;
return items?.length ? (
<div className="grid grid-flow-row auto-rows-max">
{items.map((item, index) =>
item.path && !item.disabled ? (
<Link
key={index}
to={item.path}
className={clsx(
"group flex w-full items-center rounded-md border border-transparent px-2 py-1 text-sm hover:underline",
item.disabled && "cursor-not-allowed opacity-60",
pathname === item.path ? "text-text-neutral-primary font-medium" : "text-text-neutral-tertiary"
)}
target={item.target}
rel={item.target ? "noreferrer" : ""}
{...getUiIdAttributes(item)}
// preventScrollReset
>
{item.title}
{item.side && (
<span className="ml-2 rounded-md bg-[#adfa1d] px-1.5 py-0.5 text-xs leading-none text-[#000000] no-underline group-hover:no-underline">
{item.side}
</span>
)}
</Link>
) : (
<span
key={index}
aria-disabled={item.disabled ? "true" : undefined}
className={clsx(
"text-text-neutral-tertiary flex w-full items-center rounded-md p-2",
!item.disabled && "hover:underline",
item.disabled && "cursor-not-allowed opacity-60"
)}
{...getUiIdAttributes(item)}
>
{item.title}
{item.side && (
<span className="bg-surface-neutral-secondary text-text-neutral-tertiary ml-2 rounded-md px-1.5 py-0.5 text-xs leading-none no-underline group-hover:no-underline">
{item.side}
</span>
)}
</span>
)
)}
</div>
) : null;
}
@@ -0,0 +1,84 @@
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { ScrollArea } from "~/components/ui/scroll-area";
import { DashboardTableOfContents } from "./toc";
import clsx from "clsx";
export type PreviewComponentProps = {
title: string;
description: string;
variants:
| {
id: string;
title: string;
component: ReactNode;
code?: string;
description?: string;
}[]
| {
id: string;
title: string;
iframe: string;
code?: string;
description?: string;
}[];
className?: string;
withToc?: boolean;
overflow?: "hidden" | "visible";
};
export default function PreviewComponent({ title, description, variants, className, withToc = true, overflow = "hidden" }: PreviewComponentProps) {
const { t } = useTranslation();
return (
<main className={clsx("relative pb-6 lg:gap-10 lg:pb-8", withToc && "xl:grid xl:grid-cols-[1fr_150px]")}>
<div className="space-y-8 overflow-hidden">
<div>
<h1 className="scroll-m-20 text-4xl font-bold tracking-tight">{title}</h1>
<p className="text-muted-foreground mt-1 text-lg">{description}</p>
</div>
{variants.map((variant, index) => (
<div key={index} className="space-y-2">
<div className="space-y-1">
<h3 id={variant.id} className="text-xl font-bold tracking-tight">
{variant.title}
</h3>
<p className="text-muted-foreground text-sm">{variant.description}</p>
</div>
<div
className={clsx(
"border-muted-foreground flex justify-center rounded-lg border-2 border-dashed",
className,
overflow === "hidden" ? "overflow-hidden" : "overflow-visible",
"component" in variant && "p-6"
)}
>
{"iframe" in variant ? <iframe src={variant.iframe} title={title} className="h-96 w-full border-0" /> : variant.component}
</div>
</div>
))}
</div>
{withToc && (
<div className="hidden text-sm xl:block">
<div className="sticky top-16 pt-4">
<ScrollArea className="pb-10">
<div className="sticky top-16 -mt-10 h-[calc(100vh-3.5rem)] py-12">
<DashboardTableOfContents
// title={`${title} - Variants`}
title={t("docs.onThisPage")}
toc={{
items: variants
.map((f) => f)
.map((item) => ({
title: item.title,
url: `#${item.id}`,
})),
}}
/>
</div>
</ScrollArea>
</div>
</div>
)}
</main>
);
}
@@ -0,0 +1,120 @@
import clsx from "clsx";
import { useEffect, useMemo, useState } from "react";
import ButtonEvent from "../buttons/ButtonEvent";
interface Item {
title: string;
url: string;
items?: Item[];
}
interface Items {
items?: Item[];
}
interface TocProps {
title: string;
toc: Items;
}
export function DashboardTableOfContents({ title, toc }: TocProps) {
const itemIds = useMemo(
() =>
toc.items
? toc.items
.flatMap((item) => [item.url, item?.items?.map((item) => item.url)])
.flat()
.filter(Boolean)
.map((id) => id?.split("#")[1])
: [],
[toc]
);
const activeHeading = useActiveItem(itemIds.filter(Boolean) as string[]);
const mounted = useMounted();
if (!toc?.items || !mounted) {
return null;
}
return (
<div className="space-y-2">
<p className="font-medium">{title}</p>
<Tree tree={toc} activeItem={activeHeading} />
</div>
);
}
function useActiveItem(itemIds: string[]) {
const [activeId, setActiveId] = useState<undefined | string>(undefined);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id ?? undefined);
}
});
},
{ rootMargin: `0% 0% -80% 0%` }
);
itemIds?.forEach((id) => {
const element = document.getElementById(id);
if (element) {
observer.observe(element);
}
});
return () => {
itemIds?.forEach((id) => {
const element = document.getElementById(id);
if (element) {
observer.unobserve(element);
}
});
};
}, [itemIds]);
return activeId;
}
interface TreeProps {
tree: Items;
level?: number;
activeItem?: string;
}
function Tree({ tree, level = 1, activeItem }: TreeProps) {
return tree?.items?.length && level < 3 ? (
<ul className={clsx("m-0 list-none", { "pl-4": level !== 1 })}>
{tree.items.map((item, index) => {
return (
<li key={index} className={clsx("mt-0 pt-2")}>
<ButtonEvent
event={{ action: "click", category: "toc", label: item.title, value: item.url }}
to={item.url}
className={clsx(
"hover:text-text-neutral-primary inline-block no-underline transition-colors",
item.url === `#${activeItem}` ? "text-text-neutral-primary font-medium" : "text-text-neutral-tertiary"
)}
>
{item.title}
</ButtonEvent>
{item.items?.length ? <Tree tree={item} level={level + 1} activeItem={activeItem} /> : null}
</li>
);
})}
</ul>
) : null;
}
function useMounted() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted;
}