theme-button.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <script lang="ts" setup>
  2. import { computed, nextTick } from 'vue';
  3. import { VbenButton } from '@vben-core/shadcn-ui';
  4. import { useNamespace } from '@vben-core/toolkit';
  5. interface Props {
  6. /**
  7. * 类型
  8. */
  9. type?: 'icon' | 'normal';
  10. }
  11. defineOptions({
  12. name: 'ThemeToggleButton',
  13. });
  14. const props = withDefaults(defineProps<Props>(), {
  15. type: 'normal',
  16. });
  17. const isDark = defineModel<boolean>();
  18. const { b, e, is } = useNamespace('theme-toggle');
  19. const theme = computed(() => {
  20. return isDark.value ? 'light' : 'dark';
  21. });
  22. const bindProps = computed(() => {
  23. const type = props.type;
  24. return type === 'normal'
  25. ? {
  26. variant: 'heavy' as const,
  27. }
  28. : {
  29. class: 'rounded-full',
  30. size: 'icon' as const,
  31. style: { padding: '6px' },
  32. variant: 'icon' as const,
  33. };
  34. });
  35. function toggleTheme(event: MouseEvent) {
  36. const isAppearanceTransition =
  37. // @ts-expect-error
  38. document.startViewTransition &&
  39. !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  40. if (!isAppearanceTransition || !event) {
  41. isDark.value = !isDark.value;
  42. return;
  43. }
  44. const x = event.clientX;
  45. const y = event.clientY;
  46. const endRadius = Math.hypot(
  47. Math.max(x, innerWidth - x),
  48. Math.max(y, innerHeight - y),
  49. );
  50. // @ts-expect-error: Transition API
  51. const transition = document.startViewTransition(async () => {
  52. isDark.value = !isDark.value;
  53. await nextTick();
  54. });
  55. transition.ready.then(() => {
  56. const clipPath = [
  57. `circle(0px at ${x}px ${y}px)`,
  58. `circle(${endRadius}px at ${x}px ${y}px)`,
  59. ];
  60. document.documentElement.animate(
  61. {
  62. clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
  63. },
  64. {
  65. duration: 450,
  66. easing: 'ease-in',
  67. pseudoElement: isDark.value
  68. ? '::view-transition-old(root)'
  69. : '::view-transition-new(root)',
  70. },
  71. );
  72. });
  73. }
  74. </script>
  75. <template>
  76. <VbenButton
  77. :aria-label="theme"
  78. :class="[b(), is(theme), `is-${theme}`]"
  79. aria-live="polite"
  80. class="theme-toggle cursor-pointer border-none bg-none"
  81. v-bind="bindProps"
  82. @click.stop="toggleTheme"
  83. >
  84. <svg aria-hidden="true" height="24" viewBox="0 0 24 24" width="24">
  85. <mask
  86. id="theme-toggle-moon"
  87. :class="e('moon')"
  88. class="theme-toggle__moon"
  89. fill="hsl(var(--foreground)/80%)"
  90. stroke="none"
  91. >
  92. <rect fill="white" height="100%" width="100%" x="0" y="0" />
  93. <circle cx="40" cy="8" fill="black" r="11" />
  94. </mask>
  95. <circle
  96. id="sun"
  97. :class="e('sun')"
  98. class="theme-toggle__sun"
  99. cx="12"
  100. cy="12"
  101. mask="url(#theme-toggle-moon)"
  102. r="11"
  103. />
  104. <g :class="e('sun-beams')" class="theme-toggle__sun-beams">
  105. <line x1="12" x2="12" y1="1" y2="3" />
  106. <line x1="12" x2="12" y1="21" y2="23" />
  107. <line x1="4.22" x2="5.64" y1="4.22" y2="5.64" />
  108. <line x1="18.36" x2="19.78" y1="18.36" y2="19.78" />
  109. <line x1="1" x2="3" y1="12" y2="12" />
  110. <line x1="21" x2="23" y1="12" y2="12" />
  111. <line x1="4.22" x2="5.64" y1="19.78" y2="18.36" />
  112. <line x1="18.36" x2="19.78" y1="5.64" y2="4.22" />
  113. </g>
  114. </svg>
  115. </VbenButton>
  116. </template>
  117. <style lang="scss" scoped>
  118. .theme-toggle {
  119. &__moon {
  120. & > circle {
  121. transition: transform 0.5s cubic-bezier(0, 0, 0.3, 1);
  122. }
  123. }
  124. &__sun {
  125. fill: hsl(var(--foreground) / 80%);
  126. stroke: none;
  127. transition: transform 1.6s cubic-bezier(0.25, 0, 0.2, 1);
  128. transform-origin: center center;
  129. &:hover > svg > & {
  130. fill: hsl(var(--foreground));
  131. }
  132. }
  133. &__sun-beams {
  134. stroke: hsl(var(--foreground) / 80%);
  135. stroke-width: 2px;
  136. transition:
  137. transform 1.6s cubic-bezier(0.5, 1.5, 0.75, 1.25),
  138. opacity 0.6s cubic-bezier(0.25, 0, 0.3, 1);
  139. transform-origin: center center;
  140. &:hover > svg > & {
  141. stroke: hsl(var(--foreground));
  142. }
  143. }
  144. &.is-light {
  145. .theme-toggle__sun {
  146. transform: scale(0.5);
  147. }
  148. .theme-toggle__sun-beams {
  149. transform: rotateZ(0.25turn);
  150. }
  151. }
  152. &.is-dark {
  153. .theme-toggle__moon {
  154. & > circle {
  155. transform: translateX(-20px);
  156. }
  157. }
  158. .theme-toggle__sun-beams {
  159. opacity: 0;
  160. }
  161. }
  162. &:hover > svg {
  163. .theme-toggle__sun,
  164. .theme-toggle__moon {
  165. fill: hsl(var(--foreground));
  166. }
  167. }
  168. }
  169. </style>