8.6 מנטין מתקדם פתרון
פתרון - מנטין מתקדם¶
פתרון תרגיל 1¶
import {
createTheme,
MantineColorsTuple,
MantineProvider,
CSSVariablesResolver,
Button,
TextInput,
Card,
Title,
Text,
Stack,
Group,
Container,
Paper,
} from "@mantine/core";
const projectBlue: MantineColorsTuple = [
"#e8f4fd",
"#d1e8fa",
"#a3d1f5",
"#72b8f0",
"#47a2eb",
"#2993e8",
"#1a8be7",
"#0e78cd",
"#006bb8",
"#005ca3",
];
const theme = createTheme({
colors: {
projectBlue,
},
primaryColor: "projectBlue",
primaryShade: { light: 6, dark: 8 },
fontFamily: "Rubik, sans-serif",
fontFamilyMonospace: "JetBrains Mono, monospace",
headings: {
fontFamily: "Heebo, sans-serif",
fontWeight: "700",
},
defaultRadius: "md",
cursorType: "pointer",
components: {
Button: {
defaultProps: {
radius: "md",
},
},
TextInput: {
defaultProps: {
variant: "filled",
},
},
Card: {
defaultProps: {
shadow: "sm",
withBorder: true,
},
},
},
});
const resolver: CSSVariablesResolver = (theme) => ({
variables: {
"--sidebar-width": "260px",
"--header-height": "64px",
"--content-max-width": "1200px",
},
light: {
"--app-bg": theme.colors.gray[0],
"--card-bg": "#ffffff",
"--border-color": theme.colors.gray[3],
},
dark: {
"--app-bg": theme.colors.dark[7],
"--card-bg": theme.colors.dark[6],
"--border-color": theme.colors.dark[4],
},
});
function DemoPage() {
return (
<Container size="sm" py="xl">
<Stack gap="lg">
<Title order={1}>ניהול פרויקטים</Title>
<Text c="dimmed">מערכת ניהול פרויקטים עם theme מותאם</Text>
<Card p="lg">
<Title order={3} mb="md">פרויקט חדש</Title>
<Stack gap="md">
<TextInput label="שם הפרויקט" placeholder="הכנס שם" />
<TextInput label="תיאור" placeholder="הכנס תיאור" />
<Group>
<Button>צור פרויקט</Button>
<Button variant="outline">ביטול</Button>
</Group>
</Stack>
</Card>
</Stack>
</Container>
);
}
function App() {
return (
<MantineProvider theme={theme} cssVariablesResolver={resolver}>
<DemoPage />
</MantineProvider>
);
}
- סקלת צבעים מותאמת עם 10 גוונים
- TextInput בברירת מחדל variant="filled" כמוגדר ב-theme
- Card מגיע עם shadow ו-border אוטומטית
פתרון תרגיל 2¶
/* CustomStyles.module.css */
.inputRoot {
margin-bottom: 16px;
}
.inputLabel {
font-weight: 700;
font-size: 13px;
color: var(--mantine-color-gray-7);
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.inputField {
border: 2px solid var(--mantine-color-blue-4);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.inputField:focus {
border-color: var(--mantine-color-blue-6);
box-shadow: 0 0 0 3px rgba(66, 117, 199, 0.15);
}
.inputError {
font-size: 12px;
margin-top: 4px;
font-weight: 500;
}
.buttonRoot {
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 600;
}
.cardRoot {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.cardRoot:hover {
transform: translateY(-2px);
box-shadow: var(--mantine-shadow-lg);
}
import { TextInput, Button, Card, Title, Text, Stack, Group, SimpleGrid } from "@mantine/core";
import styles from "./CustomStyles.module.css";
function StyledComponents() {
return (
<Stack gap="xl" p="xl" maw={800} mx="auto">
<Title order={2}>קומפוננטות מעוצבות</Title>
{/* TextInput מותאם */}
<TextInput
label="שם הפרויקט"
placeholder="הכנס שם..."
error="שדה חובה"
classNames={{
root: styles.inputRoot,
label: styles.inputLabel,
input: styles.inputField,
error: styles.inputError,
}}
/>
<TextInput
label="אימייל"
placeholder="your@email.com"
classNames={{
root: styles.inputRoot,
label: styles.inputLabel,
input: styles.inputField,
}}
/>
{/* Button מותאם */}
<Group>
<Button classNames={{ root: styles.buttonRoot }}>
צור פרויקט
</Button>
<Button variant="outline" classNames={{ root: styles.buttonRoot }}>
ביטול
</Button>
</Group>
{/* Card מותאם */}
<SimpleGrid cols={3}>
{["פרויקט א", "פרויקט ב", "פרויקט ג"].map((name) => (
<Card
key={name}
p="lg"
classNames={{ root: styles.cardRoot }}
withBorder
>
<Title order={4} mb="xs">{name}</Title>
<Text size="sm" c="dimmed">תיאור קצר של הפרויקט</Text>
</Card>
))}
</SimpleGrid>
</Stack>
);
}
- classNames מאפשר גישה לכל חלק פנימי של הקומפוננטה
- CSS Modules מבטיח שהסגנונות לא יתנגשו
- משתני Mantine בשימוש בתוך ה-CSS
פתרון תרגיל 3¶
/* InfoCard.module.css */
.root {
display: flex;
gap: var(--mantine-spacing-md);
padding: var(--mantine-spacing-lg);
border-radius: var(--mantine-radius-md);
background-color: var(--mantine-color-body);
border: 1px solid var(--mantine-color-default-border);
transition: all 0.2s ease;
}
.root[data-variant="highlighted"] {
background-color: var(--mantine-primary-color-light);
border-color: var(--mantine-primary-color-filled);
}
.root[data-variant="outlined"] {
background-color: transparent;
border-width: 2px;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: var(--mantine-radius-md);
background-color: var(--mantine-primary-color-light);
color: var(--mantine-primary-color-filled);
font-size: 1.5rem;
font-weight: 700;
flex-shrink: 0;
}
.content {
flex: 1;
min-width: 0;
}
.title {
font-size: var(--mantine-font-size-md);
font-weight: 600;
color: var(--mantine-color-text);
margin-bottom: 4px;
}
.description {
font-size: var(--mantine-font-size-sm);
color: var(--mantine-color-dimmed);
line-height: 1.5;
}
// InfoCard.tsx
import {
Box,
BoxProps,
StylesApiProps,
factory,
Factory,
useProps,
useStyles,
} from "@mantine/core";
import classes from "./InfoCard.module.css";
export type InfoCardStylesNames = "root" | "icon" | "content" | "title" | "description";
export type InfoCardVariant = "default" | "highlighted" | "outlined";
export interface InfoCardProps extends BoxProps, StylesApiProps<InfoCardFactory> {
icon: React.ReactNode;
title: string;
description?: string;
variant?: InfoCardVariant;
}
type InfoCardFactory = Factory<{
props: InfoCardProps;
ref: HTMLDivElement;
stylesNames: InfoCardStylesNames;
variant: InfoCardVariant;
}>;
const defaultProps: Partial<InfoCardProps> = {
variant: "default",
};
const InfoCard = factory<InfoCardFactory>((_props, ref) => {
const props = useProps("InfoCard", defaultProps, _props);
const {
icon,
title,
description,
variant,
classNames,
styles,
className,
style,
...others
} = props;
const getStyles = useStyles<InfoCardFactory>({
name: "InfoCard",
classes,
props,
classNames,
styles,
className,
style,
});
return (
<Box ref={ref} {...getStyles("root")} data-variant={variant} {...others}>
<div {...getStyles("icon")}>{icon}</div>
<div {...getStyles("content")}>
<div {...getStyles("title")}>{title}</div>
{description && <div {...getStyles("description")}>{description}</div>}
</div>
</Box>
);
});
InfoCard.displayName = "InfoCard";
InfoCard.classes = classes;
export { InfoCard };
// שימוש
import { InfoCard } from "./InfoCard";
import { SimpleGrid, Container, Title } from "@mantine/core";
function InfoCardDemo() {
return (
<Container size="md" py="xl">
<Title order={2} mb="lg">InfoCard - קומפוננטה מותאמת</Title>
<SimpleGrid cols={{ base: 1, sm: 2 }}>
<InfoCard icon="U" title="משתמשים חדשים" description="15 משתמשים נרשמו היום" />
<InfoCard icon="$" title="הכנסות" description="12,345 ש\"ח היום" variant="highlighted" />
<InfoCard icon="!" title="התראות" description="3 התראות חדשות" variant="outlined" />
<InfoCard
icon="V"
title="משימות"
description="8 משימות הושלמו"
styles={{ icon: { backgroundColor: "var(--mantine-color-green-1)", color: "var(--mantine-color-green-6)" } }}
/>
</SimpleGrid>
</Container>
);
}
פתרון תרגיל 4¶
import { Button, Text, Paper, Card, Stack, Title, Group, Container } from "@mantine/core";
function PolymorphicDemo() {
return (
<Container size="md" py="xl">
<Title order={2} mb="xl">קומפוננטות פולימורפיות</Title>
<Stack gap="xl">
{/* Button כ-anchor */}
<Paper p="lg" withBorder>
<Title order={4} mb="sm">Button component="a"</Title>
<Text size="sm" c="dimmed" mb="md">
כפתור שמתנהג כקישור - שימושי לניווט חיצוני עם עיצוב כפתור
</Text>
<Group>
<Button component="a" href="https://mantine.dev" target="_blank">
לתיעוד Mantine
</Button>
<Button component="a" href="#section2" variant="outline">
קישור פנימי
</Button>
</Group>
</Paper>
{/* Text כ-label */}
<Paper p="lg" withBorder>
<Title order={4} mb="sm">Text component="label"</Title>
<Text size="sm" c="dimmed" mb="md">
טקסט שפועל כתווית - לחיצה עליו ממקדת את ה-input המקושר
</Text>
<Stack gap="xs">
<Text component="label" htmlFor="demo-input" fw={600} size="sm">
שם מלא (לחץ עליי)
</Text>
<input id="demo-input" placeholder="שם..." style={{ padding: 8 }} />
</Stack>
</Paper>
{/* Paper כ-section */}
<Paper component="section" p="lg" withBorder aria-label="אזור תוכן">
<Title order={4} mb="sm">Paper component="section"</Title>
<Text size="sm" c="dimmed">
Paper שמרונדר כ-section - חשוב לסמנטיקה ונגישות. קוראי מסך
מזהים את זה כאזור תוכן נפרד.
</Text>
</Paper>
{/* Card כ-article */}
<Card component="article" p="lg">
<Title order={4} mb="sm">Card component="article"</Title>
<Text size="sm" c="dimmed">
Card שמרונדר כ-article - מתאים לתוכן עצמאי כמו פוסט בבלוג.
שומר על סמנטיקת HTML נכונה תוך שימוש בעיצוב של Mantine.
</Text>
</Card>
</Stack>
</Container>
);
}
פתרון תרגיל 5¶
import {
MantineProvider,
createTheme,
Button,
TextInput,
Card,
Title,
Text,
Stack,
Group,
Container,
SimpleGrid,
Alert,
} from "@mantine/core";
const mainTheme = createTheme({
primaryColor: "blue",
defaultRadius: "md",
});
const warningTheme = createTheme({
primaryColor: "red",
defaultRadius: "sm",
components: {
Button: {
defaultProps: { variant: "filled" },
},
Alert: {
defaultProps: { variant: "light", color: "red" },
},
},
});
const successTheme = createTheme({
primaryColor: "green",
defaultRadius: "xl",
components: {
Button: {
defaultProps: { variant: "light" },
},
Alert: {
defaultProps: { variant: "light", color: "green" },
},
},
});
function ThemeSection({
title,
description,
}: {
title: string;
description: string;
}) {
return (
<Card p="lg" withBorder>
<Title order={4} mb="md">{title}</Title>
<Text size="sm" c="dimmed" mb="md">{description}</Text>
<Stack gap="sm">
<TextInput placeholder="שדה טקסט" />
<Group>
<Button>כפתור ראשי</Button>
<Button variant="outline">כפתור משני</Button>
</Group>
<Alert title="התראה">הודעה לדוגמה באזור זה</Alert>
</Stack>
</Card>
);
}
function NestedThemesDemo() {
return (
<MantineProvider theme={mainTheme}>
<Container size="lg" py="xl">
<Title order={2} mb="xl" ta="center">
הדגמת Themes מקוננים
</Title>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{/* Theme ראשי */}
<ThemeSection
title="Theme ראשי (כחול)"
description="ברירת מחדל - radius md, צבע כחול"
/>
{/* Theme אזהרה */}
<MantineProvider theme={warningTheme}>
<ThemeSection
title="Theme אזהרה (אדום)"
description="צבע אדום, radius sm, כפתורים מלאים"
/>
</MantineProvider>
{/* Theme הצלחה */}
<MantineProvider theme={successTheme}>
<ThemeSection
title="Theme הצלחה (ירוק)"
description="צבע ירוק, radius xl, כפתורים בהירים"
/>
</MantineProvider>
</SimpleGrid>
</Container>
</MantineProvider>
);
}
- אותו קומפוננטה ThemeSection נראית שונה בכל אזור
- ה-theme המקונן דורס את ה-theme העליון רק עבור ילדיו
- ברירות מחדל שונות לכל אזור (variant של Button, color של Alert)
פתרון תרגיל 6¶
/* Dashboard.module.css */
.page {
min-height: 100vh;
background-color: var(--app-bg);
}
.header {
height: var(--header-height);
background-color: var(--card-bg);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
padding: 0 var(--mantine-spacing-lg);
}
.content {
max-width: var(--content-max-width);
margin: 0 auto;
padding: var(--mantine-spacing-xl);
}
.tableRow {
transition: background-color 0.15s ease;
}
.tableRow:hover {
background-color: var(--mantine-primary-color-light);
}
import {
createTheme,
MantineProvider,
CSSVariablesResolver,
Container,
Title,
Text,
SimpleGrid,
Table,
Badge,
Group,
Button,
Avatar,
Paper,
Stack,
} from "@mantine/core";
import dashStyles from "./Dashboard.module.css";
// קומפוננטה מותאמת (פשוטה - ללא factory)
function StatCard({
title,
value,
change,
}: {
title: string;
value: string;
change: string;
}) {
const isPositive = change.startsWith("+");
return (
<Paper p="lg" shadow="sm" withBorder>
<Text size="sm" c="dimmed" tt="uppercase" fw={500}>{title}</Text>
<Text size="2rem" fw={700} mt="xs">{value}</Text>
<Text size="sm" c={isPositive ? "green" : "red"} mt="xs">{change}</Text>
</Paper>
);
}
const users = [
{ name: "יוסי כהן", role: "מנהל", status: "פעיל" },
{ name: "דנה לוי", role: "מפתח", status: "פעיל" },
{ name: "אבי מזרחי", role: "מעצב", status: "חופשה" },
];
function DashboardPage() {
return (
<div className={dashStyles.page}>
{/* Header */}
<header className={dashStyles.header}>
<Group justify="space-between" w="100%">
<Title order={3}>דשבורד</Title>
<Button component="a" href="/settings" variant="subtle" size="sm">
הגדרות
</Button>
</Group>
</header>
{/* Content */}
<div className={dashStyles.content}>
<Title order={2} mb="lg">סקירה כללית</Title>
{/* Stats */}
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} mb="xl">
<StatCard title="משתמשים" value="1,234" change="+12%" />
<StatCard title="הזמנות" value="567" change="+8%" />
<StatCard title="הכנסות" value="45K" change="+23%" />
<StatCard title="החזרות" value="12" change="-5%" />
</SimpleGrid>
{/* Table */}
<Paper shadow="sm" withBorder>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>משתמש</Table.Th>
<Table.Th>תפקיד</Table.Th>
<Table.Th>סטטוס</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{users.map((user) => (
<Table.Tr key={user.name} className={dashStyles.tableRow}>
<Table.Td>
<Group gap="sm">
<Avatar size="sm" color="blue" radius="xl">
{user.name.charAt(0)}
</Avatar>
<Text size="sm" fw={500}>{user.name}</Text>
</Group>
</Table.Td>
<Table.Td>
<Text size="sm">{user.role}</Text>
</Table.Td>
<Table.Td>
<Badge
color={user.status === "פעיל" ? "green" : "yellow"}
variant="light"
>
{user.status}
</Badge>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Paper>
</div>
</div>
);
}
תשובות לשאלות¶
-
classNames לעומת styles - classNames מוסיף מחלקות CSS לחלקים הפנימיים ומאפשר עיצוב עם CSS Modules/CSS חיצוני. styles מגדיר סגנונות אינליין ישירות. נעדיף classNames כשיש הרבה סגנונות (ניקיון), pseudo-classes, ו-media queries. נעדיף styles לדברים דינמיים קטנים שמשתנים לפי props.
-
CSSVariablesResolver - הוא מאפשר הגדרת משתנים שמשתנים אוטומטית בין מצב בהיר לכהה. בלעדיו הייתם צריכים לכתוב ידנית
[data-mantine-color-scheme="dark"] { --my-var: ...; }בקובץ CSS. ה-resolver גם מקבל את אובייקט ה-theme, כך שאפשר להשתמש בצבעים מהתימה ישירות. -
factory - מעבר ל-forwardRef, factory מספק: תמיכה ב-Styles API (classNames, styles), אפשרות לדרוס סגנונות ברמת ה-theme דרך components, שמות סטייליסטיים (stylesNames) שמאפשרים למשתמשים של הקומפוננטה לדעת אילו חלקים ניתן לעצב, ו-system props אוטומטית דרך Box.
-
MantineProvider מקונן - ה-theme העליון לא נמחק. ה-provider המקונן דורס ערכים ספציפיים. אם ה-theme המקונן מגדיר רק primaryColor, כל שאר ההגדרות (פונטים, spacing, components) נשארות מה-theme העליון. זה מאפשר שינויים ממוקדים לאזורים ספציפיים.
-
polymorphic components - במקום לעטוף קומפוננטה (כמו
<a><Button>...</Button></a>) שיוצר DOM מיותר ובעיות סגנון, polymorphic component משנה את אלמנט ה-HTML הבסיסי עצמו.<Button component="a">מרנדר תגית a עם כל הסגנונות של Button ישירות. זה שומר על DOM נקי, סמנטיקה נכונה, ו-TypeScript בודק את ה-props של האלמנט החדש.