Membuat komponen {i>switch

Ringkasan dasar tentang cara membuat komponen tombol yang responsif dan aksesibel.

Dalam postingan ini, saya ingin membagikan pemikiran tentang cara membuat komponen tombol. Coba demo.

Demo

Jika Anda lebih suka menonton video, berikut versi YouTube dari postingan ini:

Ringkasan

Tombol geser berfungsi mirip dengan kotak centang tetapi secara eksplisit merepresentasikan status boolean aktif dan nonaktif.

Demo ini menggunakan <input type="checkbox" role="switch"> untuk sebagian besar fungsinya, yang memiliki keunggulan tidak memerlukan CSS atau JavaScript agar berfungsi penuh dan dapat diakses. Memuat CSS menghadirkan dukungan untuk bahasa kanan ke kiri, vertikalitas, animasi, dan lainnya. Memuat JavaScript membuat tombol dapat ditarik dan terlihat nyata.

Properti kustom

Variabel berikut mewakili berbagai bagian pengalihan dan opsinya. Sebagai class tingkat teratas, .gui-switch berisi properti kustom yang digunakan di seluruh turunan komponen, dan titik entri untuk penyesuaian terpusat.

Lacak

Panjang (--track-size), padding, dan dua warna:

.gui-switch {
  --track-size: calc(var(--thumb-size) * 2);
  --track-padding: 2px;

  --track-inactive: hsl(80 0% 80%);
  --track-active: hsl(80 60% 45%);

  --track-color-inactive: var(--track-inactive);
  --track-color-active: var(--track-active);

  @media (prefers-color-scheme: dark) {
    --track-inactive: hsl(80 0% 35%);
    --track-active: hsl(80 60% 60%);
  }
}

Kalimba

Ukuran, warna latar belakang, dan warna sorotan interaksi:

.gui-switch {
  --thumb-size: 2rem;
  --thumb: hsl(0 0% 100%);
  --thumb-highlight: hsl(0 0% 0% / 25%);

  --thumb-color: var(--thumb);
  --thumb-color-highlight: var(--thumb-highlight);

  @media (prefers-color-scheme: dark) {
    --thumb: hsl(0 0% 5%);
    --thumb-highlight: hsl(0 0% 100% / 25%);
  }
}

Pengurangan gerakan

Untuk menambahkan alias yang jelas dan mengurangi pengulangan, kueri media preferensi gerakan yang dikurangi pengguna dapat dimasukkan ke dalam properti kustom dengan plugin PostCSS berdasarkan draf spesifikasi ini di Media Queries 5:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

Markup

Saya memilih untuk membungkus elemen <input type="checkbox" role="switch"> dengan <label>, menggabungkan hubungan keduanya untuk menghindari ambiguitas asosiasi kotak centang dan label, sekaligus memberi pengguna kemampuan untuk berinteraksi dengan label untuk mengalihkan input.

Kotak centang dan label alami tanpa gaya.

<label for="switch" class="gui-switch">
  Label text
  <input type="checkbox" role="switch" id="switch">
</label>

<input type="checkbox"> sudah dibuat sebelumnya dengan API dan status. Browser mengelola properti checked dan peristiwa input seperti oninput dan onchanged.

Tata letak

Flexbox, petak, dan properti kustom sangat penting dalam mempertahankan gaya komponen ini. Mereka memusatkan nilai, memberi nama pada perhitungan atau area yang ambigu, dan mengaktifkan API properti kustom kecil untuk memudahkan penyesuaian komponen.

.gui-switch

Tata letak tingkat teratas untuk tombol adalah flexbox. Class .gui-switch berisi properti kustom pribadi dan publik yang digunakan turunan untuk menghitung tata letaknya.

Overlay Flexbox DevTools pada label dan tombol horizontal, yang menampilkan distribusi ruang tata letaknya.

.gui-switch {
  display: flex;
  align-items: center;
  gap: 2ch;
  justify-content: space-between;
}

Memperluas dan mengubah tata letak flexbox sama seperti mengubah tata letak flexbox lainnya. Misalnya, untuk menempatkan label di atas atau di bawah tombol, atau untuk mengubah flex-direction:

Overlay Flexbox DevTools yang melapisi label dan tombol vertikal.

<label for="light-switch" class="gui-switch" style="flex-direction: column">
  Default
  <input type="checkbox" role="switch" id="light-switch">
</label>

Lacak

Input kotak centang diberi gaya sebagai jalur tombol geser dengan menghapus appearance: checkbox normalnya dan memberikan ukurannya sendiri:

Overlay DevTools petak di atas jalur tombol, yang menampilkan area jalur petak bernama dengan nama &#39;track&#39;.

.gui-switch > input {
  appearance: none;

  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  padding: var(--track-padding);

  flex-shrink: 0;
  display: grid;
  align-items: center;
  grid: [track] 1fr / [track] 1fr;
}

Jalur ini juga membuat area jalur petak sel tunggal satu per satu untuk diklaim ibu jari.

Kalimba

Gaya appearance: none juga menghapus tanda centang visual yang disediakan oleh browser. Komponen ini menggunakan pseudo-element dan :checked pseudo-class pada input untuk menggantikan indikator visual ini.

Thumb adalah turunan pseudo-elemen yang dilampirkan ke input[type="checkbox"] dan ditumpuk di atas jalur, bukan di bawahnya, dengan mengklaim area petak track:

DevTools yang menampilkan ibu jari elemen semu yang diposisikan di dalam petak CSS.

.gui-switch > input::before {
  content: "";
  grid-area: track;
  inline-size: var(--thumb-size);
  block-size: var(--thumb-size);
}

Gaya

Properti kustom memungkinkan komponen tombol yang serbaguna dan beradaptasi dengan skema warna, bahasa kanan-ke-kiri, dan preferensi gerakan.

Perbandingan berdampingan antara tema terang dan gelap untuk tombol dan statusnya.

Gaya interaksi sentuh

Di perangkat seluler, browser menambahkan fitur pemilihan teks dan sorotan ketuk ke label dan input. Hal ini berdampak negatif pada gaya dan masukan interaksi visual yang diperlukan oleh peralihan ini. Dengan beberapa baris CSS, saya dapat menghapus efek tersebut dan menambahkan gaya cursor: pointer saya sendiri:

.gui-switch {
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

Tidak selalu disarankan untuk menghapus gaya tersebut, karena dapat memberikan masukan interaksi visual yang berharga. Pastikan untuk memberikan alternatif kustom jika Anda menghapusnya.

Lacak

Sebagian besar gaya elemen ini berkaitan dengan bentuk dan warnanya, yang diakses dari .gui-switch induk melalui cascade.

Mengubah varian dengan ukuran dan warna jalur kustom.

.gui-switch > input {
  appearance: none;
  border: none;
  outline-offset: 5px;
  box-sizing: content-box;

  padding: var(--track-padding);
  background: var(--track-color-inactive);
  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  border-radius: var(--track-size);
}

Berbagai opsi penyesuaian untuk jalur tombol berasal dari empat properti kustom. border: none ditambahkan karena appearance: none tidak menghapus batas dari kotak centang di semua browser.

Kalimba

Elemen thumb sudah berada di sebelah kanan track, tetapi memerlukan gaya lingkaran:

.gui-switch > input::before {
  background: var(--thumb-color);
  border-radius: 50%;
}

DevTools ditampilkan dengan menandai elemen semu ibu jari lingkaran.

Interaksi

Gunakan properti kustom untuk mempersiapkan interaksi yang akan menampilkan sorotan saat mengarahkan kursor dan perubahan posisi ibu jari. Preferensi pengguna juga diperiksa sebelum melakukan transisi gaya sorotan gerakan atau melayang.

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

Posisi ibu jari

Properti kustom menyediakan mekanisme sumber tunggal untuk memosisikan ibu jari di jalur. Kita memiliki ukuran jalur dan gambar mini yang akan kita gunakan dalam perhitungan untuk menjaga offset gambar mini dengan benar dan berada di dalam jalur: 0% dan 100%.

Elemen input memiliki variabel posisi --thumb-position, dan elemen pseudo thumb menggunakannya sebagai posisi translateX:

.gui-switch > input {
  --thumb-position: 0%;
}

.gui-switch > input::before {
  transform: translateX(var(--thumb-position));
}

Kita sekarang dapat mengubah --thumb-position dari CSS dan pseudo-class yang disediakan pada elemen kotak centang. Karena kita menetapkan transition: transform var(--thumb-transition-duration) ease secara bersyarat sebelumnya pada elemen ini, perubahan ini dapat dianimasikan saat diubah:

/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
}

/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
}

