Portofolio adalah kartu nama seorang developer: halaman pertama yang dibuka perekrut, dan sekaligus bukti bahwa pemiliknya mampu menyusun CSS yang rapi. Pada proyek ini kita membangun website portofolio satu halaman untuk Budi Santoso, lengkap dengan navigation bar yang menempel saat digulir, hero dua kolom, grid tiga kartu proyek, dan media query untuk layar kecil. Proyek ini satu tingkat lebih besar daripada Proyek: Halaman Profil , sehingga konsistensi dijaga lewat design tokens sejak baris pertama. Empat bagian berurutan memperluas satu file styles/portofolio.css, dan setiap bagian ditutup halaman uji otomatis.

Tujuan Proyek #

Target yang kita capai di akhir bagian keempat:

  • Design tokens untuk warna, spasi, radius, dan tipografi didefinisikan sekali di :root dan dikonsumsi seluruh komponen.
  • Navigation bar position: sticky dengan latar semi transparan dan backdrop-filter.
  • Hero dua kolom berbasis grid dengan judul fluid clamp() dan sepasang tombol aksi.
  • Grid proyek repeat(auto-fill, minmax(280px, 1fr)) yang menyusut jumlah kolomnya mengikuti ruang.
  • Media query max-width pada dua breakpoint (767px dan 1023px) untuk menurunkan layout ke mobile.

Lebih penting dari daftar itu: kita berlatih mengubah keputusan desain menjadi token, lalu menurunkan komponen dari token tersebut. Perubahan warna primer selanjutnya hanya menyentuh satu baris.

Prasyarat #

Proyek ini memakai konsep dari beberapa Section sebelumnya tanpa mengulang teorinya:

Struktur Folder Proyek #

Struktur berikut adalah kondisi akhir setelah empat bagian selesai:

text
website-portofolio/
├── index.html              markup halaman utuh
├── styles/
│   └── portofolio.css      satu file CSS utuh yang berkembang
├── assets/
│   ├── foto-hero.svg       ilustrasi jendela editor untuk hero
│   ├── thumbnail-toko.svg  gambar kartu proyek "Toko Kita"
│   ├── thumbnail-dasbor.svg  gambar kartu proyek "Dasbor Penjualan"
│   └── thumbnail-blog.svg  gambar kartu proyek "Blog Tulisan Kode"
├── tests/
│   ├── design-tokens.spec.js
│   ├── navigation-dan-hero.spec.js
│   ├── project-grid.spec.js
│   └── responsive-design.spec.js
└── bagian/
    ├── design-tokens/
    ├── navigation-dan-hero/
    ├── project-grid/
    └── responsive-design/

Sama seperti proyek sebelumnya, styles/portofolio.css adalah satu file yang di-update di tiap bagian, bukan diganti. Komentar penanda Bagian N di dalam file menandai asal setiap kelompok aturan, dan media query responsif ditumpuk paling akhir.

Daftar Bagian #

BagianFokus
Bagian 1: Design TokensToken warna, spasi, radius, dan komponen tombol serta badge
Bagian 2: Navigation dan HeroNavigation sticky, wadah 1120px, hero grid dua kolom
Bagian 3: Project GridGrid auto-fill minmax(280px, 1fr) dan kartu proyek
Bagian 4: Responsive DesignMedia query max-width 767px dan 1023px untuk mobile

Menjalankan Halaman Uji Otomatis #

Setiap bagian memiliki halaman uji mandiri di bawah /tests/css/proyek/website-portofolio/<topic>.html, misalnya uji Bagian 1 . Elemen probe di dalamnya membawa atribut data-expected berisi JSON pasangan properti dan nilai computed; harness membuka halaman di headless Chromium dan membandingkan hasil getComputedStyle() dengan nilai tersebut.

Jalankan seluruh uji proyek ini dengan satu perintah:

bash
node scripts/css-test/run_tests.mjs --filter website-portofolio

Ringkasan asersi per bagian juga tersedia sebagai tests/<topic>.spec.js yang dapat dibaca dan diunduh melalui bagian Berkas Sumber. Untuk pemeriksaan manual, buka halaman uji di browser, tekan F12, lalu bandingkan panel Computed dengan tabel asersi di akhir setiap bagian.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

styles/portofolio.css

Unduh berkas
css
/* =========================================================
   Proyek: Website Portofolio — styles/portofolio.css
   File utuh proyek yang berkembang mengikuti empat bagian:
   Bagian 1 (Design Tokens), Bagian 2 (Navigation dan Hero),
   Bagian 3 (Project Grid), Bagian 4 (Responsive Design).
   Komentar "Bagian N" menandai kelompok aturan yang dibahas
   pada setiap bagian.
   ========================================================= */

/* Bagian 1: Design Tokens --------------------------------- */

