Membuat komponen {i>switch

Ringkasan dasar tentang cara mem-build komponen tombol aksesibel dan responsif.

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

Demo

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

Ringkasan

Fungsi switch mirip dengan kotak centang, tetapi secara eksplisit mewakili status aktif dan nonaktif boolean.

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.

Properti kustom

Variabel berikut mewakili berbagai bagian tombol dan opsi-opsinya. Sebagai class tingkat atas, .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 pengguna preferensi gerakan yang dikurangi dapat dimasukkan ke dalam properti kustom dengan plugin PostCSS berdasarkan spesifikasi draf dalam Kueri Media 5 ini:

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

Markup

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

Label dan kotak centang alami tanpa gaya.

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

<input type="checkbox"> sudah di-build sebelumnya dengan API dan status. Browser mengelola properti checked dan peristiwa input seperti oninputdan onchanged.

Tata letak

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

.gui-switch

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

DevTools Flexbox menempatkan label dan tombol horizontal, yang menampilkan distribusi
ruang tata letak.

.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:

DevTools Flexbox yang menempatkan 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 ditata gayanya sebagai jalur tombol dengan menghapus appearance: checkbox normal dan menyediakan ukurannya sendiri:

Grid DevTools menempatkan overlay pada jalur tombol, yang menampilkan area jalur grid
yang diberi nama 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 juga membuat area jalur petak sel tunggal satu per satu untuk ibu jari yang akan diklaim.

Kalimba

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

thumb adalah turunan elemen pseudo yang dilampirkan ke input[type="checkbox"] dan tumpukan di atas trek, bukan di bawahnya dengan mengklaim area petak track:

DevTools menampilkan thumb pseudo-elemen seperti 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 serbaguna yang beradaptasi dengan skema warna, bahasa kanan-ke-kiri, dan preferensi gerakan.

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

Gaya interaksi sentuh

Di perangkat seluler, browser menambahkan sorotan ketuk dan fitur pemilihan teks ke label dan input. Hal ini berdampak negatif terhadap masukan gaya dan 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 gaya tersebut dapat menjadi umpan balik interaksi visual yang berharga. Pastikan untuk memberikan alternatif kustom jika Anda menghapusnya.

Lacak

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

Varian tombol 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 track kanan, tetapi memerlukan gaya lingkaran:

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

DevTools ditampilkan yang menyoroti pseudo-elemen thumbnail lingkaran.

Interaksi

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

.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 jempol

Properti kustom menyediakan mekanisme sumber tunggal untuk memosisikan thumb di jalur. Kita memiliki ukuran trek dan thumb yang akan digunakan dalam penghitungan untuk menjaga thumb tetap diimbangi dengan benar dan di antara dalam trek: 0% dan 100%.

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

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

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

Sekarang kita bebas mengubah --thumb-position dari CSS dan pseudo-class yang disediakan di elemen kotak centang. Karena kita menetapkan transition: transform var(--thumb-transition-duration) ease secara kondisional 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 dipisahkan ini berhasil dengan baik. Elemen thumb hanya terkait dengan satu gaya, posisi translateX. Input dapat mengelola semua kompleksitas dan penghitungan.

Vertikal

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

Namun, elemen yang diputar 3D tidak mengubah tinggi komponen secara keseluruhan, yang dapat mengacaukan tata letak blok. Perhitungkan hal ini menggunakan variabel --track-size dan --track-padding. Hitung jumlah minimum ruang yang diperlukan agar tombol vertikal 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 menu samping geser menggunakan transformasi CSS yang menangani bahasa kanan ke kiri dengan membalik satu variabel. Kami melakukannya karena tidak ada transformasi properti logis di CSS, dan mungkin tidak akan pernah ada. Elad memiliki ide cemerlang untuk menggunakan nilai properti khusus untuk membalikkan persentase, guna memungkinkan pengelolaan lokasi tunggal dari logika kustom kita sendiri untuk transformasi logis. Saya menggunakan teknik yang sama dalam tombol ini dan saya pikir hasilnya sangat bagus:

.gui-switch {
  --isLTR: 1;

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

Properti kustom yang disebut --isLTR awalnya menyimpan nilai 1, yang berarti true karena tata letak kita adalah dari kiri ke kanan secara default. Kemudian, menggunakan class pseudo 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 yang berlawanan yang diperlukan oleh tata letak kanan-ke-kiri.

Transformasi translateX pada pseudo-elemen thumb juga perlu diperbarui untuk mempertimbangkan persyaratan sisi yang 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 status yang dapat dimilikinya: :checked, :disabled, :indeterminate, dan :hover. :focus sengaja dibiarkan, dengan penyesuaian hanya dilakukan pada offset-nya; cincin fokus terlihat bagus di Firefox dan Safari:

Screenshot cincin 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 mewakili status on. Dalam status ini, latar belakang "jalur" input disetel ke warna aktif dan posisi ibu jari disetel 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.Ketidakmampuan interaksi tidak bergantung pada 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 bergaya gelap dalam status nonaktif, dicentang, dan tidak dicentang.

Status ini rumit karena memerlukan tema gelap dan terang dengan status dinonaktifkan dan dicentang. Saya secara gaya memilih gaya minimal untuk status ini guna meringankan beban pemeliharaan kombinasi gaya.

Tidak tentu

Status yang sering dilupakan adalah :indeterminate, dengan kotak centang tidak dicentang atau tidak dicentang. Kondisi ini menyenangkan, mengundang dan tidak terlalu terang-terangan. Pengingat yang baik bahwa status boolean dapat memiliki status tersembunyi di antara status.

Menyetel kotak centang ke tidak ditentukan cukup sulit, 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 tentu yang memiliki thumb trek di tengah, untuk menunjukkan belum memutuskan.

Karena statusnya, bagi saya, sederhana dan menarik, rasanya 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 arahkan kursor akan memberikan dukungan visual untuk UI yang terhubung dan juga memberikan arah ke UI interaktif. Tombol ini akan menandai ibu jari dengan cincin semi-transparan saat kursor diarahkan ke label atau input. Animasi kursor ini kemudian memberikan arah ke elemen thumb interaktif.

Efek "sorotan" dilakukan dengan box-shadow. Saat mengarahkan kursor ke input yang tidak dinonaktifkan, perbesar ukuran --highlight-size. Jika pengguna tidak keberatan dengan gerakan, kita akan mentransisikan box-shadow dan melihatnya tumbuh, jika mereka tidak keberatan dengan gerakan, sorotan akan langsung muncul:

.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 switch bisa terasa janggal dalam upayanya untuk mengemulasi antarmuka fisik, terutama dengan lingkaran di dalam lintasan. iOS melakukan hal ini dengan {i>switch<i}-nya, Anda dapat menariknya dari sisi ke sisi, dan sangat menyenangkan memiliki opsi tersebut. Sebaliknya, elemen UI dapat terasa tidak aktif jika gestur tarik dicoba dan tidak ada yang terjadi.

Jempol yang dapat ditarik

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

Karena komponen sudah berfungsi 100% sebelum skrip ini muncul, perlu cukup banyak pekerjaan untuk mempertahankan perilaku yang ada, seperti mengklik label untuk mengalihkan input. JavaScript kami tidak boleh menambahkan fitur dengan mengurangi fitur yang sudah ada.

touch-action

Menarik adalah gestur khusus, yang menjadikannya kandidat bagus untuk manfaat touch-action. Dalam kasus tombol ini, gestur horizontal harus ditangani oleh skrip kita, atau gestur vertikal yang 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 pointer dimulai dari dalam jalur tombol ini, menangani gestur vertikal, tidak melakukan apa pun dengan gestur horizontal:

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

Hasil yang diinginkan adalah gestur horizontal yang juga tidak 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 tarik, berbagai nilai angka yang dihitung harus diambil dari elemen. Fungsi JavaScript berikut menampilkan nilai piksel yang dihitung berdasarkan properti CSS. Ini digunakan dalam skrip penyiapan seperti getStyle(checkbox, 'padding-left') ini.

​​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 pseudo target. Cukup menarik bahwa JavaScript dapat membaca begitu banyak nilai dari elemen, bahkan dari elemen pseudo.

dragging

Ini adalah momen inti untuk logika tarik 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 pointer. Objek switches adalah Map() dengan kunci .gui-switch dan nilai adalah batas dan ukuran yang di-cache yang membuat skrip tetap efisien. Kanan ke kiri ditangani menggunakan properti kustom yang sama dengan CSS yaitu --isLTR, dan dapat menggunakannya untuk membalik 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 terakhir CSS 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 menghapus interaksi yang lambat.

dragEnd

Agar pengguna diizinkan untuk 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 dengan longgar dan antarmuka yang cukup cerdas untuk memperhitungkannya. Tidak perlu banyak hal untuk menanganinya dengan tombol ini, tetapi perlu pertimbangan yang 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 centang input dan menghapus semua peristiwa gestur. Kotak centang diubah dengan state.activethumb.checked = determineChecked().

determineChecked()

Fungsi ini, yang dipanggil oleh dragEnd, menentukan letak arus ibu jari dalam batas jalurnya dan menampilkan nilai benar jika sama dengan atau lebih dari setengah jalan di sepanjang 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
}

Masukan tambahan

Gestur tarik menimbulkan sedikit utang kode karena struktur HTML awal yang dipilih, terutama menggabungkan input dalam label. Label, yang merupakan elemen induk, akan menerima interaksi klik setelah input. Di akhir peristiwa dragEnd, Anda mungkin telah 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 ini nanti, karena akan menghapus centang, atau mencentang, interaksi yang dilakukan pengguna.

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

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

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

Kesimpulan

Komponen {i>switch<i} yang sangat kecil ini akhirnya menjadi pekerjaan paling banyak dari semua Tantangan GUI sejauh ini! Sekarang setelah Anda tahu bagaimana saya melakukannya, bagaimana Anda akan 🙂

Mari kita diversifikasi pendekatan kami dan mempelajari semua cara untuk membangun di web. Buat demo, tweet link-nya, dan saya akan menambahkannya ke bagian remix komunitas di bawah.

Remix komunitas

Resource

Temukan .gui-switch kode sumber di GitHub.