Saya rasa orkestrasi yang tidak terikat ini berjalan dengan baik. Elemen thumb hanya berkaitan dengan satu gaya, yaitu posisi translateX. Input dapat mengelola semua kompleksitas dan perhitungan.

Vertical

Dukungan dilakukan dengan class pengubah -vertical yang menambahkan rotasi dengan transformasi CSS ke elemen input.

Elemen yang diputar 3D tidak mengubah tinggi keseluruhan komponen, yang dapat mengganggu tata letak blok. Perhitungkan hal ini menggunakan variabel --track-size dan --track-padding. Hitung jumlah minimum ruang yang diperlukan agar tombol vertikal dapat mengalir dalam tata letak seperti yang diharapkan:

.gui-switch.-vertical {
  min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));

  & > input {
    transform: rotate(-90deg);
  }
}

(RTL) kanan-ke-kiri

Seorang teman CSS, Elad Schecter, dan saya membuat prototipe bersama-sama menu samping geser keluar menggunakan transformasi CSS yang menangani bahasa kanan ke kiri dengan membalikkan satu variabel. Kami melakukan hal ini karena tidak ada transformasi properti logis di CSS, dan mungkin tidak akan pernah ada. Elad memiliki ide bagus untuk menggunakan nilai properti kustom untuk membalikkan persentase, sehingga memungkinkan pengelolaan satu lokasi untuk logika kustom kita sendiri untuk transformasi logis. Saya menggunakan teknik yang sama dalam peralihan ini dan menurut saya hasilnya sangat bagus:

.gui-switch {
  --isLTR: 1;

  &:dir(rtl) {
    --isLTR: -1;
  }
}

Properti kustom yang disebut --isLTR awalnya memiliki nilai 1, yang berarti true karena tata letak kita adalah kiri-ke-kanan secara default. Kemudian, menggunakan class semu CSS :dir(), nilai ditetapkan ke -1 saat komponen berada dalam tata letak kanan-ke-kiri.

Terapkan --isLTR dengan menggunakannya dalam calc() di dalam transformasi:

.gui-switch.-vertical > input {
  transform: rotate(-90deg);
  transform: rotate(calc(90deg * var(--isLTR) * -1));
}

Sekarang rotasi tombol vertikal memperhitungkan posisi sisi berlawanan yang diperlukan oleh tata letak kanan-ke-kiri.

Transformasi translateX pada elemen semu thumb juga perlu diperbarui untuk memperhitungkan persyaratan sisi berlawanan:

.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
  --thumb-position: calc(
   ((var(--track-size) / 2) - (var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

Meskipun pendekatan ini tidak akan berfungsi untuk memenuhi semua kebutuhan terkait konsep seperti transformasi CSS logis, pendekatan ini menawarkan beberapa prinsip DRY untuk banyak kasus penggunaan.

Negara bagian

Penggunaan input[type="checkbox"] bawaan tidak akan lengkap tanpa menangani berbagai statusnya: :checked, :disabled, :indeterminate, dan :hover. :focus sengaja dibiarkan, dengan penyesuaian hanya dilakukan pada offset-nya; ring fokus terlihat bagus di Firefox dan Safari:

Screenshot ring fokus yang berfokus pada tombol di Firefox dan Safari.

Dicentang

<label for="switch-checked" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>

Status ini merepresentasikan status on. Dalam status ini, latar belakang "jalur" input ditetapkan ke warna aktif dan posisi ibu jari ditetapkan ke "akhir".

.gui-switch > input:checked {
  background: var(--track-color-active);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

Nonaktif

<label for="switch-disabled" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>

Tombol :disabled tidak hanya terlihat berbeda secara visual, tetapi juga harus membuat elemen tidak dapat diubah.Imutabilitas interaksi bebas dari browser, tetapi status visual memerlukan gaya karena penggunaan appearance: none.

.gui-switch > input:disabled {
  cursor: not-allowed;
  --thumb-color: transparent;

  &::before {
    cursor: not-allowed;
    box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);

    @media (prefers-color-scheme: dark) { & {
      box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
    }}
  }
}

Tombol gaya gelap dalam keadaan dinonaktifkan, dicentang, dan tidak dicentang.

Status ini rumit karena memerlukan tema gelap dan terang dengan status dinonaktifkan dan dicentang. Saya memilih gaya minimalis untuk status ini secara stilistika untuk mengurangi beban pemeliharaan kombinasi gaya.

Tidak dapat ditentukan

Status yang sering dilupakan adalah :indeterminate, saat kotak centang tidak dicentang atau tidak dicentang. Ini adalah status yang menyenangkan, mengundang, dan tidak mencolok. Pengingat yang baik bahwa status boolean dapat memiliki status di antaranya yang tidak terduga.

Menyetel kotak centang ke tidak ditentukan cukup rumit, hanya JavaScript yang dapat menyetelnya:

<label for="switch-indeterminate" class="gui-switch">
  Indeterminate
  <input type="checkbox" role="switch" id="switch-indeterminate">
  <script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>

Status tidak ditentukan yang memiliki ibu jari jalur di tengah, untuk menunjukkan belum diputuskan.

Karena statusnya bagi saya tidak mencolok dan menarik, saya merasa tepat untuk menempatkan posisi ibu jari tombol di tengah:

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

Arahkan kursor

Interaksi saat kursor diarahkan harus memberikan dukungan visual untuk UI yang terhubung dan juga memberikan petunjuk ke arah UI interaktif. Tombol ini menandai ibu jari dengan cincin semi-transparan saat label atau input diarahkan kursor. Animasi melayang ini kemudian memberikan arah menuju elemen thumbnail interaktif.

Efek "sorotan" dilakukan dengan box-shadow. Saat kursor diarahkan ke input yang tidak dinonaktifkan, perbesar ukuran --highlight-size. Jika pengguna menyetujui gerakan, kita akan melakukan transisi box-shadow dan melihatnya bertambah, jika pengguna tidak menyetujui gerakan, sorotan akan muncul secara instan:

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

.gui-switch > input:not(:disabled):hover::before {
  --highlight-size: .5rem;
}

JavaScript

Bagi saya, antarmuka tombol dapat terasa aneh dalam upaya meniru antarmuka fisik, terutama yang memiliki lingkaran di dalam jalur. iOS berhasil melakukannya dengan tombolnya, Anda dapat menariknya dari sisi ke sisi, dan opsi ini sangat memuaskan. Sebaliknya, elemen UI dapat terasa tidak aktif jika gestur tarik dicoba dan tidak ada yang terjadi.

Thumbnail yang dapat ditarik

Pseudo-elemen thumb menerima posisinya dari .gui-switch > input yang tercakup var(--thumb-position), JavaScript dapat memberikan nilai gaya inline pada input untuk memperbarui posisi thumb secara dinamis sehingga tampak mengikuti gestur penunjuk. Saat pointer dilepaskan, hapus gaya inline dan tentukan apakah penarikan lebih dekat ke nonaktif atau aktif dengan menggunakan properti kustom --thumb-position. Ini adalah tulang punggung solusi; peristiwa penunjuk melacak posisi penunjuk secara kondisional untuk mengubah properti kustom CSS.

Karena komponen sudah 100% berfungsi sebelum skrip ini muncul, diperlukan banyak upaya untuk mempertahankan perilaku yang ada, seperti mengklik label untuk mengganti input. JavaScript kami tidak boleh menambahkan fitur dengan mengorbankan fitur yang sudah ada.

touch-action

Menyeret adalah gestur, gestur kustom, yang menjadikannya kandidat yang tepat untuk manfaat touch-action. Dalam kasus tombol ini, gestur horizontal harus ditangani oleh skrip kita, atau gestur vertikal diambil untuk varian tombol vertikal. Dengan touch-action, kita dapat memberi tahu browser gestur apa yang harus ditangani pada elemen ini, sehingga skrip dapat menangani gestur tanpa persaingan.

CSS berikut menginstruksikan browser bahwa saat gestur penunjuk dimulai dari dalam jalur peralihan ini, tangani gestur vertikal, jangan lakukan apa pun dengan gestur horizontal:

.gui-switch > input {
  touch-action: pan-y;
}

Hasil yang diinginkan adalah gestur horizontal yang tidak juga menggeser atau men-scroll halaman. Pointer dapat men-scroll secara vertikal mulai dari dalam input dan men-scroll halaman, tetapi pointer horizontal ditangani secara kustom.

Utilitas gaya nilai piksel

Saat penyiapan dan selama penarikan, berbagai nilai angka terkomputasi perlu diambil dari elemen. Fungsi JavaScript berikut menampilkan nilai piksel yang dihitung untuk properti CSS tertentu. Variabel ini digunakan dalam skrip penyiapan seperti ini getStyle(checkbox, 'padding-left').

​​const getStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}

const getPseudoStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}

export {
  getStyle,
  getPseudoStyle,
}

Perhatikan bagaimana window.getComputedStyle() menerima argumen kedua, yaitu elemen semu target. JavaScript dapat membaca begitu banyak nilai dari elemen, bahkan dari elemen semu.

dragging

Ini adalah momen inti untuk logika penarikan dan ada beberapa hal yang perlu diperhatikan dari pengendali peristiwa fungsi:

const dragging = event => {
  if (!state.activethumb) return

  let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
  let directionality = getStyle(state.activethumb, '--isLTR')

  let track = (directionality === -1)
    ? (state.activethumb.clientWidth * -1) + thumbsize + padding
    : 0

  let pos = Math.round(event.offsetX - thumbsize / 2)

  if (pos < bounds.lower) pos = 0
  if (pos > bounds.upper) pos = bounds.upper

  state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}

Hero skrip adalah state.activethumb, lingkaran kecil yang diposisikan skrip ini bersama dengan penunjuk. Objek switches adalah Map() dengan kuncinya adalah .gui-switch dan nilainya adalah batas dan ukuran yang di-cache yang membuat skrip tetap efisien. Kanan-ke-kiri ditangani menggunakan properti kustom yang sama yang digunakan CSS untuk --isLTR, dan dapat menggunakannya untuk membalikkan logika dan terus mendukung RTL. event.offsetX juga berharga, karena berisi nilai delta yang berguna untuk memosisikan ibu jari.

state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)

