theme-button.vue 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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 - startViewTransition is not available in the current DOM lib target
  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. const transition = document.startViewTransition(async () => {
  49. isDark.value = !isDark.value;
  50. await nextTick();
  51. });
  52. transition.ready.then(() => {
  53. const clipPath = [
  54. `circle(0px at ${x}px ${y}px)`,
  55. `circle(${endRadius}px at ${x}px ${y}px)`,
  56. ];
  57. const animate = document.documentElement.animate(
  58. {
  59. clipPath: isDark.value ? [...clipPath].toReversed() : clipPath,
  60. },
  61. {
  62. duration: 450,
  63. easing: 'ease-in',
  64. pseudoElement: isDark.value
  65. ? '::view-transition-old(root)'
  66. : '::view-transition-new(root)',
  67. },
  68. );
  69. animate.onfinish = () => {
  70. transition.skipTransition();
  71. };
  72. });
  73. }
  74. </script>
  75. <template>
  76. <VbenButton
  77. :aria-label="theme"
  78. :class="[`is-${theme}`]"
  79. aria-live="polite"
  80. class="theme-toggle cursor-pointer border-none bg-none hover:animate-[shrink_0.3s_ease-in-out]"
  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 id="theme-toggle-moon" class="theme-toggle__moon">
  86. <rect fill="white" height="100%" width="100%" x="0" y="0" />
  87. <circle cx="40" cy="8" fill="black" r="11" />
  88. </mask>
  89. <circle
  90. id="sun"
  91. class="theme-toggle__sun fill-foreground/90"
  92. cx="12"
  93. cy="12"
  94. mask="url(#theme-toggle-moon)"
  95. r="11"
  96. />
  97. <g class="theme-toggle__sun-beams stroke-foreground/90 stroke-2">
  98. <line x1="12" x2="12" y1="1" y2="3" />
  99. <line x1="12" x2="12" y1="21" y2="23" />
  100. <line x1="4.22" x2="5.64" y1="4.22" y2="5.64" />
  101. <line x1="18.36" x2="19.78" y1="18.36" y2="19.78" />
  102. <line x1="1" x2="3" y1="12" y2="12" />
  103. <line x1="21" x2="23" y1="12" y2="12" />
  104. <line x1="4.22" x2="5.64" y1="19.78" y2="18.36" />
  105. <line x1="18.36" x2="19.78" y1="5.64" y2="4.22" />
  106. </g>
  107. </svg>
  108. </VbenButton>
  109. </template>
  110. <style scoped>
  111. @reference "@vben-core/design/theme";
  112. .theme-toggle__moon > circle {
  113. transition: transform 0.5s cubic-bezier(0, 0, 0.3, 1);
  114. }
  115. .theme-toggle__sun {
  116. stroke: none;
  117. transform-origin: center center;
  118. transition: transform 1.6s cubic-bezier(0.25, 0, 0.2, 1);
  119. }
  120. .theme-toggle__sun-beams {
  121. transform-origin: center center;
  122. transition:
  123. transform 1.6s cubic-bezier(0.5, 1.5, 0.75, 1.25),
  124. opacity 0.6s cubic-bezier(0.25, 0, 0.3, 1);
  125. }
  126. .theme-toggle.is-light .theme-toggle__sun {
  127. @apply scale-50;
  128. }
  129. .theme-toggle.is-light .theme-toggle__sun-beams {
  130. transform: rotateZ(0.25turn);
  131. }
  132. .theme-toggle.is-dark .theme-toggle__moon > circle {
  133. transform: translateX(-20px);
  134. }
  135. .theme-toggle.is-dark .theme-toggle__sun-beams {
  136. @apply opacity-0;
  137. }
  138. .theme-toggle:hover > svg .theme-toggle__sun {
  139. fill: hsl(var(--foreground));
  140. }
  141. .theme-toggle:hover > svg .theme-toggle__sun-beams {
  142. stroke: hsl(var(--foreground));
  143. }
  144. </style>