theme-button.vue 4.2 KB

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