ThemeSwitch.vue 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { useVModel } from '@vueuse/core'
  4. import { computed } from 'vue'
  5. import stopEvent from '#shared/utils/events.ts'
  6. export interface Props {
  7. modelValue?: string
  8. size?: 'medium' | 'small'
  9. }
  10. const props = withDefaults(defineProps<Props>(), {
  11. size: 'medium',
  12. })
  13. const emit = defineEmits<{
  14. 'update:modelValue': [value: string]
  15. }>()
  16. const localValue = useVModel(props, 'modelValue', emit)
  17. const isLight = computed(() => localValue.value === 'light')
  18. const isDark = computed(() => localValue.value === 'dark')
  19. const nextValue = () => {
  20. if (isLight.value) return 'auto'
  21. if (isDark.value) return 'light'
  22. return 'dark'
  23. }
  24. const cycleValue = () => {
  25. localValue.value = nextValue()
  26. }
  27. defineExpose({ cycleValue })
  28. const updateLocalValue = (e: Event) => {
  29. stopEvent(e)
  30. cycleValue()
  31. }
  32. const isSmall = computed(() => props.size === 'small')
  33. const trackSizeClasses = computed(() => {
  34. if (isSmall.value) return 'w-11 h-[19px]'
  35. return 'w-14 h-6'
  36. })
  37. const knobSizeClasses = computed(() => {
  38. if (isSmall.value) return 'w-[17px] h-[17px]'
  39. return 'w-[22px] h-[22px]'
  40. })
  41. const knobTranslateClasses = computed(() => {
  42. if (isLight.value) return 'ltr:translate-x-px rtl:-translate-x-px'
  43. if (isDark.value) {
  44. if (isSmall.value) return 'ltr:translate-x-[26px] rtl:-translate-x-[26px]'
  45. return 'ltr:translate-x-[33px] rtl:-translate-x-[33px]'
  46. }
  47. if (isSmall.value) return 'ltr:translate-x-[14px] rtl:-translate-x-[14px]'
  48. return 'ltr:translate-x-[17px] rtl:-translate-x-[17px]'
  49. })
  50. const icon = computed(() => {
  51. if (isLight.value) return 'sun'
  52. if (isDark.value) return 'moon-stars'
  53. return 'magic'
  54. })
  55. const ariaChecked = computed(() => {
  56. if (isLight.value) return 'false'
  57. if (isDark.value) return 'true'
  58. return 'mixed'
  59. })
  60. </script>
  61. <template>
  62. <button
  63. type="button"
  64. role="checkbox"
  65. class="-:bg-stone-200 dark:-:bg-gray-500 hover:-:outline-blue-600 dark:hover:-:outline-blue-900 focus:-:outline-blue-800 hover:focus:-:outline-blue-800 dark:hover:focus:-:outline-blue-800 relative inline-flex flex-shrink-0 cursor-pointer items-center rounded-full ring-1 ring-neutral-100 transition-colors duration-200 ease-in-out hover:outline hover:outline-1 hover:outline-offset-2 focus:outline focus:outline-1 focus:outline-offset-2 dark:ring-gray-900"
  66. :class="[
  67. trackSizeClasses,
  68. {
  69. 'bg-white dark:bg-white': isLight,
  70. 'bg-blue-800 dark:bg-blue-800': isDark,
  71. },
  72. ]"
  73. :aria-label="$t('Dark Mode')"
  74. :aria-checked="ariaChecked"
  75. tabindex="0"
  76. @click="updateLocalValue"
  77. @keydown.space="updateLocalValue"
  78. >
  79. <div
  80. class="-:bg-white -:text-black pointer-events-none flex transform items-center justify-center rounded-full transition duration-200 ease-in-out"
  81. :class="[
  82. knobSizeClasses,
  83. knobTranslateClasses,
  84. {
  85. 'bg-blue-800 text-white': isLight,
  86. },
  87. ]"
  88. >
  89. <CommonIcon :name="icon" size="xs" />
  90. </div>
  91. </button>
  92. </template>