diff --git a/src/admin/pages/UiKit.tsx b/src/admin/pages/UiKit.tsx
index afd95db..933f070 100644
--- a/src/admin/pages/UiKit.tsx
+++ b/src/admin/pages/UiKit.tsx
@@ -1,4 +1,5 @@
import { useState } from "react";
+import { Link as RouterLink } from "react-router-dom";
import ScopedCssBaseline from "@mui/material/ScopedCssBaseline";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
@@ -17,9 +18,41 @@ import {
DataTable,
DateField,
type DataColumn,
+ PageHeader,
+ EmptyState,
+ LoadingState,
+ FilterBar,
+ StatCard,
} from "../ui";
import { useTheme } from "../../context/ThemeContext";
+const BoxIcon = (
+
+);
+
+const InboxIcon = (
+
+);
+
interface DemoRow {
id: number;
name: string;
@@ -53,6 +86,7 @@ export default function UiKit() {
const [date, setDate] = useState("2026-06-06");
const [sortBy, setSortBy] = useState("name");
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
+ const [lastClicked, setLastClicked] = useState(null);
const onSort = (key: string) => {
if (sortBy === key) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
@@ -185,6 +219,146 @@ export default function UiKit() {
Informace.
+
+ {/* ── Phase 6 primitives ── */}
+
+ Kit — Phase 6 Primitives
+
+ PageHeader — title only
+
+
+ PageHeader — with subtitle + actions
+
+
+
+ >
+ }
+ />
+
+ EmptyState — icon + action
+ Přidat první položku}
+ />
+
+ EmptyState — title only
+
+
+ LoadingState
+
+
+ FilterBar
+
+
+
+
+
+
+
+
+
+
+ StatCard grid — all colors
+
+
+
+
+
+
+
+
+
+
+ DataTable — onRowClick + rowDanger (row 2 = danger)
+
+ {lastClicked && (
+
+ Naposledy kliknuto: {lastClicked}
+
+ )}
+
+ columns={columns}
+ rows={DEMO_ROWS}
+ rowKey={(r) => r.id}
+ onRowClick={(r) => setLastClicked(r.name)}
+ rowDanger={(r) => r.id === 2}
+ rowInactive={(r) => r.id === 3}
+ />
+
+ Button as RouterLink
+
+
+
+
+
diff --git a/src/admin/ui/Button.tsx b/src/admin/ui/Button.tsx
index 5fbaede..2c9e39c 100644
--- a/src/admin/ui/Button.tsx
+++ b/src/admin/ui/Button.tsx
@@ -1,6 +1,12 @@
import MuiButton, { type ButtonProps } from "@mui/material/Button";
+import type { ElementType } from "react";
-/** App Button: defaults to the brand primary contained style. */
-export default function Button(props: ButtonProps) {
+/** App Button: defaults to the brand primary contained style.
+ * Accepts `component` + any extra props the component requires
+ * (e.g. `component={RouterLink} to="/foo"` or `component="a" href="…"`).
+ */
+export default function Button(
+ props: ButtonProps,
+) {
return ;
}
diff --git a/src/admin/ui/DataTable.tsx b/src/admin/ui/DataTable.tsx
index 9ca02d4..2dc8fb7 100644
--- a/src/admin/ui/DataTable.tsx
+++ b/src/admin/ui/DataTable.tsx
@@ -32,6 +32,10 @@ export interface DataTableProps {
rows: T[];
rowKey: (row: T) => string | number;
rowInactive?: (row: T) => boolean;
+ /** When set, each row is clickable and shows a pointer cursor. */
+ onRowClick?: (row: T) => void;
+ /** When it returns true, the row is tinted with the error palette (danger highlight). */
+ rowDanger?: (row: T) => boolean;
empty?: ReactNode;
/** Server-sort wiring: active key (matches a column's sortKey), direction, handler. */
sortBy?: string;
@@ -45,6 +49,8 @@ export default function DataTable({
rows,
rowKey,
rowInactive,
+ onRowClick,
+ rowDanger,
empty,
sortBy,
sortDir,
@@ -115,7 +121,21 @@ export default function DataTable({
onRowClick(row) : undefined}
+ sx={[
+ onRowClick ? { cursor: "pointer" } : {},
+ rowDanger?.(row)
+ ? {
+ bgcolor: "error.light",
+ opacity: 0.88,
+ "&:hover": {
+ bgcolor: "error.light",
+ filter: "brightness(0.97)",
+ },
+ }
+ : {},
+ rowInactive?.(row) ? { opacity: 0.55 } : {},
+ ]}
>
{columns.map((c) => (
+ {icon && (
+
+ {icon}
+
+ )}
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+ {action && {action}}
+
+ );
+}
diff --git a/src/admin/ui/FilterBar.tsx b/src/admin/ui/FilterBar.tsx
new file mode 100644
index 0000000..26c9433
--- /dev/null
+++ b/src/admin/ui/FilterBar.tsx
@@ -0,0 +1,26 @@
+import type { ReactNode } from "react";
+import Box from "@mui/material/Box";
+
+export interface FilterBarProps {
+ children: ReactNode;
+}
+
+/**
+ * Flex row wrapper for filter controls — replaces admin-search-bar/admin-form-row-N.
+ * Children size themselves; callers wrap individual controls in a Box with width.
+ */
+export default function FilterBar({ children }: FilterBarProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/admin/ui/LoadingState.tsx b/src/admin/ui/LoadingState.tsx
new file mode 100644
index 0000000..74324c5
--- /dev/null
+++ b/src/admin/ui/LoadingState.tsx
@@ -0,0 +1,30 @@
+import Box from "@mui/material/Box";
+import CircularProgress from "@mui/material/CircularProgress";
+import Typography from "@mui/material/Typography";
+
+export interface LoadingStateProps {
+ label?: string;
+}
+
+/** Centered loading indicator — replaces admin-loading/admin-spinner CSS patterns. */
+export default function LoadingState({ label }: LoadingStateProps) {
+ return (
+
+
+ {label && (
+
+ {label}
+
+ )}
+
+ );
+}
diff --git a/src/admin/ui/PageHeader.tsx b/src/admin/ui/PageHeader.tsx
new file mode 100644
index 0000000..e38d59e
--- /dev/null
+++ b/src/admin/ui/PageHeader.tsx
@@ -0,0 +1,48 @@
+import type { ReactNode } from "react";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+
+export interface PageHeaderProps {
+ title: string;
+ subtitle?: string;
+ actions?: ReactNode;
+}
+
+/**
+ * Standard page header: title (+ optional subtitle) on the left,
+ * action controls on the right. No framer-motion — pages add their own entrance.
+ */
+export default function PageHeader({
+ title,
+ subtitle,
+ actions,
+}: PageHeaderProps) {
+ return (
+
+
+ {title}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ {actions && (
+
+ {actions}
+
+ )}
+
+ );
+}
diff --git a/src/admin/ui/StatCard.tsx b/src/admin/ui/StatCard.tsx
new file mode 100644
index 0000000..a5bd9fb
--- /dev/null
+++ b/src/admin/ui/StatCard.tsx
@@ -0,0 +1,85 @@
+import type { ReactNode } from "react";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import Card from "./Card";
+
+export type StatCardColor =
+ | "default"
+ | "primary"
+ | "success"
+ | "warning"
+ | "error"
+ | "info";
+
+export interface StatCardProps {
+ label: string;
+ value: ReactNode;
+ icon?: ReactNode;
+ color?: StatCardColor;
+ footer?: ReactNode;
+}
+
+/**
+ * KPI / metric tile. Dense Soft-SaaS look: tinted icon square, big value,
+ * small uppercase label, optional footer caption.
+ */
+export default function StatCard({
+ label,
+ value,
+ icon,
+ color = "default",
+ footer,
+}: StatCardProps) {
+ const iconBg = color === "default" ? "action.hover" : `${color}.light`;
+ const iconColor = color === "default" ? "text.secondary" : `${color}.main`;
+
+ return (
+
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
+ {value}
+
+
+ {label}
+
+
+
+ {footer && (
+
+
+ {footer}
+
+
+ )}
+
+ );
+}
diff --git a/src/admin/ui/index.ts b/src/admin/ui/index.ts
index 276f3d0..82d48d0 100644
--- a/src/admin/ui/index.ts
+++ b/src/admin/ui/index.ts
@@ -16,3 +16,9 @@ export { Tabs, TabPanel } from "./Tabs";
export type { TabDef } from "./Tabs";
export { default as Pagination } from "./Pagination";
export { default as DateField } from "./DateField";
+export { default as PageHeader } from "./PageHeader";
+export { default as EmptyState } from "./EmptyState";
+export { default as LoadingState } from "./LoadingState";
+export { default as FilterBar } from "./FilterBar";
+export { default as StatCard } from "./StatCard";
+export type { StatCardColor } from "./StatCard";