8.1 סקירת גישות לעיצוב פתרון
פתרון - סקירת גישות לעיצוב¶
פתרון תרגיל 1¶
interface StatusBadgeProps {
status: "success" | "warning" | "error";
children: React.ReactNode;
}
function StatusBadge({ status, children }: StatusBadgeProps) {
const colorMap: Record<string, string> = {
success: "#4CAF50",
warning: "#FF9800",
error: "#F44336",
};
const style: React.CSSProperties = {
backgroundColor: colorMap[status],
color: "white",
padding: "4px 12px",
borderRadius: "12px",
fontSize: "14px",
fontWeight: 500,
display: "inline-block",
};
return <span style={style}>{children}</span>;
}
// שימוש
function App() {
return (
<div style={{ display: "flex", gap: "8px" }}>
<StatusBadge status="success">הצלחה</StatusBadge>
<StatusBadge status="warning">אזהרה</StatusBadge>
<StatusBadge status="error">שגיאה</StatusBadge>
</div>
);
}
- השתמשנו ב-Record למיפוי צבעים לפי סטטוס
- הטיפוס React.CSSProperties מבטיח שכל התכונות תקינות
- הגישה של inline styles מתאימה כאן כי הסגנון דינמי ופשוט
פתרון תרגיל 2¶
/* ProfileCard.css */
.profile-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
border: 1px solid #e0e0e0;
border-radius: 12px;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.profile-card__avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 12px;
}
.profile-card__name {
font-size: 20px;
font-weight: bold;
margin: 0 0 4px 0;
}
.profile-card__bio {
font-size: 14px;
color: #666;
text-align: center;
margin: 0 0 16px 0;
}
.profile-card__button {
padding: 8px 24px;
border: 2px solid #007bff;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.profile-card__button--follow {
background-color: #007bff;
color: white;
}
.profile-card__button--follow:hover {
background-color: #0056b3;
}
.profile-card__button--following {
background-color: white;
color: #007bff;
}
.profile-card__button--following:hover {
background-color: #f0f0f0;
}
// ProfileCard.tsx
import clsx from "clsx";
import "./ProfileCard.css";
interface ProfileCardProps {
name: string;
bio: string;
avatarUrl: string;
isFollowing: boolean;
onToggleFollow: () => void;
}
function ProfileCard({
name,
bio,
avatarUrl,
isFollowing,
onToggleFollow,
}: ProfileCardProps) {
return (
<div className="profile-card">
<img className="profile-card__avatar" src={avatarUrl} alt={name} />
<h3 className="profile-card__name">{name}</h3>
<p className="profile-card__bio">{bio}</p>
<button
className={clsx("profile-card__button", {
"profile-card__button--follow": !isFollowing,
"profile-card__button--following": isFollowing,
})}
onClick={onToggleFollow}
>
{isFollowing ? "עוקב" : "עקוב"}
</button>
</div>
);
}
- השתמשנו בשיטת BEM למניעת התנגשויות שמות
- clsx מאפשר לנו להחליף בין מחלקות בצורה נקייה
- שימו לב לשימוש ב-transition עבור אנימציית hover חלקה
פתרון תרגיל 3¶
/* ProfileCard.module.css */
.card {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
border: 1px solid #e0e0e0;
border-radius: 12px;
max-width: 300px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 12px;
}
.name {
font-size: 20px;
font-weight: bold;
margin: 0 0 4px 0;
}
.bio {
font-size: 14px;
color: #666;
text-align: center;
margin: 0 0 16px 0;
}
.button {
padding: 8px 24px;
border: 2px solid #007bff;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.followButton {
composes: button;
background-color: #007bff;
color: white;
}
.followButton:hover {
background-color: #0056b3;
}
.followingButton {
composes: button;
background-color: white;
color: #007bff;
}
.followingButton:hover {
background-color: #f0f0f0;
}
// ProfileCard.tsx
import styles from "./ProfileCard.module.css";
interface ProfileCardProps {
name: string;
bio: string;
avatarUrl: string;
isFollowing: boolean;
onToggleFollow: () => void;
}
function ProfileCard({
name,
bio,
avatarUrl,
isFollowing,
onToggleFollow,
}: ProfileCardProps) {
return (
<div className={styles.card}>
<img className={styles.avatar} src={avatarUrl} alt={name} />
<h3 className={styles.name}>{name}</h3>
<p className={styles.bio}>{bio}</p>
<button
className={isFollowing ? styles.followingButton : styles.followButton}
onClick={onToggleFollow}
>
{isFollowing ? "עוקב" : "עקוב"}
</button>
</div>
);
}
- עם CSS Modules אין צורך בשמות BEM ארוכים - השמות יהיו ייחודיים אוטומטית
- composes מאפשר לנו לשתף סגנונות בין מחלקות
- הקוד נקי יותר כי אין צורך ב-clsx לרוב המקרים
פתרון תרגיל 4¶
import styled, { css } from "styled-components";
type Variant = "filled" | "outline" | "ghost";
type Size = "sm" | "md" | "lg";
const sizeStyles = {
sm: css`
padding: 6px 12px;
font-size: 12px;
`,
md: css`
padding: 10px 20px;
font-size: 14px;
`,
lg: css`
padding: 14px 28px;
font-size: 16px;
`,
};
const variantStyles = {
filled: css`
background-color: #007bff;
color: white;
border: none;
&:hover {
background-color: #0056b3;
transform: translateY(-1px);
}
`,
outline: css`
background-color: transparent;
color: #007bff;
border: 2px solid #007bff;
&:hover {
background-color: #007bff;
color: white;
}
`,
ghost: css`
background-color: transparent;
color: #007bff;
border: none;
&:hover {
background-color: rgba(0, 123, 255, 0.1);
}
`,
};
const StyledThemeButton = styled.button<{ $variant: Variant; $size: Size }>`
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
${(props) => sizeStyles[props.$size]}
${(props) => variantStyles[props.$variant]}
&:active {
transform: scale(0.98);
}
`;
interface ThemeButtonProps {
variant?: Variant;
size?: Size;
children: React.ReactNode;
onClick?: () => void;
}
function ThemeButton({
variant = "filled",
size = "md",
children,
onClick,
}: ThemeButtonProps) {
return (
<StyledThemeButton $variant={variant} $size={size} onClick={onClick}>
{children}
</StyledThemeButton>
);
}
// שימוש
function App() {
return (
<div style={{ display: "flex", gap: "12px", padding: "20px" }}>
<ThemeButton variant="filled" size="sm">קטן</ThemeButton>
<ThemeButton variant="outline" size="md">בינוני</ThemeButton>
<ThemeButton variant="ghost" size="lg">גדול</ThemeButton>
</div>
);
}
- הפרדנו את הסגנונות לאובייקטים נפרדים לפי variant ו-size
- השתמשנו ב-css helper של styled-components עבור קטעי סגנון
- $ prefix ב-props מונע העברה ל-DOM
פתרון תרגיל 5¶
interface FeatureCardProps {
icon: string;
title: string;
description: string;
}
function FeatureCard({ icon, title, description }: FeatureCardProps) {
return (
<div className="w-full md:w-80 p-6 bg-white border border-gray-200 rounded-xl shadow-sm hover:shadow-lg transition-shadow duration-300 cursor-pointer">
<div className="text-4xl mb-4">{icon}</div>
<h3 className="text-lg md:text-xl font-bold text-gray-800 mb-2">
{title}
</h3>
<p className="text-sm md:text-base text-gray-600 leading-relaxed">
{description}
</p>
</div>
);
}
// שימוש
function FeaturesSection() {
const features = [
{ icon: "#", title: "מהיר", description: "ביצועים גבוהים ללא פשרות" },
{ icon: "*", title: "מאובטח", description: "אבטחה מובנית בכל שכבה" },
{ icon: "+", title: "גמיש", description: "מתאים לכל פרויקט ודרישה" },
];
return (
<div className="flex flex-col md:flex-row gap-6 p-8 justify-center">
{features.map((feature) => (
<FeatureCard key={feature.title} {...feature} />
))}
</div>
);
}
- w-full בטלפון, md:w-80 בדסקטופ
- hover:shadow-lg ו-transition-shadow ליצירת אפקט הרמה
- שימוש ב-responsive prefixes (md:) לעיצוב רספונסיבי
פתרון תרגיל 6¶
import { Card as MantineCard, Text, Button, Group } from "@mantine/core";
import styles from "./ComparisonCard.module.css";
// גישה 1 - Inline Styles
function InlineCard() {
return (
<div
style={{
padding: "16px",
border: "1px solid #e0e0e0",
borderRadius: "8px",
maxWidth: "250px",
}}
>
<h3 style={{ margin: "0 0 8px 0", fontSize: "18px" }}>כותרת</h3>
<p style={{ margin: "0 0 12px 0", color: "#666", fontSize: "14px" }}>
תוכן הכרטיסייה עם תיאור קצר
</p>
<button
style={{
padding: "8px 16px",
backgroundColor: "#007bff",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
>
לחץ כאן
</button>
</div>
);
}
// גישה 2 - CSS Modules
function ModulesCard() {
return (
<div className={styles.card}>
<h3 className={styles.title}>כותרת</h3>
<p className={styles.description}>תוכן הכרטיסייה עם תיאור קצר</p>
<button className={styles.button}>לחץ כאן</button>
</div>
);
}
// גישה 3 - Tailwind
function TailwindCard() {
return (
<div className="p-4 border border-gray-200 rounded-lg max-w-[250px]">
<h3 className="text-lg font-bold mb-2">כותרת</h3>
<p className="text-sm text-gray-500 mb-3">
תוכן הכרטיסייה עם תיאור קצר
</p>
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
לחץ כאן
</button>
</div>
);
}
// גישה 4 - Mantine
function MantineCardExample() {
return (
<MantineCard shadow="sm" padding="md" radius="md" withBorder maw={250}>
<Text fw={700} size="lg" mb={8}>
כותרת
</Text>
<Text size="sm" c="dimmed" mb={12}>
תוכן הכרטיסייה עם תיאור קצר
</Text>
<Button variant="filled" size="sm">
לחץ כאן
</Button>
</MantineCard>
);
}
// דף השוואה
function ComparisonPage() {
const cards = [
{ title: "Inline Styles", component: <InlineCard /> },
{ title: "CSS Modules", component: <ModulesCard /> },
{ title: "Tailwind CSS", component: <TailwindCard /> },
{ title: "Mantine", component: <MantineCardExample /> },
];
return (
<div style={{ padding: "40px" }}>
<h1 style={{ textAlign: "center", marginBottom: "32px" }}>
השוואת גישות עיצוב
</h1>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
gap: "24px",
}}
>
{cards.map((card) => (
<div key={card.title}>
<h2
style={{
textAlign: "center",
marginBottom: "12px",
color: "#333",
}}
>
{card.title}
</h2>
<div style={{ display: "flex", justifyContent: "center" }}>
{card.component}
</div>
</div>
))}
</div>
</div>
);
}
/* ComparisonCard.module.css */
.card {
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
max-width: 250px;
}
.title {
margin: 0 0 8px 0;
font-size: 18px;
}
.description {
margin: 0 0 12px 0;
color: #666;
font-size: 14px;
}
.button {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.button:hover {
background-color: #0056b3;
}
- כל ארבע הגישות מייצרות כרטיס דומה חזותית
- הבדלים בכמות הקוד ובגישה - Mantine הכי קצר, inline הכי ארוך
תשובות לשאלות¶
-
CSS רגיל לעומת CSS Modules מבחינת scope - ב-CSS רגיל כל המחלקות הן גלובליות ויכולות להתנגש עם מחלקות באותו שם בקומפוננטות אחרות. ב-CSS Modules השמות עוברים hash אוטומטי (למשל
.cardהופך ל-.Card_card_x7g3k) כך שכל מחלקה ייחודית ומקומית לקומפוננטה. -
למה styled-components עלולה לפגוע בביצועים - styled-components מייצרת CSS בזמן ריצה (runtime). בכל רנדור, הספריה צריכה לחשב את הסגנונות, ליצור מחרוזות CSS, ולהזריק אותן ל-DOM דרך תגיות style. CSS Modules, לעומת זאת, מייצרות קבצי CSS סטטיים בזמן build, והדפדפן מטפל בהם ישירות ללא עלות JavaScript.
-
יתרון וחיסרון של Tailwind - היתרון: מהירות פיתוח גבוהה מאוד, אין צורך לחשוב על שמות מחלקות, והקובץ הסופי קטן כי Tailwind מסיר מחלקות שלא בשימוש. החיסרון: ה-HTML הופך ארוך ומלא מחלקות, מה שפוגע בקריאות, וקשה יותר לעשות שינויים עיצוביים גלובליים.
-
גישה לדשבורד אדמין - ספרית קומפוננטות כמו Mantine היא הבחירה הטובה ביותר. דשבורד דורש טבלאות, טפסים, modals, ניווט וגרפים - כולם קומפוננטות מורכבות שלא כדאי לבנות מאפס. Mantine מספקת את כולם מוכנים עם נגישות מובנית, וה-theme המאוחד מבטיח עקביות בעיצוב.
-
מתי inline styles מתאימים ומתי לא - מתאימים: סגנונות דינמיים פשוטים שמשתנים על בסיס props או state (כמו רוחב של progress bar, מיקום של tooltip). לא מתאימים: כשצריך hover effects, media queries, animations, או כשיש הרבה סגנונות - במקרים אלה עדיף CSS Modules או Tailwind.