Baris CSS terakhir ini menetapkan properti kustom yang digunakan oleh elemen thumb. Penetapan nilai ini akan bertransisi dari waktu ke waktu, tetapi peristiwa pointer sebelumnya telah menetapkan --thumb-transition-duration ke 0s untuk sementara, sehingga menghilangkan interaksi yang lambat.

dragEnd

Agar pengguna dapat menarik jauh di luar tombol dan melepaskannya, peristiwa jendela global perlu didaftarkan:

window.addEventListener('pointerup', event => {
  if (!state.activethumb) return

  dragEnd(event)
})

Menurut saya, sangat penting bagi pengguna untuk memiliki kebebasan menarik secara longgar dan antarmuka cukup pintar untuk memperhitungkannya. Tidak perlu banyak upaya untuk menanganinya dengan peralihan ini, tetapi perlu dipertimbangkan dengan cermat selama proses pengembangan.

const dragEnd = event => {
  if (!state.activethumb) return

  state.activethumb.checked = determineChecked()

  if (state.activethumb.indeterminate)
    state.activethumb.indeterminate = false

  state.activethumb.style.removeProperty('--thumb-transition-duration')
  state.activethumb.style.removeProperty('--thumb-position')
  state.activethumb.removeEventListener('pointermove', dragging)
  state.activethumb = null

  padRelease()
}