:root {
  /* Warna semantik */
  --warna-primer: #2563eb;
  --warna-primer-tua: #1e40af;
  --warna-latar: #f8fafc;
  --warna-permukaan: #ffffff;
  --warna-teks: #0f172a;
  --warna-teks-lembut: #475569;
  --warna-teks-balik: #ffffff;
  --warna-garis: #e2e8f0;

  /* Skala spasi */
  --spasi-1: 4px;
  --spasi-2: 8px;
  --spasi-3: 12px;
  --spasi-4: 16px;
  --spasi-6: 24px;
  --spasi-8: 32px;
  --spasi-16: 64px;

  /* Bentuk */
  --radius-sedang: 10px;
  --radius-penuh: 999px;

  /* Tipografi */
  --font-utama: system-ui, sans-serif;
  --teks-dasar: 16px;
  --teks-lg: 20px;
}

* {
  margin: 0;
  box-sizing: border-box;
}

body {
  font-family: var(--font-utama);
  font-size: var(--teks-dasar);
  line-height: 1.6;
  color: var(--warna-teks);
  background-color: var(--warna-latar);
}

.tombol {
  display: inline-block;
  padding: var(--spasi-3) var(--spasi-4);
  border-radius: var(--radius-sedang);
  font-size: var(--teks-dasar);
  font-weight: 500;
  text-decoration: none;
}

.tombol-primer {
  background-color: var(--warna-primer);
  color: var(--warna-teks-balik);
}

.tombol-kedua {
  background-color: var(--warna-permukaan);
  color: var(--warna-primer);
  border: 1px solid var(--warna-primer);
}

.badge {
  display: inline-block;
  padding: var(--spasi-1) var(--spasi-2);
  margin-bottom: var(--spasi-4);
  border-radius: var(--radius-penuh);
  background-color: rgb(37 99 235 / 12%);
  color: var(--warna-primer-tua);
  font-size: 14px;
  font-weight: 500;
}

/* Bagian 2: Navigation dan Hero --------------------------- */

.navigasi {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spasi-4);
  padding: var(--spasi-4) var(--spasi-6);
  background-color: rgb(255 255 255 / 85%);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--warna-garis);
}

.logo {
  font-size: var(--teks-lg);
  font-weight: 700;
  color: var(--warna-teks);
  text-decoration: none;
}

.navigasi-menu {
  display: flex;
  gap: var(--spasi-6);
  list-style: none;
}

.navigasi-tautan {
  color: var(--warna-teks-lembut);
  font-weight: 500;
  text-decoration: none;
}

.navigasi-tautan:hover {
  color: var(--warna-primer);
}

.wadah {
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 var(--spasi-6); /* Bagian 4: 16px pada layar <= 767px */
}

.bagian-hero {
  display: grid;
  grid-template-columns: 1.1fr 0.9fr; /* Bagian 4: satu kolom pada layar <= 1023px */
  gap: var(--spasi-8);
  align-items: center;
  padding: var(--spasi-16) 0;
}

.judul-hero {
  font-size: clamp(30px, 28px + 1.25vw, 44px);
  line-height: 1.15;
  color: var(--warna-teks);
}

.teks-aksen {
  color: var(--warna-primer);
}

.deskripsi-hero {
  margin-top: var(--spasi-4);
  font-size: 18px;
  line-height: 1.6;
  color: var(--warna-teks-lembut);
}

.hero-aksi {
  display: flex;
  gap: var(--spasi-4);
  margin-top: var(--spasi-6);
}

.foto-hero {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 16px;
  border: 1px solid var(--warna-garis);
}

/* Bagian 3: Project Grid ---------------------------------- */

.judul-bagian {
  font-size: 28px;
  font-weight: 700;
}

.deskripsi-bagian {
  margin-top: var(--spasi-2);
  max-width: 560px;
  color: var(--warna-teks-lembut);
}

.grid-proyek {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); /* Bagian 4: minmax(240px, 1fr) pada layar <= 767px */
  gap: var(--spasi-6);
  margin-top: var(--spasi-8);
}

.kartu-proyek {
  display: flex;
  flex-direction: column;
  background-color: var(--warna-permukaan);
  border: 1px solid var(--warna-garis);
  border-radius: var(--radius-sedang);
  overflow: hidden;
}

.kartu-proyek-img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

.kartu-proyek-isi {
  display: grid;
  gap: var(--spasi-2);
  padding: var(--spasi-6);
}

.kartu-proyek-judul {
  font-size: var(--teks-lg);
  font-weight: 600;
}

.kartu-proyek-deskripsi {
  font-size: 14px;
  color: var(--warna-teks-lembut);
}

.tautan-kartu {
  color: var(--warna-primer);
  font-weight: 500;
  text-decoration: none;
}

