tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..b463afd
--- /dev/null
+++ b/src/components/ui/tabs.tsx
@@ -0,0 +1,91 @@
+"use client"
+
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { Tabs as TabsPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ orientation = "horizontal",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+const tabsListVariants = cva(
+ "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
+ {
+ variants: {
+ variant: {
+ default: "bg-muted",
+ line: "gap-1 bg-transparent",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function TabsList({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..e67d8fe
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..d4d2254
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,55 @@
+import * as React from "react"
+import { Tooltip as TooltipPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/src/hooks/useModuleRoute.test.ts b/src/hooks/useModuleRoute.test.ts
new file mode 100644
index 0000000..dc8ce49
--- /dev/null
+++ b/src/hooks/useModuleRoute.test.ts
@@ -0,0 +1,43 @@
+import { describe, it, expect, beforeEach, afterEach } from "vitest";
+import { renderHook, act } from "@testing-library/react";
+import { useModuleRoute } from "./useModuleRoute";
+
+describe("useModuleRoute", () => {
+ const original = window.location.pathname;
+
+ beforeEach(() => {
+ window.history.pushState({}, "", "/my-module");
+ });
+
+ afterEach(() => {
+ window.history.pushState({}, "", original);
+ });
+
+ it("returns an empty subPath at the module root", () => {
+ const { result } = renderHook(() => useModuleRoute("/my-module"));
+ expect(result.current.subPath).toBe("");
+ });
+
+ it("strips the base path from deeper routes", () => {
+ window.history.pushState({}, "", "/my-module/items/42");
+ const { result } = renderHook(() => useModuleRoute("/my-module"));
+ expect(result.current.subPath).toBe("items/42");
+ });
+
+ it("does not match a sibling module whose path shares the prefix", () => {
+ window.history.pushState({}, "", "/my-module-foo/items");
+ const { result } = renderHook(() => useModuleRoute("/my-module"));
+ expect(result.current.subPath).toBe("");
+ });
+
+ it("updates subPath when popstate fires", () => {
+ const { result } = renderHook(() => useModuleRoute("/my-module"));
+
+ act(() => {
+ window.history.pushState({}, "", "/my-module/settings");
+ window.dispatchEvent(new PopStateEvent("popstate"));
+ });
+
+ expect(result.current.subPath).toBe("settings");
+ });
+});
diff --git a/src/hooks/useModuleRoute.ts b/src/hooks/useModuleRoute.ts
new file mode 100644
index 0000000..ce379cc
--- /dev/null
+++ b/src/hooks/useModuleRoute.ts
@@ -0,0 +1,41 @@
+import { useCallback, useEffect, useState } from "react";
+
+/**
+ * Minimal in-module router. The shell owns URL-level routing; this hook
+ * just reads the browser's pathname and re-renders on `popstate`.
+ *
+ * The shell's module frame dispatches a synthetic PopStateEvent whenever
+ * React Router navigates (see platform-backoffice-shell module-frame.tsx),
+ * so -based sidebar entries and programmatic navigate() calls both
+ * wake this hook up automatically.
+ *
+ * `basePath` is the module's root route (matches `route` in module.json
+ * and `path` in each navigation entry). Everything after it is treated as
+ * the sub-route, e.g. for basePath="/my-module":
+ * /my-module → subPath = ""
+ * /my-module/items → subPath = "items"
+ * /my-module/items/42 → subPath = "items/42"
+ */
+export function useModuleRoute(basePath: string) {
+ const [pathname, setPathname] = useState(() => window.location.pathname);
+
+ useEffect(() => {
+ const onChange = () => setPathname(window.location.pathname);
+ window.addEventListener("popstate", onChange);
+ return () => window.removeEventListener("popstate", onChange);
+ }, []);
+
+ const navigate = useCallback((path: string) => {
+ if (window.location.pathname === path) return;
+ window.history.pushState({}, "", path);
+ window.dispatchEvent(new PopStateEvent("popstate"));
+ }, []);
+
+ const matchesBase =
+ pathname === basePath || pathname.startsWith(basePath + "/");
+ const subPath = matchesBase
+ ? pathname.slice(basePath.length).replace(/^\/+/, "")
+ : "";
+
+ return { pathname, subPath, navigate };
+}