לדלג לתוכן

8.2 מודולי CSS וטיילווינד פתרון

פתרון - מודולי CSS וטיילווינד

פתרון תרגיל 1

/* shared.module.css */
.flexCenter {
  display: flex;
  align-items: center;
}

.transitionAll {
  transition: all 0.3s ease;
}
/* Sidebar.module.css */
.sidebar {
  composes: transitionAll from "./shared.module.css";
  width: 60px;
  height: 100vh;
  background-color: #1a1a2e;
  color: white;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: fixed;
  top: 0;
  right: 0;
}

.sidebar:hover {
  width: 240px;
}

.logo {
  composes: flexCenter from "./shared.module.css";
  composes: transitionAll from "./shared.module.css";
  padding: 16px;
  height: 60px;
  font-size: 20px;
  font-weight: bold;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  white-space: nowrap;
  overflow: hidden;
}

.nav {
  display: flex;
  flex-direction: column;
  padding: 8px;
  gap: 4px;
  flex: 1;
}

.link {
  composes: flexCenter from "./shared.module.css";
  composes: transitionAll from "./shared.module.css";
  gap: 12px;
  padding: 10px 16px;
  border-radius: 8px;
  color: #a0a0b0;
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  cursor: pointer;
}

.link:hover {
  background-color: rgba(255, 255, 255, 0.1);
  color: white;
}

.linkActive {
  composes: link;
  background-color: rgba(233, 69, 96, 0.2);
  color: white;
  border-right: 3px solid #e94560;
}

.icon {
  min-width: 24px;
  text-align: center;
  font-size: 18px;
}

.label {
  composes: transitionAll from "./shared.module.css";
  opacity: 0;
  font-size: 14px;
}

.sidebar:hover .label {
  opacity: 1;
}
// Sidebar.tsx
import styles from "./Sidebar.module.css";

interface NavItem {
  icon: string;
  label: string;
  path: string;
}

interface SidebarProps {
  activePath: string;
  onNavigate: (path: string) => void;
}

const navItems: NavItem[] = [
  { icon: "H", label: "בית", path: "/" },
  { icon: "U", label: "משתמשים", path: "/users" },
  { icon: "S", label: "הגדרות", path: "/settings" },
  { icon: "R", label: "דוחות", path: "/reports" },
];

function Sidebar({ activePath, onNavigate }: SidebarProps) {
  return (
    <aside className={styles.sidebar}>
      <div className={styles.logo}>
        <span className={styles.icon}>A</span>
        <span className={styles.label}>אפליקציה</span>
      </div>
      <nav className={styles.nav}>
        {navItems.map((item) => (
          <a
            key={item.path}
            className={
              activePath === item.path ? styles.linkActive : styles.link
            }
            onClick={() => onNavigate(item.path)}
          >
            <span className={styles.icon}>{item.icon}</span>
            <span className={styles.label}>{item.label}</span>
          </a>
        ))}
      </nav>
    </aside>
  );
}
  • composes משתף סגנונות בין מחלקות ללא כפילות
  • הסיידבר מתכווץ ומתרחב עם CSS transition בלבד
  • שימוש ב-overflow: hidden מונע טקסט חתוך

פתרון תרגיל 2

interface Post {
  id: number;
  title: string;
  excerpt: string;
  date: string;
}

const posts: Post[] = [
  { id: 1, title: "פוסט ראשון", excerpt: "תיאור קצר של הפוסט הראשון שלי", date: "2024-01-15" },
  { id: 2, title: "פוסט שני", excerpt: "תיאור קצר של הפוסט השני שלי", date: "2024-02-20" },
  { id: 3, title: "פוסט שלישי", excerpt: "תיאור קצר של הפוסט השלישי שלי", date: "2024-03-10" },
  { id: 4, title: "פוסט רביעי", excerpt: "תיאור קצר של הפוסט הרביעי שלי", date: "2024-04-05" },
  { id: 5, title: "פוסט חמישי", excerpt: "תיאור קצר של הפוסט החמישי שלי", date: "2024-05-18" },
  { id: 6, title: "פוסט שישי", excerpt: "תיאור קצר של הפוסט השישי שלי", date: "2024-06-22" },
];