/* Bagian 4: Responsive Design ----------------------------- */

@media (max-width: 767px) {
  .wadah {
    padding: 0 var(--spasi-4);
  }

  .navigasi-menu {
    flex-direction: column;
    gap: var(--spasi-2);
  }

  .grid-proyek {
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  }
}

@media (max-width: 1023px) {
  .bagian-hero {
    grid-template-columns: 1fr;
  }
}

tests/design-tokens.spec.js

Unduh berkas
javascript
// ============================================================
// Spesifikasi uji — Proyek: Website Portofolio
// Bagian 1: Design Tokens
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji:
//   /tests/css/proyek/website-portofolio/design-tokens.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.

const asersi = [
  // Token tersimpan di :root (elemen html) dan terbaca apa adanya.
  { elemen: "html", properti: "--warna-primer", diharapkan: "#2563eb" },
  { elemen: "html", properti: "--spasi-6", diharapkan: "24px" },
  { elemen: "html", properti: "--radius-sedang", diharapkan: "10px" },

  // body mengonsumsi token: latar slate-50 dan teks slate-900.
  { elemen: "body", properti: "background-color", diharapkan: "rgb(248, 250, 252)" },
  { elemen: "body", properti: "color", diharapkan: "rgb(15, 23, 42)" },
  { elemen: "body", properti: "font-size", diharapkan: "16px" },

  // .tombol-primer: padding dari token spasi, radius dari token bentuk.
  { elemen: ".tombol-primer", properti: "background-color", diharapkan: "rgb(37, 99, 235)" },
  { elemen: ".tombol-primer", properti: "color", diharapkan: "rgb(255, 255, 255)" },
  { elemen: ".tombol-primer", properti: "padding-top", diharapkan: "12px" },
  { elemen: ".tombol-primer", properti: "padding-left", diharapkan: "16px" },
  { elemen: ".tombol-primer", properti: "border-radius", diharapkan: "10px" },

  // .tombol-kedua: permukaan putih dengan teks dan border primer.
  { elemen: ".tombol-kedua", properti: "background-color", diharapkan: "rgb(255, 255, 255)" },
  { elemen: ".tombol-kedua", properti: "color", diharapkan: "rgb(37, 99, 235)" },
  { elemen: ".tombol-kedua", properti: "border-top-color", diharapkan: "rgb(37, 99, 235)" },

  // .badge memakai warna primer dengan alpha 12% dan radius penuh.
  { elemen: ".badge", properti: "background-color", diharapkan: "rgba(37, 99, 235, 0.12)" },
  { elemen: ".badge", properti: "color", diharapkan: "rgb(30, 64, 175)" },
  { elemen: ".badge", properti: "border-radius", diharapkan: "999px" },
  { elemen: ".badge", properti: "font-size", diharapkan: "14px" },
];

tests/navigation-dan-hero.spec.js

Unduh berkas
javascript
// ============================================================
// Spesifikasi uji — Proyek: Website Portofolio
// Bagian 2: Navigation dan Hero
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji (viewport 1280x720):
//   /tests/css/proyek/website-portofolio/navigation-dan-hero.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.

const asersi = [
  // Navigation menempel di atas: sticky dengan jarak 0 dari tepi.
  { elemen: ".navigasi", properti: "position", diharapkan: "sticky" },
  { elemen: ".navigasi", properti: "top", diharapkan: "0px" },
  { elemen: ".navigasi", properti: "z-index", diharapkan: "10" },

  // Latar navigasi putih transparan 85% dengan efek blur di belakangnya.
  { elemen: ".navigasi", properti: "background-color", diharapkan: "rgba(255, 255, 255, 0.85)" },
  { elemen: ".navigasi", properti: "backdrop-filter", diharapkan: "blur(8px)" },

  // Menu navigasi tersusun horizontal dengan jarak antartautan 24px.
  { elemen: ".navigasi-menu", properti: "display", diharapkan: "flex" },
  { elemen: ".navigasi-menu", properti: "flex-direction", diharapkan: "row" },
  { elemen: ".navigasi-menu", properti: "gap", diharapkan: "24px" },

  // Judul hero fluid: clamp mencapai batas atas 44px pada viewport 1280px.
  { elemen: ".judul-hero", properti: "font-size", diharapkan: "44px" },
  { elemen: ".judul-hero", properti: "line-height", diharapkan: "50.6px" },

  // Deskripsi hero 18px dengan warna teks lembut.
  { elemen: ".deskripsi-hero", properti: "font-size", diharapkan: "18px" },
  { elemen: ".deskripsi-hero", properti: "color", diharapkan: "rgb(71, 85, 105)" },

  // Baris tombol aksi adalah flex dengan gap 16px.
  { elemen: ".hero-aksi", properti: "display", diharapkan: "flex" },
  { elemen: ".hero-aksi", properti: "gap", diharapkan: "16px" },

  // Wadah konten dibatasi 1120px dan terpusat di viewport 1280px.
  { elemen: ".wadah", properti: "max-width", diharapkan: "1120px" },
  { elemen: ".wadah", properti: "margin-left", diharapkan: "80px" },
];