Interaksi dengan elemen telah selesai, saatnya menetapkan properti input yang dicentang dan menghapus semua peristiwa gestur. Kotak centang diubah dengan state.activethumb.checked = determineChecked().

determineChecked()

Fungsi ini, yang dipanggil oleh dragEnd, menentukan posisi ibu jari saat ini dalam batas jalurnya dan menampilkan nilai benar jika sama dengan atau lebih dari setengah panjang jalur:

const determineChecked = () => {
  let {bounds} = switches.get(state.activethumb.parentElement)

  let curpos =
    Math.abs(
      parseInt(
        state.activethumb.style.getPropertyValue('--thumb-position')))

  if (!curpos) {
    curpos = state.activethumb.checked
      ? bounds.lower
      : bounds.upper
  }

  return curpos >= bounds.middle
}

Pikiran tambahan

Gestur tarik menimbulkan sedikit utang kode karena struktur HTML awal yang dipilih, terutama membungkus input dalam label. Label, yang merupakan elemen induk, akan menerima interaksi klik setelah input. Di akhir peristiwa dragEnd, Anda mungkin melihat padRelease() sebagai fungsi yang terdengar aneh.

const padRelease = () => {
  state.recentlyDragged = true

  setTimeout(_ => {
    state.recentlyDragged = false
  }, 300)
}

Hal ini untuk memperhitungkan label yang mendapatkan klik berikutnya ini, karena akan membatalkan pilihan, atau memilih, interaksi yang dilakukan pengguna.

Jika saya melakukannya lagi, saya mungkin mempertimbangkan untuk menyesuaikan DOM dengan JavaScript selama upgrade UX, untuk membuat elemen yang menangani klik label itu sendiri dan tidak bertentangan dengan perilaku bawaan.

Jenis JavaScript ini adalah yang paling tidak saya sukai untuk ditulis, saya tidak ingin mengelola bubbling peristiwa kondisional:

const preventBubbles = event => {
  if (state.recentlyDragged)
    event.preventDefault() && event.stopPropagation()
}

Kesimpulan

Komponen tombol kecil ini ternyata menjadi yang paling sulit dari semua Tantangan GUI sejauh ini. Sekarang setelah Anda tahu cara saya melakukannya, bagaimana Anda‽ 🙂

Mari kita diversifikasi pendekatan kita dan pelajari semua cara untuk membangun di web. Buat demo, tweet linknya kepada saya, dan saya akan menambahkannya ke bagian remix komunitas di bawah.

Remix komunitas

Resource

Temukan .gui-switch kode sumber di GitHub.