function ProfilePage() {
  return (
    <div className="min-h-screen bg-gray-100 dark:bg-gray-900">
      {/* כותרת עם תמונת כיסוי */}
      <div className="relative">
        <div className="h-48 md:h-64 bg-gradient-to-l from-blue-500 to-purple-600" />
        <div className="absolute -bottom-16 right-8">
          <div className="w-32 h-32 rounded-full border-4 border-white dark:border-gray-900 bg-gray-300 dark:bg-gray-700 flex items-center justify-center text-4xl text-gray-500">
            U
          </div>
        </div>
      </div>

      {/* פרטי משתמש */}
      <div className="pt-20 px-8">
        <h1 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white">
          ישראל ישראלי
        </h1>
        <p className="text-gray-600 dark:text-gray-400 mt-1">
          מפתח Full Stack - אוהב לבנות דברים יפים ושימושיים
        </p>
      </div>

      {/* סטטיסטיקות */}
      <div className="flex gap-8 px-8 mt-6 pb-6 border-b border-gray-200 dark:border-gray-700">
        <div className="text-center">
          <div className="text-xl font-bold text-gray-900 dark:text-white">42</div>
          <div className="text-sm text-gray-500 dark:text-gray-400">פוסטים</div>
        </div>
        <div className="text-center">
          <div className="text-xl font-bold text-gray-900 dark:text-white">1.2K</div>
          <div className="text-sm text-gray-500 dark:text-gray-400">עוקבים</div>
        </div>
        <div className="text-center">
          <div className="text-xl font-bold text-gray-900 dark:text-white">350</div>
          <div className="text-sm text-gray-500 dark:text-gray-400">עוקב</div>
        </div>
      </div>

      {/* רשת פוסטים */}
      <div className="p-8">
        <h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">
          הפוסטים שלי
        </h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {posts.map((post) => (
            <div
              key={post.id}
              className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
            >
              <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
                {post.title}
              </h3>
              <p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
                {post.excerpt}
              </p>
              <span className="text-xs text-gray-400 dark:text-gray-500">
                {post.date}
              </span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}
  • תמונת הפרופיל חופפת לתמונת הכיסוי עם absolute ו--bottom-16
  • רשת רספונסיבית עם grid-cols-1/2/3 לפי גודל מסך
  • כל הצבעים תומכים במצב כהה עם dark: prefix

פתרון תרגיל 3

interface PlanFeature {
  text: string;
  included: boolean;
}

interface Plan {
  name: string;
  price: string;
  period: string;
  features: PlanFeature[];
  popular?: boolean;
}

const plans: Plan[] = [
  {
    name: "בסיסי",
    price: "29",
    period: "חודש",
    features: [
      { text: "עד 5 פרויקטים", included: true },
      { text: "אחסון 10GB", included: true },
      { text: "תמיכה באימייל", included: true },
      { text: "API גישה", included: false },
      { text: "התאמה אישית", included: false },
    ],
  },
  {
    name: "פרו",
    price: "79",
    period: "חודש",
    popular: true,
    features: [
      { text: "פרויקטים ללא הגבלה", included: true },
      { text: "אחסון 100GB", included: true },
      { text: "תמיכה מועדפת", included: true },
      { text: "API גישה", included: true },
      { text: "התאמה אישית", included: false },
    ],
  },
  {
    name: "ארגוני",
    price: "199",
    period: "חודש",
    features: [
      { text: "פרויקטים ללא הגבלה", included: true },
      { text: "אחסון ללא הגבלה", included: true },
      { text: "תמיכה 24/7", included: true },
      { text: "API גישה", included: true },
      { text: "התאמה אישית מלאה", included: true },
    ],
  },
];

function PricingCards() {
  return (
    <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-16 px-4">
      <h2 className="text-3xl md:text-4xl font-bold text-center text-gray-900 dark:text-white mb-4">
        בחרו את התוכנית שלכם
      </h2>
      <p className="text-center text-gray-600 dark:text-gray-400 mb-12">
        התחילו בחינם ושדרגו בכל עת
      </p>

      <div className="max-w-5xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8 items-start">
        {plans.map((plan) => (
          <div
            key={plan.name}
            className={`relative bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-sm hover:shadow-xl transition-shadow ${
              plan.popular
                ? "border-2 border-blue-500 scale-105 md:scale-110"
                : "border border-gray-200 dark:border-gray-700"
            }`}
          >
            {plan.popular && (
              <div className="absolute -top-4 right-1/2 translate-x-1/2 bg-blue-500 text-white text-sm font-medium px-4 py-1 rounded-full">
                פופולרי
              </div>
            )}

            <h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
              {plan.name}
            </h3>
            <div className="flex items-baseline gap-1 mb-6">
              <span className="text-4xl font-bold text-gray-900 dark:text-white">
                {plan.price}
              </span>
              <span className="text-gray-500 dark:text-gray-400">
                ש"ח / {plan.period}
              </span>
            </div>

            <ul className="space-y-3 mb-8">
              {plan.features.map((feature) => (
                <li key={feature.text} className="flex items-center gap-2">
                  <span
                    className={
                      feature.included
                        ? "text-green-500 font-bold"
                        : "text-gray-300 dark:text-gray-600"
                    }
                  >
                    {feature.included ? "V" : "X"}
                  </span>
                  <span
                    className={
                      feature.included
                        ? "text-gray-700 dark:text-gray-300"
                        : "text-gray-400 dark:text-gray-500 line-through"
                    }
                  >
                    {feature.text}
                  </span>
                </li>
              ))}
            </ul>

            <button
              className={`w-full py-3 rounded-lg font-medium transition-colors ${
                plan.popular
                  ? "bg-blue-500 text-white hover:bg-blue-600"
                  : "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
              }`}
            >
              {plan.popular ? "התחל עכשיו" : "בחר תוכנית"}
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}
  • כרטיס הפרו גדול יותר עם scale-105/110
  • תגית "פופולרי" ממוקמת עם absolute ו-negative top
  • שימוש ב-line-through לתכונות לא כלולות

פתרון תרגיל 4

/* ResponsiveTable.module.css */
.tableContainer {
  overflow-x: auto;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.table {
  width: 100%;
  border-collapse: collapse;
  background-color: white;
}

.headerRow {
  background-color: #f8f9fa;
  border-bottom: 2px solid #dee2e6;
}

.headerCell {
  padding: 12px 16px;
  text-align: right;
  font-weight: 600;
  color: #333;
  font-size: 14px;
}

.row {
  border-bottom: 1px solid #eee;
  transition: background-color 0.15s;
}

.row:hover {
  background-color: #f8f9ff;
}

.row:nth-child(even) {
  background-color: #fafafa;
}

.row:nth-child(even):hover {
  background-color: #f0f0ff;
}

.cell {
  padding: 12px 16px;
  font-size: 14px;
  color: #555;
}

.label {
  display: none;
  font-weight: 600;
  color: #333;
  margin-bottom: 4px;
}

@media (max-width: 768px) {
  .headerRow {
    display: none;
  }

  .table,
  .table tbody,
  .row,
  .cell {
    display: block;
    width: 100%;
  }

  .row {
    padding: 16px;
    margin-bottom: 12px;
    border-radius: 8px;
    border: 1px solid #eee;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
  }

  .row:nth-child(even) {
    background-color: white;
  }

  .cell {
    padding: 4px 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .cell::before {
    display: none;
  }

  .label {
    display: block;
  }
}
// ResponsiveTable.tsx
import styles from "./ResponsiveTable.module.css";

interface Employee {
  name: string;
  role: string;
  department: string;
  joinDate: string;
}

const employees: Employee[] = [
  { name: "יוסי כהן", role: "מפתח Frontend", department: "פיתוח", joinDate: "2022-03-15" },
  { name: "דנה לוי", role: "מעצבת UX", department: "עיצוב", joinDate: "2021-07-01" },
  { name: "אבי מזרחי", role: "מפתח Backend", department: "פיתוח", joinDate: "2023-01-10" },
  { name: "שירה אברהם", role: "מנהלת פרויקט", department: "ניהול", joinDate: "2020-11-20" },
];

const columns = [
  { key: "name", label: "שם" },
  { key: "role", label: "תפקיד" },
  { key: "department", label: "מחלקה" },
  { key: "joinDate", label: "תאריך הצטרפות" },
] as const;

function ResponsiveTable() {
  return (
    <div className={styles.tableContainer}>
      <table className={styles.table}>
        <thead>
          <tr className={styles.headerRow}>
            {columns.map((col) => (
              <th key={col.key} className={styles.headerCell}>
                {col.label}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {employees.map((employee) => (
            <tr key={employee.name} className={styles.row}>
              {columns.map((col) => (
                <td key={col.key} className={styles.cell}>
                  <span className={styles.label}>{col.label}:</span>
                  <span>{employee[col.key]}</span>
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
  • בדסקטופ: טבלה רגילה עם שורות מפוספסות ו-hover
  • בטלפון: כל שורה הופכת לכרטיס עם תוויות
  • ה-media query ב-768px הופך את הטבלה ל-block layout

פתרון תרגיל 5

import { useState } from "react";
import clsx from "clsx";

interface FormData {
  name: string;
  email: string;
  subject: string;
  message: string;
}

interface FormErrors {
  name?: string;
  email?: string;
  subject?: string;
  message?: string;
}

function ContactForm() {
  const [formData, setFormData] = useState<FormData>({
    name: "",
    email: "",
    subject: "",
    message: "",
  });

  const [errors, setErrors] = useState<FormErrors>({});
  const [touched, setTouched] = useState<Record<string, boolean>>({});

  function validate(data: FormData): FormErrors {
    const errs: FormErrors = {};
    if (!data.name.trim()) errs.name = "שם הוא שדה חובה";
    if (!data.email.trim()) errs.email = "אימייל הוא שדה חובה";
    else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email))
      errs.email = "כתובת אימייל לא תקינה";
    if (!data.subject) errs.subject = "יש לבחור נושא";
    if (!data.message.trim()) errs.message = "הודעה היא שדה חובה";
    else if (data.message.trim().length < 10)
      errs.message = "ההודעה צריכה להכיל לפחות 10 תווים";
    return errs;
  }

  function handleChange(field: keyof FormData, value: string) {
    const updated = { ...formData, [field]: value };
    setFormData(updated);
    if (touched[field]) {
      setErrors(validate(updated));
    }
  }

  function handleBlur(field: string) {
    setTouched((prev) => ({ ...prev, [field]: true }));
    setErrors(validate(formData));
  }

  const currentErrors = validate(formData);
  const isValid = Object.keys(currentErrors).length === 0;

  function inputClasses(field: keyof FormErrors) {
    return clsx(
      "w-full px-4 py-3 rounded-lg border transition-colors",
      "focus:outline-none focus:ring-2",
      "dark:bg-gray-800 dark:text-white",
      {
        "border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-200 dark:focus:ring-blue-800":
          !touched[field] || !errors[field],
        "border-red-500 focus:border-red-500 focus:ring-red-200 dark:focus:ring-red-800":
          touched[field] && errors[field],
        "border-green-500 focus:border-green-500 focus:ring-green-200 dark:focus:ring-green-800":
          touched[field] && !errors[field] && formData[field],
      }
    );
  }

  return (
    <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
      <div className="max-w-2xl mx-auto">
        <h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2 text-center">
          צור קשר
        </h2>
        <p className="text-gray-600 dark:text-gray-400 mb-8 text-center">
          נשמח לשמוע ממך
        </p>

        <form className="space-y-6 bg-white dark:bg-gray-800 p-8 rounded-xl shadow-sm">
          <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
            {/* שם */}
            <div>
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
                שם מלא
              </label>
              <input
                type="text"
                value={formData.name}
                onChange={(e) => handleChange("name", e.target.value)}
                onBlur={() => handleBlur("name")}
                className={inputClasses("name")}
                placeholder="הכנס שם מלא"
              />
              {touched.name && errors.name && (
                <p className="mt-1 text-sm text-red-500">{errors.name}</p>
              )}
            </div>

            {/* אימייל */}
            <div>
              <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
                אימייל
              </label>
              <input
                type="email"
                value={formData.email}
                onChange={(e) => handleChange("email", e.target.value)}
                onBlur={() => handleBlur("email")}
                className={inputClasses("email")}
                placeholder="example@email.com"
              />
              {touched.email && errors.email && (
                <p className="mt-1 text-sm text-red-500">{errors.email}</p>
              )}
            </div>
          </div>

          {/* נושא */}
          <div>
            <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
              נושא
            </label>
            <select
              value={formData.subject}
              onChange={(e) => handleChange("subject", e.target.value)}
              onBlur={() => handleBlur("subject")}
              className={inputClasses("subject")}
            >
              <option value="">בחר נושא</option>
              <option value="general">שאלה כללית</option>
              <option value="support">תמיכה טכנית</option>
              <option value="billing">חיוב ותשלום</option>
              <option value="feedback">משוב</option>
            </select>
            {touched.subject && errors.subject && (
              <p className="mt-1 text-sm text-red-500">{errors.subject}</p>
            )}
          </div>

          {/* הודעה */}
          <div>
            <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
              הודעה
            </label>
            <textarea
              rows={5}
              value={formData.message}
              onChange={(e) => handleChange("message", e.target.value)}
              onBlur={() => handleBlur("message")}
              className={`${inputClasses("message")} resize-none`}
              placeholder="כתוב את ההודעה שלך..."
            />
            {touched.message && errors.message && (
              <p className="mt-1 text-sm text-red-500">{errors.message}</p>
            )}
          </div>

          {/* כפתור שליחה */}
          <button
            type="submit"
            disabled={!isValid}
            className={clsx(
              "w-full py-3 rounded-lg font-medium transition-colors",
              {
                "bg-blue-500 text-white hover:bg-blue-600": isValid,
                "bg-gray-300 dark:bg-gray-600 text-gray-500 dark:text-gray-400 cursor-not-allowed":
                  !isValid,
              }
            )}
          >
            שלח הודעה
          </button>
        </form>
      </div>
    </div>
  );
}
  • ולידציה עם שלושה מצבים ויזואליים: רגיל, שגיאה (אדום), תקין (ירוק)
  • שדות מוצגים בשתי עמודות בדסקטופ ואחת בטלפון
  • הכפתור disabled כשהטופס לא תקין

פתרון תרגיל 6

/* Header.module.css */
.header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 60px;
  background-color: white;
  border-bottom: 1px solid #e5e7eb;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
  z-index: 50;
  animation: slideDown 0.3s ease-out;
}

.logo {
  font-size: 20px;
  font-weight: bold;
  color: #1a1a2e;
}

.nav {
  display: flex;
  gap: 24px;
}

.navLink {
  color: #555;
  text-decoration: none;
  font-size: 14px;
  transition: color 0.2s;
}

.navLink:hover {
  color: #007bff;
}

.hamburger {
  display: none;
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
}

@keyframes slideDown {
  from {
    transform: translateY(-100%);
  }
  to {
    transform: translateY(0);
  }
}

@media (max-width: 768px) {
  .nav {
    display: none;
  }
  .hamburger {
    display: block;
  }
}
/* AppSidebar.module.css */
.sidebar {
  position: fixed;
  top: 60px;
  right: 0;
  width: 250px;
  height: calc(100vh - 60px);
  background-color: #f8f9fa;
  border-left: 1px solid #e5e7eb;
  padding: 16px;
  transform: translateX(0);
  transition: transform 0.3s ease;
  z-index: 40;
}

.sidebarClosed {
  transform: translateX(100%);
}

.overlay {
  position: fixed;
  inset: 0;
  top: 60px;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 30;
  animation: fadeIn 0.2s ease;
}

.sidebarLink {
  display: block;
  padding: 10px 12px;
  border-radius: 6px;
  color: #555;
  text-decoration: none;
  transition: background-color 0.15s;
  font-size: 14px;
}

.sidebarLink:hover {
  background-color: #e9ecef;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@media (min-width: 769px) {
  .sidebar {
    transform: translateX(0) !important;
  }
  .overlay {
    display: none;
  }
}
// AppLayout.tsx
import { useState } from "react";
import headerStyles from "./Header.module.css";
import sidebarStyles from "./AppSidebar.module.css";

function AppLayout() {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  return (
    <div className="min-h-screen bg-gray-50">
      {/* Header - CSS Modules */}
      <header className={headerStyles.header}>
        <div className={headerStyles.logo}>MyApp</div>
        <nav className={headerStyles.nav}>
          <a href="/" className={headerStyles.navLink}>בית</a>
          <a href="/about" className={headerStyles.navLink}>אודות</a>
          <a href="/contact" className={headerStyles.navLink}>צור קשר</a>
        </nav>
        <button
          className={headerStyles.hamburger}
          onClick={() => setSidebarOpen(!sidebarOpen)}
        >
          =
        </button>
      </header>

      {/* Sidebar - CSS Modules */}
      {sidebarOpen && (
        <div
          className={sidebarStyles.overlay}
          onClick={() => setSidebarOpen(false)}
        />
      )}
      <aside
        className={`${sidebarStyles.sidebar} ${
          !sidebarOpen ? sidebarStyles.sidebarClosed : ""
        }`}
      >
        <a href="#" className={sidebarStyles.sidebarLink}>דשבורד</a>
        <a href="#" className={sidebarStyles.sidebarLink}>משתמשים</a>
        <a href="#" className={sidebarStyles.sidebarLink}>הגדרות</a>
        <a href="#" className={sidebarStyles.sidebarLink}>דוחות</a>
      </aside>

      {/* תוכן ראשי - Tailwind */}
      <main className="pt-[60px] md:pr-[250px] p-6">
        <h1 className="text-2xl font-bold text-gray-800 mb-6">דשבורד</h1>
        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
          {[1, 2, 3, 4, 5, 6].map((n) => (
            <div
              key={n}
              className="bg-white rounded-lg p-6 shadow-sm hover:shadow-md transition-shadow"
            >
              <h3 className="text-lg font-semibold text-gray-800 mb-2">
                כרטיסייה {n}
              </h3>
              <p className="text-gray-500 text-sm">
                תוכן לדוגמה של הכרטיסייה
              </p>
            </div>
          ))}
        </div>
      </main>

      {/* Footer - Tailwind */}
      <footer className="md:pr-[250px] bg-white border-t border-gray-200 py-6 px-6 text-center text-sm text-gray-500">
        כל הזכויות שמורות 2024
      </footer>
    </div>
  );
}
  • Header ו-Sidebar עם CSS Modules בגלל אנימציות מורכבות
  • תוכן ראשי ו-Footer עם Tailwind לפשטות
  • בטלפון הסיידבר מוסתר ומופיע עם כפתור המבורגר

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

  1. composes וכפילות - composes לא יוצר כפילות CSS. הוא פשוט מוסיף את שמות המחלקות המיובאות לאלמנט ב-HTML. אם מחלקה A עושה composes ל-B, האלמנט יקבל את שתי המחלקות, וה-CSS של B נכתב פעם אחת בלבד.

  2. sm: לעומת max-sm: - sm: הוא mobile-first ומפעיל סגנונות מ-640px ומעלה. max-sm: מפעיל סגנונות עד 640px בלבד. נשתמש ב-sm: כשרוצים להוסיף סגנונות למסכים גדולים (הדרך המומלצת), וב-max-sm: כשרוצים להגדיר משהו רק לטלפונים.

  3. CSS קטן ב-Tailwind - Tailwind סורק את קבצי הפרויקט ומייצר CSS רק למחלקות שבאמת בשימוש (tree-shaking). אם לא השתמשתם ב-text-purple-700, הוא לא ייכלל בקובץ הסופי. לכן גודל הקובץ קטן.

  4. מתי CSS Module ומתי Tailwind - CSS Modules מתאימים לאנימציות מורכבות, keyframes, סלקטורים מקוננים ומורכבים, ומצבים שקשה לבטא ב-Tailwind. Tailwind מתאים לרוב הסגנונות היומיומיים - spacing, colors, flexbox, typography. הם משתלבים יפה יחד.

  5. group-hover: לעומת CSS רגיל - group-hover: מאפשר להגדיר את האפקט ישירות על אלמנט הילד בלי צורך לכתוב CSS עם סלקטור מורכב כמו .parent:hover .child. זה שומר על הלוגיקה הויזואלית צמודה לאלמנט עצמו ומקל על הבנת הקוד.