tests/project-grid.spec.js

Unduh berkas
javascript
// ============================================================
// Spesifikasi uji — Proyek: Website Portofolio
// Bagian 3: Project Grid
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji (viewport 1280x720, wadah 1120px dengan
// padding 24px sehingga konten selebar 1072px):
//   /tests/css/proyek/website-portofolio/project-grid.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.

const asersi = [
  // Grid proyek memakai auto-fill minmax(280px, 1fr): pada konten
  // 1072px muat 3 track berukuran sekitar 341px dengan gap 24px.
  // Nilai di bawah diambil dari serialisasi computed browser.
  { elemen: ".grid-proyek", properti: "display", diharapkan: "grid" },
  { elemen: ".grid-proyek", properti: "gap", diharapkan: "24px" },
  {
    elemen: ".grid-proyek",
    properti: "grid-template-columns",
    diharapkan: "341.328px 341.328px 341.344px",
  },

  // Kartu proyek: permukaan putih, garis 1px, radius 10px.
  { elemen: ".kartu-proyek", properti: "background-color", diharapkan: "rgb(255, 255, 255)" },
  { elemen: ".kartu-proyek", properti: "border-top-width", diharapkan: "1px" },
  { elemen: ".kartu-proyek", properti: "border-radius", diharapkan: "10px" },

  // Isi kartu adalah grid kecil dengan gap 8px dan padding 24px.
  { elemen: ".kartu-proyek-isi", properti: "display", diharapkan: "grid" },
  { elemen: ".kartu-proyek-isi", properti: "gap", diharapkan: "8px" },
  { elemen: ".kartu-proyek-isi", properti: "padding", diharapkan: "24px" },

  // Judul kartu 20px, deskripsi 14px dengan warna lembut.
  { elemen: ".kartu-proyek-judul", properti: "font-size", diharapkan: "20px" },
  { elemen: ".kartu-proyek-deskripsi", properti: "font-size", diharapkan: "14px" },
  { elemen: ".kartu-proyek-deskripsi", properti: "color", diharapkan: "rgb(71, 85, 105)" },

  // Tautan kartu berwarna primer.
  { elemen: ".tautan-kartu", properti: "color", diharapkan: "rgb(37, 99, 235)" },
];

tests/responsive-design.spec.js

Unduh berkas
javascript
// ============================================================
// Spesifikasi uji — Proyek: Website Portofolio
// Bagian 4: Responsive Design
// ============================================================
// Berkas ini merangkum asersi yang diverifikasi oleh harness
// pada halaman uji (viewport 1280x720, sehingga seluruh media
// query max-width berada pada keadaan tidak aktif dan nilai
// desktop yang berlaku):
//   /tests/css/proyek/website-portofolio/responsive-design.html
// Setiap entri memasangkan elemen probe dengan properti CSS
// dan nilai computed yang diharapkan.

const asersi = [
  // Wadah tetap 1120px dan berpadding 24px di luar breakpoint 767px.
  { elemen: ".wadah", properti: "max-width", diharapkan: "1120px" },
  { elemen: ".wadah", properti: "padding-left", diharapkan: "24px" },

  // Hero dua kolom: track 1.1fr dan 0.9fr dari konten 1040px
  // (1072 - gap 32) dibagi proporsional menjadi 572px dan 468px.
  { elemen: ".bagian-hero", properti: "display", diharapkan: "grid" },
  { elemen: ".bagian-hero", properti: "gap", diharapkan: "32px" },
  { elemen: ".bagian-hero", properti: "grid-template-columns", diharapkan: "572px 468px" },

  // Menu navigasi tetap horizontal dengan gap 24px.
  { elemen: ".navigasi-menu", properti: "flex-direction", diharapkan: "row" },
  { elemen: ".navigasi-menu", properti: "gap", diharapkan: "24px" },

  // Grid proyek tetap tiga track (minmax 280px) di desktop.
  {
    elemen: ".grid-proyek",
    properti: "grid-template-columns",
    diharapkan: "341.328px 341.328px 341.344px",
  },

  // Judul hero tetap 44px: clamp mencapai nilai maksimumnya.
  { elemen: ".judul-hero", properti: "font-size", diharapkan: "44px" },

  // Navigation tetap sticky di atas halaman.
  { elemen: ".navigasi", properti: "position", diharapkan: "sticky" },
  { elemen: ".navigasi", properti: "top", diharapkan: "0px" },
];