feat(mui): kit primitives — PageHeader, EmptyState, LoadingState, FilterBar, StatCard + DataTable onRowClick/rowDanger
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import ScopedCssBaseline from "@mui/material/ScopedCssBaseline";
|
import ScopedCssBaseline from "@mui/material/ScopedCssBaseline";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
@@ -17,9 +18,41 @@ import {
|
|||||||
DataTable,
|
DataTable,
|
||||||
DateField,
|
DateField,
|
||||||
type DataColumn,
|
type DataColumn,
|
||||||
|
PageHeader,
|
||||||
|
EmptyState,
|
||||||
|
LoadingState,
|
||||||
|
FilterBar,
|
||||||
|
StatCard,
|
||||||
} from "../ui";
|
} from "../ui";
|
||||||
import { useTheme } from "../../context/ThemeContext";
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
|
|
||||||
|
const BoxIcon = (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const InboxIcon = (
|
||||||
|
<svg
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
>
|
||||||
|
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
||||||
|
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
interface DemoRow {
|
interface DemoRow {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -53,6 +86,7 @@ export default function UiKit() {
|
|||||||
const [date, setDate] = useState("2026-06-06");
|
const [date, setDate] = useState("2026-06-06");
|
||||||
const [sortBy, setSortBy] = useState("name");
|
const [sortBy, setSortBy] = useState("name");
|
||||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
||||||
|
const [lastClicked, setLastClicked] = useState<string | null>(null);
|
||||||
|
|
||||||
const onSort = (key: string) => {
|
const onSort = (key: string) => {
|
||||||
if (sortBy === key) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
if (sortBy === key) setSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||||
@@ -185,6 +219,146 @@ export default function UiKit() {
|
|||||||
<Alert severity="info">Informace.</Alert>
|
<Alert severity="info">Informace.</Alert>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* ── Phase 6 primitives ── */}
|
||||||
|
<Card>
|
||||||
|
<Typography variant="h6">Kit — Phase 6 Primitives</Typography>
|
||||||
|
|
||||||
|
<SectionLabel>PageHeader — title only</SectionLabel>
|
||||||
|
<PageHeader title="Přehled projektů" />
|
||||||
|
|
||||||
|
<SectionLabel>PageHeader — with subtitle + actions</SectionLabel>
|
||||||
|
<PageHeader
|
||||||
|
title="Sklady"
|
||||||
|
subtitle="Správa skladových zásob"
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Button variant="outlined" color="inherit" size="small">
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
<Button size="small">Nová položka</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SectionLabel>EmptyState — icon + action</SectionLabel>
|
||||||
|
<EmptyState
|
||||||
|
icon={InboxIcon}
|
||||||
|
title="Žádné záznamy"
|
||||||
|
description="Zatím nebyly přidány žádné položky."
|
||||||
|
action={<Button size="small">Přidat první položku</Button>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SectionLabel>EmptyState — title only</SectionLabel>
|
||||||
|
<EmptyState title="Žádné výsledky" />
|
||||||
|
|
||||||
|
<SectionLabel>LoadingState</SectionLabel>
|
||||||
|
<LoadingState label="Načítám data…" />
|
||||||
|
|
||||||
|
<SectionLabel>FilterBar</SectionLabel>
|
||||||
|
<FilterBar>
|
||||||
|
<Box sx={{ width: 200 }}>
|
||||||
|
<TextField placeholder="Hledat…" />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ width: 160 }}>
|
||||||
|
<Select
|
||||||
|
value={selectVal}
|
||||||
|
onChange={setSelectVal}
|
||||||
|
options={[
|
||||||
|
{ value: "a", label: "Všechny stavy" },
|
||||||
|
{ value: "b", label: "Aktivní" },
|
||||||
|
{ value: "c", label: "Uzavřené" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Button size="small" variant="outlined" color="inherit">
|
||||||
|
Resetovat
|
||||||
|
</Button>
|
||||||
|
</FilterBar>
|
||||||
|
|
||||||
|
<SectionLabel>StatCard grid — all colors</SectionLabel>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
|
||||||
|
gap: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StatCard label="Celkem" value="1 240" icon={BoxIcon} />
|
||||||
|
<StatCard
|
||||||
|
label="Aktivní"
|
||||||
|
value="38"
|
||||||
|
icon={BoxIcon}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Dokončené"
|
||||||
|
value="112"
|
||||||
|
icon={BoxIcon}
|
||||||
|
color="success"
|
||||||
|
footer="↑ 12 oproti minulému měsíci"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Varování"
|
||||||
|
value="5"
|
||||||
|
icon={BoxIcon}
|
||||||
|
color="warning"
|
||||||
|
/>
|
||||||
|
<StatCard label="Chyby" value="2" icon={BoxIcon} color="error" />
|
||||||
|
<StatCard
|
||||||
|
label="Informace"
|
||||||
|
value="99"
|
||||||
|
icon={BoxIcon}
|
||||||
|
color="info"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<SectionLabel>
|
||||||
|
DataTable — onRowClick + rowDanger (row 2 = danger)
|
||||||
|
</SectionLabel>
|
||||||
|
{lastClicked && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
mb={1}
|
||||||
|
display="block"
|
||||||
|
>
|
||||||
|
Naposledy kliknuto: {lastClicked}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<DataTable<DemoRow>
|
||||||
|
columns={columns}
|
||||||
|
rows={DEMO_ROWS}
|
||||||
|
rowKey={(r) => r.id}
|
||||||
|
onRowClick={(r) => setLastClicked(r.name)}
|
||||||
|
rowDanger={(r) => r.id === 2}
|
||||||
|
rowInactive={(r) => r.id === 3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SectionLabel>Button as RouterLink</SectionLabel>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<Button
|
||||||
|
component={RouterLink}
|
||||||
|
to="/ui-kit"
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
RouterLink button (current page)
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href="https://mui.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
Anchor button
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</ScopedCssBaseline>
|
</ScopedCssBaseline>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import MuiButton, { type ButtonProps } from "@mui/material/Button";
|
import MuiButton, { type ButtonProps } from "@mui/material/Button";
|
||||||
|
import type { ElementType } from "react";
|
||||||
|
|
||||||
/** App Button: defaults to the brand primary contained style. */
|
/** App Button: defaults to the brand primary contained style.
|
||||||
export default function Button(props: ButtonProps) {
|
* Accepts `component` + any extra props the component requires
|
||||||
|
* (e.g. `component={RouterLink} to="/foo"` or `component="a" href="…"`).
|
||||||
|
*/
|
||||||
|
export default function Button<C extends ElementType = "button">(
|
||||||
|
props: ButtonProps<C, { component?: C }>,
|
||||||
|
) {
|
||||||
return <MuiButton variant="contained" color="primary" {...props} />;
|
return <MuiButton variant="contained" color="primary" {...props} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export interface DataTableProps<T> {
|
|||||||
rows: T[];
|
rows: T[];
|
||||||
rowKey: (row: T) => string | number;
|
rowKey: (row: T) => string | number;
|
||||||
rowInactive?: (row: T) => boolean;
|
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;
|
empty?: ReactNode;
|
||||||
/** Server-sort wiring: active key (matches a column's sortKey), direction, handler. */
|
/** Server-sort wiring: active key (matches a column's sortKey), direction, handler. */
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
@@ -45,6 +49,8 @@ export default function DataTable<T>({
|
|||||||
rows,
|
rows,
|
||||||
rowKey,
|
rowKey,
|
||||||
rowInactive,
|
rowInactive,
|
||||||
|
onRowClick,
|
||||||
|
rowDanger,
|
||||||
empty,
|
empty,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortDir,
|
sortDir,
|
||||||
@@ -115,7 +121,21 @@ export default function DataTable<T>({
|
|||||||
<TableRow
|
<TableRow
|
||||||
key={rowKey(row)}
|
key={rowKey(row)}
|
||||||
hover
|
hover
|
||||||
sx={rowInactive?.(row) ? { opacity: 0.55 } : undefined}
|
onClick={onRowClick ? () => 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) => (
|
{columns.map((c) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
|
|||||||
52
src/admin/ui/EmptyState.tsx
Normal file
52
src/admin/ui/EmptyState.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
export interface EmptyStateProps {
|
||||||
|
icon?: ReactNode;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
action?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Centered empty-state placeholder: icon, title, description, optional CTA. */
|
||||||
|
export default function EmptyState({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
action,
|
||||||
|
}: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
py: 6,
|
||||||
|
color: "text.secondary",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon && (
|
||||||
|
<Box
|
||||||
|
sx={{ color: "text.disabled", mb: 0.5, fontSize: 48, lineHeight: 1 }}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ fontWeight: 500, color: "text.secondary" }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
{description && (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{action && <Box sx={{ mt: 1.5 }}>{action}</Box>}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/admin/ui/FilterBar.tsx
Normal file
26
src/admin/ui/FilterBar.tsx
Normal file
@@ -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 (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 1.5,
|
||||||
|
alignItems: "flex-end",
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/admin/ui/LoadingState.tsx
Normal file
30
src/admin/ui/LoadingState.tsx
Normal file
@@ -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 (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
py: 8,
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={36} />
|
||||||
|
{label && (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/admin/ui/PageHeader.tsx
Normal file
48
src/admin/ui/PageHeader.tsx
Normal file
@@ -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 (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4">{title}</Typography>
|
||||||
|
{subtitle && (
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||||
|
{subtitle}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{actions && (
|
||||||
|
<Box
|
||||||
|
sx={{ display: "flex", alignItems: "center", gap: 1, flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
{actions}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
src/admin/ui/StatCard.tsx
Normal file
85
src/admin/ui/StatCard.tsx
Normal file
@@ -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 (
|
||||||
|
<Card>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "flex-start", gap: 1.5 }}>
|
||||||
|
{icon && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: iconBg,
|
||||||
|
color: iconColor,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box sx={{ minWidth: 0 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
sx={{ fontFamily: "'DM Mono', Menlo, monospace", lineHeight: 1.2 }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: ".06em",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{footer && (
|
||||||
|
<Box sx={{ mt: 1.5, pt: 1.5, borderTop: 1, borderColor: "divider" }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{footer}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,3 +16,9 @@ export { Tabs, TabPanel } from "./Tabs";
|
|||||||
export type { TabDef } from "./Tabs";
|
export type { TabDef } from "./Tabs";
|
||||||
export { default as Pagination } from "./Pagination";
|
export { default as Pagination } from "./Pagination";
|
||||||
export { default as DateField } from "./DateField";
|
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";
|
||||||
|
|||||||
Reference in New Issue
Block a user