לדלג לתוכן

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>
  );
}

תשובות לשאלות

  1. classNames לעומת styles - classNames מוסיף מחלקות CSS לחלקים הפנימיים ומאפשר עיצוב עם CSS Modules/CSS חיצוני. styles מגדיר סגנונות אינליין ישירות. נעדיף classNames כשיש הרבה סגנונות (ניקיון), pseudo-classes, ו-media queries. נעדיף styles לדברים דינמיים קטנים שמשתנים לפי props.

  2. CSSVariablesResolver - הוא מאפשר הגדרת משתנים שמשתנים אוטומטית בין מצב בהיר לכהה. בלעדיו הייתם צריכים לכתוב ידנית [data-mantine-color-scheme="dark"] { --my-var: ...; } בקובץ CSS. ה-resolver גם מקבל את אובייקט ה-theme, כך שאפשר להשתמש בצבעים מהתימה ישירות.

  3. factory - מעבר ל-forwardRef, factory מספק: תמיכה ב-Styles API (classNames, styles), אפשרות לדרוס סגנונות ברמת ה-theme דרך components, שמות סטייליסטיים (stylesNames) שמאפשרים למשתמשים של הקומפוננטה לדעת אילו חלקים ניתן לעצב, ו-system props אוטומטית דרך Box.

  4. MantineProvider מקונן - ה-theme העליון לא נמחק. ה-provider המקונן דורס ערכים ספציפיים. אם ה-theme המקונן מגדיר רק primaryColor, כל שאר ההגדרות (פונטים, spacing, components) נשארות מה-theme העליון. זה מאפשר שינויים ממוקדים לאזורים ספציפיים.

  5. polymorphic components - במקום לעטוף קומפוננטה (כמו <a><Button>...</Button></a>) שיוצר DOM מיותר ובעיות סגנון, polymorphic component משנה את אלמנט ה-HTML הבסיסי עצמו. <Button component="a"> מרנדר תגית a עם כל הסגנונות של Button ישירות. זה שומר על DOM נקי, סמנטיקה נכונה, ו-TypeScript בודק את ה-props של האלמנט החדש.