theme-button.vue 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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)]"
  79. aria-live="polite"
  80. class="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. fill="hsl(var(--foreground)/80%)"
  89. stroke="none"
  90. >
  91. <rect fill="white" height="100%" width="100%" x="0" y="0" />
  92. <circle cx="40" cy="8" fill="black" r="11" />
  93. </mask>
  94. <circle
  95. id="sun"
  96. :class="e('sun')"
  97. cx="12"
  98. cy="12"
  99. mask="url(#theme-toggle-moon)"
  100. r="11"
  101. />
  102. <g :class="e('sun-beams')">
  103. <line x1="12" x2="12" y1="1" y2="3" />
  104. <line x1="12" x2="12" y1="21" y2="23" />
  105. <line x1="4.22" x2="5.64" y1="4.22" y2="5.64" />
  106. <line x1="18.36" x2="19.78" y1="18.36" y2="19.78" />
  107. <line x1="1" x2="3" y1="12" y2="12" />
  108. <line x1="21" x2="23" y1="12" y2="12" />
  109. <line x1="4.22" x2="5.64" y1="19.78" y2="18.36" />
  110. <line x1="18.36" x2="19.78" y1="5.64" y2="4.22" />
  111. </g>
  112. </svg>
  113. </VbenButton>
  114. </template>
  115. <style lang="scss" scoped>
  116. @import '@vben-core/design/global';
  117. @include b('theme-toggle') {
  118. @include e('moon') {
  119. & > circle {
  120. transition: transform 0.5s cubic-bezier(0, 0, 0.3, 1);
  121. }
  122. }
  123. @include e('sun') {
  124. fill: hsl(var(--foreground) / 80%);
  125. stroke: none;
  126. transition: transform 1.6s cubic-bezier(0.25, 0, 0.2, 1);
  127. transform-origin: center center;
  128. &:hover > svg > & {
  129. fill: hsl(var(--foreground));
  130. }
  131. }
  132. @include e('sun-beams') {
  133. stroke: hsl(var(--foreground) / 80%);
  134. stroke-width: 2px;
  135. transition:
  136. transform 1.6s cubic-bezier(0.5, 1.5, 0.75, 1.25),
  137. opacity 0.6s cubic-bezier(0.25, 0, 0.3, 1);
  138. transform-origin: center center;
  139. &:hover > svg > & {
  140. stroke: hsl(var(--foreground));
  141. }
  142. }
  143. @include is('light') {
  144. @include b('theme-toggle') {
  145. @include e('sun') {
  146. transform: scale(0.5);
  147. }
  148. @include e('sun-beams') {
  149. transform: rotateZ(0.25turn);
  150. }
  151. }
  152. }
  153. @include is('dark') {
  154. @include b('theme-toggle') {
  155. @include e('moon') {
  156. & > circle {
  157. transform: translateX(-20px);
  158. }
  159. }
  160. @include e('sun-beams') {
  161. opacity: 0;
  162. }
  163. }
  164. }
  165. &:hover > svg {
  166. @include b('theme-toggle') {
  167. &__moon,
  168. &__sun {
  169. fill: hsl(var(--foreground));
  170. }
  171. }
  172. }
  173. }
  174. </style>