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
:rootdan dikonsumsi seluruh komponen. - Navigation bar
position: stickydengan latar semi transparan danbackdrop-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-widthpada 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:
- Design tokens dan arsitektur: Design Tokens serta Custom Properties .
- Layout: Flex Container dan Items untuk navigation, Grid Container dan Items untuk hero, dan Auto-fit dan Auto-fill untuk grid proyek.
- Pola halaman: Centered Container untuk wadah konten dan Card Grid sebagai gambaran pola kartu.
- Responsif: Media Queries dan Breakpoints .
Struktur Folder Proyek #
Struktur berikut adalah kondisi akhir setelah empat bagian selesai:
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 #
| Bagian | Fokus |
|---|---|
| Bagian 1: Design Tokens | Token warna, spasi, radius, dan komponen tombol serta badge |
| Bagian 2: Navigation dan Hero | Navigation sticky, wadah 1120px, hero grid dua kolom |
| Bagian 3: Project Grid | Grid auto-fill minmax(280px, 1fr) dan kartu proyek |
| Bagian 4: Responsive Design | Media 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:
node scripts/css-test/run_tests.mjs --filter website-portofolioRingkasan 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.
Definisikan token warna, spasi, radius, dan tipografi di :root sebagai design tokens yang dikonsumsi seluruh komponen portofolio.
Buka → # Bagian 2: Navigation dan HeroSusun navigation bar sticky dengan backdrop-filter dan hero grid dua kolom dengan judul fluid serta sepasang tombol aksi.
Buka → # Bagian 3: Project GridTampilkan tiga kartu proyek dalam grid repeat(auto-fill, minmax(280px, 1fr)) yang menyusut jumlah kolomnya mengikuti ruang.
Buka → # Bagian 4: Responsive DesignTurunkan portofolio ke layar kecil dengan media query max-width 767px dan 1023px untuk wadah, navigasi, hero, dan grid proyek.
Buka →Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
styles/portofolio.css
Unduh berkas/* =========================================================
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// ============================================================
// 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// ============================================================
// 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// ============================================================
// 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// ============================================================
// 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" },
];