8.8 ערכות נושא ומצב כהה הרצאה
ערכות נושא ומצב כהה - Theming and Dark Mode¶
בשיעור זה נלמד כיצד לבנות מערכת עיצוב גמישה עם ערכות נושא, ליישם מצב כהה עם Mantine, לשמור את העדפת המשתמש, וליצור ערכות צבעים מותאמות.
Design Tokens - אסימוני עיצוב¶
Design tokens הם הערכים הקטנים ביותר במערכת העיצוב - צבעים, מרווחים, פונטים, צללים וכו'. הם הבסיס לעקביות בעיצוב.
למה Design Tokens חשובים¶
- עקביות - כל הקומפוננטות משתמשות באותם ערכים
- תחזוקה - שינוי במקום אחד משפיע על כל האפליקציה
- ערכות נושא - החלפת tokens מחליפה את כל העיצוב
- תקשורת - שפה משותפת בין מעצבים ומפתחים
Tokens ב-Mantine¶
import { createTheme } from "@mantine/core";
const theme = createTheme({
// צבעים
colors: {
brand: ["#f0f4ff", "#dce4f5", "#b4c6e7", "#8aa7db", "#668dd0", "#4e7dc9", "#4275c7", "#3363b0", "#29589e", "#1a4b8c"],
},
primaryColor: "brand",
// טיפוגרפיה
fontFamily: "Rubik, sans-serif",
fontSizes: { xs: "0.75rem", sm: "0.875rem", md: "1rem", lg: "1.125rem", xl: "1.25rem" },
// ריווח
spacing: { xs: "0.5rem", sm: "0.75rem", md: "1rem", lg: "1.5rem", xl: "2rem" },
// רדיוס
radius: { xs: "2px", sm: "4px", md: "8px", lg: "16px", xl: "32px" },
// צללים
shadows: {
xs: "0 1px 2px rgba(0, 0, 0, 0.05)",
sm: "0 1px 3px rgba(0, 0, 0, 0.1)",
md: "0 4px 6px rgba(0, 0, 0, 0.1)",
lg: "0 10px 15px rgba(0, 0, 0, 0.1)",
xl: "0 20px 25px rgba(0, 0, 0, 0.15)",
},
});
מצב כהה עם Mantine¶
הגדרה בסיסית¶
import { MantineProvider } from "@mantine/core";
function App() {
return (
<MantineProvider defaultColorScheme="auto">
{/* auto = עוקב אחרי הגדרות המערכת */}
{/* light = תמיד בהיר */}
{/* dark = תמיד כהה */}
</MantineProvider>
);
}
useMantineColorScheme - שליטה על מצב הצבע¶
import { useMantineColorScheme, Button, Group, Text, Paper, Stack } from "@mantine/core";
function ColorSchemeToggle() {
const { setColorScheme, colorScheme } = useMantineColorScheme();
return (
<Stack>
<Text>מצב נוכחי: {colorScheme}</Text>
<Group>
<Button
variant={colorScheme === "light" ? "filled" : "outline"}
onClick={() => setColorScheme("light")}
>
בהיר
</Button>
<Button
variant={colorScheme === "dark" ? "filled" : "outline"}
onClick={() => setColorScheme("dark")}
>
כהה
</Button>
<Button
variant={colorScheme === "auto" ? "filled" : "outline"}
onClick={() => setColorScheme("auto")}
>
אוטומטי
</Button>
</Group>
</Stack>
);
}
useComputedColorScheme - המצב האמיתי¶
import { useComputedColorScheme, useMantineColorScheme, Switch } from "@mantine/core";
function ThemeSwitch() {
const { setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme("light");
// computedColorScheme הוא תמיד "light" או "dark"
// גם כש-colorScheme הוא "auto"
return (
<Switch
label="מצב כהה"
checked={computedColorScheme === "dark"}
onChange={(e) =>
setColorScheme(e.currentTarget.checked ? "dark" : "light")
}
size="lg"
onLabel="כהה"
offLabel="בהיר"
/>
);
}
- useMantineColorScheme מחזיר את הערך המוגדר (light/dark/auto)
- useComputedColorScheme מחזיר את הערך בפועל (light/dark) - גם כש-auto
- שימושי כשצריך לדעת מה באמת מוצג
שמירת העדפת המשתמש¶
Mantine שומרת אוטומטית את העדפת ערכת הנושא ב-localStorage. אפשר לשלוט בזה:
import { MantineProvider } from "@mantine/core";
function App() {
return (
<MantineProvider
defaultColorScheme="auto"
// Mantine שומרת את הבחירה ב-localStorage אוטומטית
// המפתח ברירת מחדל הוא "mantine-color-scheme-value"
>
<TheApp />
</MantineProvider>
);
}
כפתור מעבר מלא עם שמירה¶
import {
useMantineColorScheme,
useComputedColorScheme,
ActionIcon,
Tooltip,
} from "@mantine/core";
function ThemeToggle() {
const { setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme("light");
function toggleColorScheme() {
setColorScheme(computedColorScheme === "dark" ? "light" : "dark");
}
return (
<Tooltip label={computedColorScheme === "dark" ? "מצב בהיר" : "מצב כהה"}>
<ActionIcon
variant="default"
size="lg"
onClick={toggleColorScheme}
aria-label="החלף ערכת צבעים"
>
{computedColorScheme === "dark" ? "S" : "M"}
</ActionIcon>
</Tooltip>
);
}
יצירת ערכות צבעים מותאמות¶
צבעים שונים לבהיר וכהה¶
import { createTheme, CSSVariablesResolver, MantineProvider } from "@mantine/core";
const theme = createTheme({
primaryColor: "brand",
colors: {
brand: [
"#eef3ff", "#dee2f2", "#bcc3d8", "#97a3be", "#7888a8",
"#63779d", "#577198", "#466085", "#3b5478", "#2c466b",
],
},
primaryShade: { light: 6, dark: 7 },
});
const resolver: CSSVariablesResolver = (theme) => ({
variables: {},
light: {
"--app-bg": "#f8f9fa",
"--card-bg": "#ffffff",
"--text-primary": "#212529",
"--text-secondary": "#6c757d",
"--border-color": "#dee2e6",
"--hover-bg": "#f1f3f5",
"--sidebar-bg": "#ffffff",
"--header-bg": "#ffffff",
"--success-bg": "#d3f9d8",
"--error-bg": "#ffe3e3",
},
dark: {
"--app-bg": "#1a1b1e",
"--card-bg": "#25262b",
"--text-primary": "#c1c2c5",
"--text-secondary": "#909296",
"--border-color": "#373a40",
"--hover-bg": "#2c2e33",
"--sidebar-bg": "#1a1b1e",
"--header-bg": "#1a1b1e",
"--success-bg": "#2b3b2d",
"--error-bg": "#3b2b2b",
},
});
function App() {
return (
<MantineProvider
theme={theme}
defaultColorScheme="auto"
cssVariablesResolver={resolver}
>
<TheApp />
</MantineProvider>
);
}
שימוש במשתנים ב-CSS¶
/* Dashboard.module.css */
.page {
min-height: 100vh;
background-color: var(--app-bg);
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--mantine-radius-md);
padding: var(--mantine-spacing-lg);
transition: background-color 0.2s, border-color 0.2s;
}
.card:hover {
background-color: var(--hover-bg);
}
.sidebar {
background-color: var(--sidebar-bg);
border-left: 1px solid var(--border-color);
}
.textPrimary {
color: var(--text-primary);
}
.textSecondary {
color: var(--text-secondary);
}
שימוש ב-data attribute לצבעי מצב¶
/* אלטרנטיבה ללא CSSVariablesResolver */
.card {
background-color: white;
color: #333;
}
[data-mantine-color-scheme="dark"] .card {
background-color: #25262b;
color: #c1c2c5;
}
גישת CSS Variables לערכות נושא¶
ניתן ליצור מערכת ערכות נושא מלאה עם CSS Variables:
import { createTheme, MantineProvider, CSSVariablesResolver } from "@mantine/core";
type ThemeName = "default" | "ocean" | "forest" | "sunset";
const themeConfigs: Record<ThemeName, {
primary: string;
light: Record<string, string>;
dark: Record<string, string>;
}> = {
default: {
primary: "blue",
light: {
"--accent": "#228be6",
"--accent-light": "#e7f5ff",
"--surface": "#ffffff",
},
dark: {
"--accent": "#4dabf7",
"--accent-light": "#1c3a5c",
"--surface": "#25262b",
},
},
ocean: {
primary: "cyan",
light: {
"--accent": "#15aabf",
"--accent-light": "#e3fafc",
"--surface": "#f0fffe",
},
dark: {
"--accent": "#3bc9db",
"--accent-light": "#1a3a3e",
"--surface": "#1a2e30",
},
},
forest: {
primary: "green",
light: {
"--accent": "#40c057",
"--accent-light": "#ebfbee",
"--surface": "#f0fff0",
},
dark: {
"--accent": "#51cf66",
"--accent-light": "#1a3b1e",
"--surface": "#1a2e1a",
},
},
sunset: {
primary: "orange",
light: {
"--accent": "#fd7e14",
"--accent-light": "#fff4e6",
"--surface": "#fffaf0",
},
dark: {
"--accent": "#ffa94d",
"--accent-light": "#3b2e1a",
"--surface": "#2e251a",
},
},
};
function useThemeConfig(themeName: ThemeName) {
const config = themeConfigs[themeName];
const theme = createTheme({
primaryColor: config.primary,
});
const resolver: CSSVariablesResolver = () => ({
variables: {},
light: config.light,
dark: config.dark,
});
return { theme, resolver };
}
import { useState } from "react";
import {
MantineProvider,
Button,
Group,
Paper,
Title,
Text,
Stack,
Container,
Select,
} from "@mantine/core";
function ThemeSwitcherApp() {
const [themeName, setThemeName] = useState<ThemeName>("default");
const { theme, resolver } = useThemeConfig(themeName);
return (
<MantineProvider
theme={theme}
defaultColorScheme="auto"
cssVariablesResolver={resolver}
>
<Container size="sm" py="xl">
<Stack gap="lg">
<Title order={2}>בחירת ערכת נושא</Title>
<Select
label="ערכת נושא"
value={themeName}
onChange={(val) => setThemeName((val as ThemeName) || "default")}
data={[
{ value: "default", label: "ברירת מחדל (כחול)" },
{ value: "ocean", label: "אוקיינוס (תכלת)" },
{ value: "forest", label: "יער (ירוק)" },
{ value: "sunset", label: "שקיעה (כתום)" },
]}
/>
<Group>
<Button>כפתור ראשי</Button>
<Button variant="light">כפתור בהיר</Button>
<Button variant="outline">כפתור מתאר</Button>
</Group>
<Paper
p="lg"
withBorder
style={{ backgroundColor: "var(--accent-light)" }}
>
<Text fw={500} style={{ color: "var(--accent)" }}>
כרטיס עם צבעי ערכת הנושא
</Text>
<Text size="sm" c="dimmed" mt="xs">
הצבעים משתנים אוטומטית כשמחליפים ערכת נושא
</Text>
</Paper>
</Stack>
</Container>
</MantineProvider>
);
}
דפוסי עיצוב רספונסיביים¶
התאמת theme לגודל מסך¶
import { useMediaQuery, useComputedColorScheme } from "@mantine/hooks";
import { Paper, Text, Stack, Group, Button, Container, Title } from "@mantine/core";
function ResponsiveThemedPage() {
const isMobile = useMediaQuery("(max-width: 768px)");
const colorScheme = useComputedColorScheme("light");
return (
<Container size="md" py={isMobile ? "md" : "xl"}>
<Stack gap={isMobile ? "md" : "xl"}>
<Title order={isMobile ? 3 : 1}>
ממשק רספונסיבי ותמטי
</Title>
<Text size={isMobile ? "sm" : "lg"} c="dimmed">
הממשק מתאים את עצמו לגודל המסך ולערכת הצבעים
</Text>
<Paper
p={isMobile ? "md" : "xl"}
shadow={isMobile ? "xs" : "md"}
withBorder
>
<Stack gap="md">
<Text>
מצב: {colorScheme === "dark" ? "כהה" : "בהיר"}
{" | "}
מכשיר: {isMobile ? "טלפון" : "דסקטופ"}
</Text>
<Group
justify={isMobile ? "center" : "flex-start"}
gap={isMobile ? "xs" : "md"}
>
<Button size={isMobile ? "sm" : "md"}>
פעולה
</Button>
<Button
variant="outline"
size={isMobile ? "sm" : "md"}
>
ביטול
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Container>
);
}
CSS רספונסיבי עם ערכות נושא¶
/* ResponsiveThemed.module.css */
.hero {
padding: var(--mantine-spacing-xl) var(--mantine-spacing-md);
text-align: center;
background-color: var(--mantine-primary-color-light);
border-radius: var(--mantine-radius-lg);
}
@media (min-width: 48em) {
.hero {
padding: calc(var(--mantine-spacing-xl) * 3) var(--mantine-spacing-xl);
}
}
.statsGrid {
display: grid;
grid-template-columns: 1fr;
gap: var(--mantine-spacing-md);
}
@media (min-width: 48em) {
.statsGrid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 62em) {
.statsGrid {
grid-template-columns: repeat(4, 1fr);
}
}
.statCard {
padding: var(--mantine-spacing-lg);
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--mantine-radius-md);
transition: transform 0.2s, box-shadow 0.2s;
}
.statCard:hover {
transform: translateY(-2px);
box-shadow: var(--mantine-shadow-md);
}
דוגמה מלאה - דשבורד עם מצב כהה¶
import {
AppShell,
Burger,
Group,
NavLink,
Title,
Text,
Button,
SimpleGrid,
Paper,
Container,
Stack,
Switch,
useMantineColorScheme,
useComputedColorScheme,
ActionIcon,
Tooltip,
Divider,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
function ThemedDashboard() {
const [opened, { toggle }] = useDisclosure();
const { setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme("light");
const stats = [
{ title: "משתמשים", value: "1,234", change: "+12%", positive: true },
{ title: "הזמנות", value: "567", change: "+8%", positive: true },
{ title: "הכנסות", value: "45K", change: "+23%", positive: true },
{ title: "החזרות", value: "12", change: "-5%", positive: false },
];
return (
<AppShell
header={{ height: 60 }}
navbar={{ width: 250, breakpoint: "sm", collapsed: { mobile: !opened } }}
padding="md"
>
<AppShell.Header>
<Group h="100%" px="md" justify="space-between">
<Group>
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Title order={3}>דשבורד</Title>
</Group>
<Group>
<Tooltip label={computedColorScheme === "dark" ? "מצב בהיר" : "מצב כהה"}>
<ActionIcon
variant="default"
size="lg"
onClick={() =>
setColorScheme(
computedColorScheme === "dark" ? "light" : "dark"
)
}
>
{computedColorScheme === "dark" ? "S" : "M"}
</ActionIcon>
</Tooltip>
</Group>
</Group>
</AppShell.Header>
<AppShell.Navbar p="md">
<Stack gap="xs">
<NavLink label="דשבורד" active />
<NavLink label="משתמשים" />
<NavLink label="הזמנות" />
<NavLink label="מוצרים" />
<Divider my="sm" />
<NavLink label="הגדרות" />
<div style={{ marginTop: "auto" }}>
<Switch
label="מצב כהה"
checked={computedColorScheme === "dark"}
onChange={(e) =>
setColorScheme(e.currentTarget.checked ? "dark" : "light")
}
/>
</div>
</Stack>
</AppShell.Navbar>
<AppShell.Main>
<Container fluid>
<Title order={2} mb="lg">סקירה כללית</Title>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} mb="xl">
{stats.map((stat) => (
<Paper key={stat.title} shadow="xs" p="md" withBorder>
<Text size="sm" c="dimmed" tt="uppercase" fw={500}>
{stat.title}
</Text>
<Text size="xl" fw={700} mt="xs">
{stat.value}
</Text>
<Text size="sm" c={stat.positive ? "green" : "red"} mt="xs">
{stat.change}
</Text>
</Paper>
))}
</SimpleGrid>
<SimpleGrid cols={{ base: 1, md: 2 }}>
<Paper shadow="xs" p="lg" withBorder>
<Title order={4} mb="md">פעילות אחרונה</Title>
<Stack gap="sm">
{["משתמש חדש נרשם", "הזמנה הושלמה", "תשלום התקבל"].map(
(activity) => (
<Text key={activity} size="sm">{activity}</Text>
)
)}
</Stack>
</Paper>
<Paper shadow="xs" p="lg" withBorder>
<Title order={4} mb="md">פעולות מהירות</Title>
<Stack gap="sm">
<Button variant="light" fullWidth>הוסף משתמש</Button>
<Button variant="light" fullWidth>צור הזמנה</Button>
<Button variant="light" fullWidth>הפק דוח</Button>
</Stack>
</Paper>
</SimpleGrid>
</Container>
</AppShell.Main>
</AppShell>
);
}
- כפתור החלפת מצב ב-header
- Switch נוסף בתחתית הסיידבר
- Mantine מטפלת אוטומטית בכל הצבעים - Paper, Text, Button וכו'
- ההעדפה נשמרת אוטומטית ב-localStorage
סיכום¶
- Design tokens הם הבסיס למערכת עיצוב עקבית ותחזוקתית
- Mantine תומכת במצב כהה מחוץ לקופסה עם defaultColorScheme
- useMantineColorScheme ו-setColorScheme שולטים על המצב
- useComputedColorScheme מחזיר את המצב האמיתי (light/dark) גם כש-auto
- Mantine שומרת את העדפת המשתמש ב-localStorage אוטומטית
- CSSVariablesResolver מאפשר הגדרת משתנים שונים למצב בהיר וכהה
- ניתן ליצור מערכת ערכות נושא מלאה עם CSS Variables ו-createTheme
- data-mantine-color-scheme attribute מאפשר CSS מותנה למצב
- שילוב עיצוב רספונסיבי עם ערכות נושא דרך media queries ו-system props