refactor: 优化UI组件结构与积分系统

重构部分UI组件脚本结构,统一导入风格,提升可维护性。CardUnlockOverlay解锁条件弹窗改为列表展示,提升可读性。修复QueueNotifications滚动区域高度。ScrollableDialogContent增加最大高度。StarsBackground与ParticlesBg组件代码格式优化。App.vue引入玩家积分定时更新逻辑,NPC成长系统补充间谍探测器修复。
This commit is contained in:
谦君
2025-12-18 03:47:38 +08:00
parent 2e3ac1231f
commit 2ed15c4782
42 changed files with 1342 additions and 749 deletions

View File

@@ -17,8 +17,14 @@
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ requirementsDialogTitle }}</AlertDialogTitle>
<AlertDialogDescription class="whitespace-pre-line">
{{ requirementsDialogMessage }}
<AlertDialogDescription>
<div class="space-y-2">
<div v-for="(req, index) in requirementsDialogItems" :key="index" class="flex items-center gap-2 text-sm">
<Check v-if="req.met" :size="16" class="text-green-500 flex-shrink-0" />
<X v-else :size="16" class="text-red-500 flex-shrink-0" />
<span>{{ req.name }}: Lv {{ req.requiredLevel }} ({{ t('common.current') }}: Lv {{ req.currentLevel }})</span>
</div>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
@@ -35,7 +41,7 @@
import { useI18n } from '@/composables/useI18n'
import { useGameConfig } from '@/composables/useGameConfig'
import { BuildingType, TechnologyType } from '@/types/game'
import { Lock } from 'lucide-vue-next'
import { Lock, Check, X } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import {
AlertDialog,
@@ -61,7 +67,7 @@
// AlertDialog 状态
const requirementsDialogOpen = ref(false)
const requirementsDialogTitle = ref('')
const requirementsDialogMessage = ref('')
const requirementsDialogItems = ref<Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }>>([])
const isUnlocked = computed(() => {
// 如果已经建造过level > 0则认为已解锁不显示遮罩
@@ -70,34 +76,32 @@
return publicLogic.checkRequirements(gameStore.currentPlanet, gameStore.player.technologies, props.requirements)
})
const getRequirementsList = (): string => {
if (!props.requirements || !gameStore.currentPlanet) return ''
const getRequirementsList = (): Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> => {
if (!props.requirements || !gameStore.currentPlanet) return []
const lines: string[] = []
const items: Array<{ name: string; requiredLevel: number; currentLevel: number; met: boolean }> = []
for (const [key, requiredLevel] of Object.entries(props.requirements)) {
// 检查是否为建筑类型
if (Object.values(BuildingType).includes(key as BuildingType)) {
const buildingType = key as BuildingType
const currentLevel = gameStore.currentPlanet.buildings[buildingType] || 0
const name = BUILDINGS.value[buildingType]?.name || buildingType
const status = currentLevel >= requiredLevel ? '✓' : '✗'
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
}
// 检查是否为科技类型
else if (Object.values(TechnologyType).includes(key as TechnologyType)) {
const techType = key as TechnologyType
const currentLevel = gameStore.player.technologies[techType] || 0
const name = TECHNOLOGIES.value[techType]?.name || techType
const status = currentLevel >= requiredLevel ? '✓' : '✗'
lines.push(`${status} ${name}: Lv ${requiredLevel} (${t('common.current')}: Lv ${currentLevel})`)
items.push({ name, requiredLevel, currentLevel, met: currentLevel >= requiredLevel })
}
}
return lines.join('\n')
return items
}
const showRequirements = () => {
requirementsDialogTitle.value = t('common.requirementsNotMet')
requirementsDialogMessage.value = getRequirementsList()
requirementsDialogItems.value = getRequirementsList()
requirementsDialogOpen.value = true
}
</script>

View File

@@ -16,7 +16,7 @@
<div class="flex items-center justify-between p-4 border-b">
<h3 class="font-semibold">{{ t('queue.title') }}</h3>
</div>
<ScrollArea class="max-h-96">
<ScrollArea class="h-[480px]">
<div v-if="totalQueueCount === 0" class="p-8 text-center text-muted-foreground">
{{ t('queue.empty') }}
</div>

View File

@@ -1,26 +1,17 @@
<template>
<div
:class="
cn(
'relative size-full overflow-hidden bg-[radial-gradient(ellipse_at_bottom,_#262626_0%,_#000_100%)]',
props.class,
)
"
:class="cn('relative size-full overflow-hidden bg-[radial-gradient(ellipse_at_bottom,_#262626_0%,_#000_100%)]', props.class)"
@mousemove="handleMouseMove"
>
<motion.div :style="{ x: springX, y: springY }">
<!-- Star Layer 1 -->
<motion.div
class="absolute top-0 left-0 w-full h-[2000px]"
:animate="{ y: [0, -2000] }"
:transition="starLayer1Transition"
>
<motion.div class="absolute top-0 left-0 w-full h-[2000px]" :animate="{ y: [0, -2000] }" :transition="starLayer1Transition">
<div
class="absolute bg-transparent rounded-full"
:style="{
width: '1px',
height: '1px',
boxShadow: boxShadow1,
boxShadow: boxShadow1
}"
/>
<div
@@ -28,23 +19,19 @@
:style="{
width: '1px',
height: '1px',
boxShadow: boxShadow1,
boxShadow: boxShadow1
}"
/>
</motion.div>
<!-- Star Layer 2 -->
<motion.div
class="absolute top-0 left-0 w-full h-[2000px]"
:animate="{ y: [0, -2000] }"
:transition="starLayer2Transition"
>
<motion.div class="absolute top-0 left-0 w-full h-[2000px]" :animate="{ y: [0, -2000] }" :transition="starLayer2Transition">
<div
class="absolute bg-transparent rounded-full"
:style="{
width: '2px',
height: '2px',
boxShadow: boxShadow2,
boxShadow: boxShadow2
}"
/>
<div
@@ -52,23 +39,19 @@
:style="{
width: '2px',
height: '2px',
boxShadow: boxShadow2,
boxShadow: boxShadow2
}"
/>
</motion.div>
<!-- Star Layer 3 -->
<motion.div
class="absolute top-0 left-0 w-full h-[2000px]"
:animate="{ y: [0, -2000] }"
:transition="starLayer3Transition"
>
<motion.div class="absolute top-0 left-0 w-full h-[2000px]" :animate="{ y: [0, -2000] }" :transition="starLayer3Transition">
<div
class="absolute bg-transparent rounded-full"
:style="{
width: '3px',
height: '3px',
boxShadow: boxShadow3,
boxShadow: boxShadow3
}"
/>
<div
@@ -76,7 +59,7 @@
:style="{
width: '3px',
height: '3px',
boxShadow: boxShadow3,
boxShadow: boxShadow3
}"
/>
</motion.div>
@@ -88,89 +71,89 @@
</template>
<script setup lang="ts">
import type {SpringOptions, Transition} from "motion-v";
import { cn } from "@/lib/utils";
import { motion, useMotionValue, useSpring } from "motion-v";
import { computed, onMounted, ref, watch } from "vue";
import type { SpringOptions, Transition } from 'motion-v'
import { cn } from '@/lib/utils'
import { motion, useMotionValue, useSpring } from 'motion-v'
import { computed, onMounted, ref, watch } from 'vue'
interface StarsBackgroundProps {
factor?: number;
speed?: number;
transition?: SpringOptions;
starColor?: string;
class?: string;
}
const props = withDefaults(defineProps<StarsBackgroundProps>(), {
factor: 0.05,
speed: 50,
transition: () => ({ stiffness: 50, damping: 20 }),
starColor: "#fff",
});
// For slot content
defineSlots();
function generateStars(count: number, starColor: string) {
const shadows: string[] = [];
for (let i = 0; i < count; i++) {
const x = Math.floor(Math.random() * 4000) - 2000;
const y = Math.floor(Math.random() * 4000) - 2000;
shadows.push(`${x}px ${y}px ${starColor}`);
interface StarsBackgroundProps {
factor?: number
speed?: number
transition?: SpringOptions
starColor?: string
class?: string
}
return shadows.join(", ");
}
const offsetX = useMotionValue(1);
const offsetY = useMotionValue(1);
const props = withDefaults(defineProps<StarsBackgroundProps>(), {
factor: 0.05,
speed: 50,
transition: () => ({ stiffness: 50, damping: 20 }),
starColor: '#fff'
})
const springX = useSpring(offsetX, props.transition);
const springY = useSpring(offsetY, props.transition);
// For slot content
defineSlots()
function handleMouseMove(e: MouseEvent) {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const newOffsetX = -(e.clientX - centerX) * props.factor;
const newOffsetY = -(e.clientY - centerY) * props.factor;
offsetX.set(newOffsetX);
offsetY.set(newOffsetY);
}
function generateStars(count: number, starColor: string) {
const shadows: string[] = []
for (let i = 0; i < count; i++) {
const x = Math.floor(Math.random() * 4000) - 2000
const y = Math.floor(Math.random() * 4000) - 2000
shadows.push(`${x}px ${y}px ${starColor}`)
}
return shadows.join(', ')
}
const boxShadow1 = ref("");
const boxShadow2 = ref("");
const boxShadow3 = ref("");
const offsetX = useMotionValue(1)
const offsetY = useMotionValue(1)
onMounted(() => {
boxShadow1.value = generateStars(1000, props.starColor);
boxShadow2.value = generateStars(400, props.starColor);
boxShadow3.value = generateStars(200, props.starColor);
});
const springX = useSpring(offsetX, props.transition)
const springY = useSpring(offsetY, props.transition)
// Watch for starColor changes
watch(
() => props.starColor,
(newColor) => {
boxShadow1.value = generateStars(1000, newColor);
boxShadow2.value = generateStars(400, newColor);
boxShadow3.value = generateStars(200, newColor);
},
);
function handleMouseMove(e: MouseEvent) {
const centerX = window.innerWidth / 2
const centerY = window.innerHeight / 2
const newOffsetX = -(e.clientX - centerX) * props.factor
const newOffsetY = -(e.clientY - centerY) * props.factor
offsetX.set(newOffsetX)
offsetY.set(newOffsetY)
}
const starLayer1Transition = computed<Transition>(() => ({
repeat: Infinity,
duration: props.speed,
ease: "linear" as const,
}));
const boxShadow1 = ref('')
const boxShadow2 = ref('')
const boxShadow3 = ref('')
const starLayer2Transition = computed<Transition>(() => ({
repeat: Infinity,
duration: props.speed * 2,
ease: "linear" as const,
}));
onMounted(() => {
boxShadow1.value = generateStars(1000, props.starColor)
boxShadow2.value = generateStars(400, props.starColor)
boxShadow3.value = generateStars(200, props.starColor)
})
const starLayer3Transition = computed<Transition>(() => ({
repeat: Infinity,
duration: props.speed * 3,
ease: "linear" as const,
}));
// Watch for starColor changes
watch(
() => props.starColor,
newColor => {
boxShadow1.value = generateStars(1000, newColor)
boxShadow2.value = generateStars(400, newColor)
boxShadow3.value = generateStars(200, newColor)
}
)
const starLayer1Transition = computed<Transition>(() => ({
repeat: Infinity,
duration: props.speed,
ease: 'linear' as const
}))
const starLayer2Transition = computed<Transition>(() => ({
repeat: Infinity,
duration: props.speed * 2,
ease: 'linear' as const
}))
const starLayer3Transition = computed<Transition>(() => ({
repeat: Infinity,
duration: props.speed * 3,
ease: 'linear' as const
}))
</script>

View File

@@ -1 +1 @@
export { default as StarsBackground } from "./StarsBackground.vue";
export { default as StarsBackground } from './StarsBackground.vue'

View File

@@ -17,7 +17,7 @@
</div>
<!-- 可滚动的内容区域 -->
<div class="overflow-y-auto px-4 py-3 sm:px-6 sm:py-4">
<div class="overflow-y-auto px-4 py-3 sm:px-6 sm:py-4 max-h-[60vh]">
<slot />
</div>

View File

@@ -1,250 +1,238 @@
<template>
<div
ref="canvasContainerRef"
:class="$props.class"
aria-hidden="true"
>
<canvas ref="canvasRef"></canvas>
<div ref="canvasContainerRef" :class="$props.class" aria-hidden="true">
<canvas ref="canvasRef" />
</div>
</template>
<script setup lang="ts">
import { useMouse, useDevicePixelRatio } from "@vueuse/core";
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from "vue";
import { useMouse, useDevicePixelRatio } from '@vueuse/core'
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from 'vue'
type Circle = {
x: number;
y: number;
translateX: number;
translateY: number;
size: number;
alpha: number;
targetAlpha: number;
dx: number;
dy: number;
magnetism: number;
};
type Props = {
color?: string;
quantity?: number;
staticity?: number;
ease?: number;
class?: string;
};
const props = withDefaults(defineProps<Props>(), {
color: "#FFF",
quantity: 100,
staticity: 50,
ease: 50,
class: "",
});
const canvasRef = ref<HTMLCanvasElement | null>(null);
const canvasContainerRef = ref<HTMLDivElement | null>(null);
const context = ref<CanvasRenderingContext2D | null>(null);
const circles = ref<Circle[]>([]);
const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 });
const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 });
const { x: mouseX, y: mouseY } = useMouse();
const { pixelRatio } = useDevicePixelRatio();
const color = computed(() => {
// Remove the leading '#' if it's present
let hex = props.color.replace(/^#/, "");
// If the hex code is 3 characters, expand it to 6 characters
if (hex.length === 3) {
hex = hex
.split("")
.map((char) => char + char)
.join("");
type Circle = {
x: number
y: number
translateX: number
translateY: number
size: number
alpha: number
targetAlpha: number
dx: number
dy: number
magnetism: number
}
// Parse the r, g, b values from the hex string
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255; // Extract the red component
const g = (bigint >> 8) & 255; // Extract the green component
const b = bigint & 255; // Extract the blue component
// Return the RGB values as a string separated by spaces
return `${r} ${g} ${b}`;
});
onMounted(() => {
if (canvasRef.value) {
context.value = canvasRef.value.getContext("2d");
type Props = {
color?: string
quantity?: number
staticity?: number
ease?: number
class?: string
}
initCanvas();
animate();
window.addEventListener("resize", initCanvas);
});
const props = withDefaults(defineProps<Props>(), {
color: '#FFF',
quantity: 100,
staticity: 50,
ease: 50,
class: ''
})
onBeforeUnmount(() => {
window.removeEventListener("resize", initCanvas);
});
const canvasRef = ref<HTMLCanvasElement | null>(null)
const canvasContainerRef = ref<HTMLDivElement | null>(null)
const context = ref<CanvasRenderingContext2D | null>(null)
const circles = ref<Circle[]>([])
const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 })
const canvasSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 })
const { x: mouseX, y: mouseY } = useMouse()
const { pixelRatio } = useDevicePixelRatio()
watch([mouseX, mouseY], () => {
onMouseMove();
});
const color = computed(() => {
// Remove the leading '#' if it's present
let hex = props.color.replace(/^#/, '')
function initCanvas() {
resizeCanvas();
drawParticles();
}
function onMouseMove() {
if (canvasRef.value) {
const rect = canvasRef.value.getBoundingClientRect();
const { w, h } = canvasSize;
const x = mouseX.value - rect.left - w / 2;
const y = mouseY.value - rect.top - h / 2;
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
if (inside) {
mouse.x = x;
mouse.y = y;
}
}
}
function resizeCanvas() {
if (canvasContainerRef.value && canvasRef.value && context.value) {
circles.value.length = 0;
canvasSize.w = canvasContainerRef.value.offsetWidth;
canvasSize.h = canvasContainerRef.value.offsetHeight;
canvasRef.value.width = canvasSize.w * pixelRatio.value;
canvasRef.value.height = canvasSize.h * pixelRatio.value;
canvasRef.value.style.width = canvasSize.w + "px";
canvasRef.value.style.height = canvasSize.h + "px";
context.value.scale(pixelRatio.value, pixelRatio.value);
}
}
function circleParams(): Circle {
const x = Math.floor(Math.random() * canvasSize.w);
const y = Math.floor(Math.random() * canvasSize.h);
const translateX = 0;
const translateY = 0;
const size = Math.floor(Math.random() * 2) + 1;
const alpha = 0;
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
const dx = (Math.random() - 0.5) * 0.2;
const dy = (Math.random() - 0.5) * 0.2;
const magnetism = 0.1 + Math.random() * 4;
return {
x,
y,
translateX,
translateY,
size,
alpha,
targetAlpha,
dx,
dy,
magnetism,
};
}
function drawCircle(circle: Circle, update = false) {
if (context.value) {
const { x, y, translateX, translateY, size, alpha } = circle;
context.value.translate(translateX, translateY);
context.value.beginPath();
context.value.arc(x, y, size, 0, 2 * Math.PI);
context.value.fillStyle = `rgba(${color.value.split(" ").join(", ")}, ${alpha})`;
context.value.fill();
context.value.setTransform(pixelRatio.value, 0, 0, pixelRatio.value, 0, 0);
if (!update) {
circles.value.push(circle);
}
}
}
function clearContext() {
if (context.value) {
context.value.clearRect(0, 0, canvasSize.w, canvasSize.h);
}
}
function drawParticles() {
clearContext();
const particleCount = props.quantity;
for (let i = 0; i < particleCount; i++) {
const circle = circleParams();
drawCircle(circle);
}
}
function remapValue(
value: number,
start1: number,
end1: number,
start2: number,
end2: number,
): number {
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
return remapped > 0 ? remapped : 0;
}
function animate() {
clearContext();
circles.value.forEach((circle, i) => {
// Handle the alpha value
const edge = [
circle.x + circle.translateX - circle.size, // distance from left edge
canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
circle.y + circle.translateY - circle.size, // distance from top edge
canvasSize.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
];
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
if (remapClosestEdge > 1) {
circle.alpha += 0.02;
if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha;
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge;
// If the hex code is 3 characters, expand it to 6 characters
if (hex.length === 3) {
hex = hex
.split('')
.map(char => char + char)
.join('')
}
circle.x += circle.dx;
circle.y += circle.dy;
circle.translateX +=
(mouse.x / (props.staticity / circle.magnetism) - circle.translateX) / props.ease;
circle.translateY +=
(mouse.y / (props.staticity / circle.magnetism) - circle.translateY) / props.ease;
// Parse the r, g, b values from the hex string
const bigint = parseInt(hex, 16)
const r = (bigint >> 16) & 255 // Extract the red component
const g = (bigint >> 8) & 255 // Extract the green component
const b = bigint & 255 // Extract the blue component
// circle gets out of the canvas
if (
circle.x < -circle.size ||
circle.x > canvasSize.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.h + circle.size
) {
// remove the circle from the array
circles.value.splice(i, 1);
// create a new circle
const newCircle = circleParams();
drawCircle(newCircle);
// update the circle position
} else {
drawCircle(
{
...circle,
x: circle.x,
y: circle.y,
translateX: circle.translateX,
translateY: circle.translateY,
alpha: circle.alpha,
},
true,
);
// Return the RGB values as a string separated by spaces
return `${r} ${g} ${b}`
})
onMounted(() => {
if (canvasRef.value) {
context.value = canvasRef.value.getContext('2d')
}
});
window.requestAnimationFrame(animate);
}
initCanvas()
animate()
window.addEventListener('resize', initCanvas)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', initCanvas)
})
watch([mouseX, mouseY], () => {
onMouseMove()
})
function initCanvas() {
resizeCanvas()
drawParticles()
}
function onMouseMove() {
if (canvasRef.value) {
const rect = canvasRef.value.getBoundingClientRect()
const { w, h } = canvasSize
const x = mouseX.value - rect.left - w / 2
const y = mouseY.value - rect.top - h / 2
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
if (inside) {
mouse.x = x
mouse.y = y
}
}
}
function resizeCanvas() {
if (canvasContainerRef.value && canvasRef.value && context.value) {
circles.value.length = 0
canvasSize.w = canvasContainerRef.value.offsetWidth
canvasSize.h = canvasContainerRef.value.offsetHeight
canvasRef.value.width = canvasSize.w * pixelRatio.value
canvasRef.value.height = canvasSize.h * pixelRatio.value
canvasRef.value.style.width = canvasSize.w + 'px'
canvasRef.value.style.height = canvasSize.h + 'px'
context.value.scale(pixelRatio.value, pixelRatio.value)
}
}
function circleParams(): Circle {
const x = Math.floor(Math.random() * canvasSize.w)
const y = Math.floor(Math.random() * canvasSize.h)
const translateX = 0
const translateY = 0
const size = Math.floor(Math.random() * 2) + 1
const alpha = 0
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
const dx = (Math.random() - 0.5) * 0.2
const dy = (Math.random() - 0.5) * 0.2
const magnetism = 0.1 + Math.random() * 4
return {
x,
y,
translateX,
translateY,
size,
alpha,
targetAlpha,
dx,
dy,
magnetism
}
}
function drawCircle(circle: Circle, update = false) {
if (context.value) {
const { x, y, translateX, translateY, size, alpha } = circle
context.value.translate(translateX, translateY)
context.value.beginPath()
context.value.arc(x, y, size, 0, 2 * Math.PI)
context.value.fillStyle = `rgba(${color.value.split(' ').join(', ')}, ${alpha})`
context.value.fill()
context.value.setTransform(pixelRatio.value, 0, 0, pixelRatio.value, 0, 0)
if (!update) {
circles.value.push(circle)
}
}
}
function clearContext() {
if (context.value) {
context.value.clearRect(0, 0, canvasSize.w, canvasSize.h)
}
}
function drawParticles() {
clearContext()
const particleCount = props.quantity
for (let i = 0; i < particleCount; i++) {
const circle = circleParams()
drawCircle(circle)
}
}
function remapValue(value: number, start1: number, end1: number, start2: number, end2: number): number {
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
return remapped > 0 ? remapped : 0
}
function animate() {
clearContext()
circles.value.forEach((circle, i) => {
// Handle the alpha value
const edge = [
circle.x + circle.translateX - circle.size, // distance from left edge
canvasSize.w - circle.x - circle.translateX - circle.size, // distance from right edge
circle.y + circle.translateY - circle.size, // distance from top edge
canvasSize.h - circle.y - circle.translateY - circle.size // distance from bottom edge
]
const closestEdge = edge.reduce((a, b) => Math.min(a, b))
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2))
if (remapClosestEdge > 1) {
circle.alpha += 0.02
if (circle.alpha > circle.targetAlpha) circle.alpha = circle.targetAlpha
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge
}
circle.x += circle.dx
circle.y += circle.dy
circle.translateX += (mouse.x / (props.staticity / circle.magnetism) - circle.translateX) / props.ease
circle.translateY += (mouse.y / (props.staticity / circle.magnetism) - circle.translateY) / props.ease
// circle gets out of the canvas
if (
circle.x < -circle.size ||
circle.x > canvasSize.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.h + circle.size
) {
// remove the circle from the array
circles.value.splice(i, 1)
// create a new circle
const newCircle = circleParams()
drawCircle(newCircle)
// update the circle position
} else {
drawCircle(
{
...circle,
x: circle.x,
y: circle.y,
translateX: circle.translateX,
translateY: circle.translateY,
alpha: circle.alpha
},
true
)
}
})
window.requestAnimationFrame(animate)
}
</script>

View File

@@ -1 +1 @@
export { default as ParticlesBg } from "./ParticlesBg.vue";
export { default as ParticlesBg } from './ParticlesBg.vue'

View File

@@ -1,20 +1,3 @@
<script setup lang="ts">
import type { SeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Separator } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<
SeparatorProps & { class?: HTMLAttributes["class"] }
>(), {
orientation: "horizontal",
decorative: true,
})
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<Separator
data-slot="separator"
@@ -22,8 +5,23 @@ const delegatedProps = reactiveOmit(props, "class")
:class="
cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
props.class,
props.class
)
"
/>
</template>
<script setup lang="ts">
import type { SeparatorProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { Separator } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), {
orientation: 'horizontal',
decorative: true
})
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -1 +1 @@
export { default as Separator } from "./Separator.vue"
export { default as Separator } from './Separator.vue'

View File

@@ -1,19 +1,15 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot
v-slot="slotProps"
data-slot="sheet"
v-bind="forwarded"
>
<DialogRoot v-slot="slotProps" data-slot="sheet" v-bind="forwarded">
<slot v-bind="slotProps" />
</DialogRoot>
</template>
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
import { DialogRoot, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
import type { DialogCloseProps } from "reka-ui"
import { DialogClose } from "reka-ui"
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose
data-slot="sheet-close"
v-bind="props"
>
<DialogClose data-slot="sheet-close" v-bind="props">
<slot />
</DialogClose>
</template>
<script setup lang="ts">
import type { DialogCloseProps } from 'reka-ui'
import { DialogClose } from 'reka-ui'
const props = defineProps<DialogCloseProps>()
</script>

View File

@@ -1,52 +1,21 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import SheetOverlay from "./SheetOverlay.vue"
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes["class"]
side?: "top" | "right" | "bottom" | "left"
}
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SheetContentProps>(), {
side: "right",
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class", "side")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<SheetOverlay />
<DialogContent
data-slot="sheet-content"
:class="cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right'
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left'
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top'
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom'
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
props.class)"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right' &&
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left' &&
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top' && 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom' &&
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
props.class
)
"
v-bind="{ ...$attrs, ...forwarded }"
>
<slot />
@@ -60,3 +29,31 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
</DialogContent>
</DialogPortal>
</template>
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { X } from 'lucide-vue-next'
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import SheetOverlay from './SheetOverlay.vue'
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes['class']
side?: 'top' | 'right' | 'bottom' | 'left'
}
defineOptions({
inheritAttrs: false
})
const props = withDefaults(defineProps<SheetContentProps>(), {
side: 'right'
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, 'class', 'side')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

View File

@@ -1,21 +1,17 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogDescription } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogDescription
data-slot="sheet-description"
:class="cn('text-muted-foreground text-sm', props.class)"
v-bind="delegatedProps"
>
<DialogDescription data-slot="sheet-description" :class="cn('text-muted-foreground text-sm', props.class)" v-bind="delegatedProps">
<slot />
</DialogDescription>
</template>
<script setup lang="ts">
import type { DialogDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DialogDescription } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -1,16 +1,12 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-footer"
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
"
>
<div data-slot="sheet-footer" :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
</script>

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<div data-slot="sheet-header" :class="cn('flex flex-col gap-1.5 p-4', props.class)">
<slot />
</div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
</script>

View File

@@ -1,21 +1,26 @@
<script setup lang="ts">
import type { DialogOverlayProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogOverlay } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogOverlay
data-slot="sheet-overlay"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
:class="
cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
props.class
)
"
v-bind="delegatedProps"
>
<slot />
</DialogOverlay>
</template>
<script setup lang="ts">
import type { DialogOverlayProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DialogOverlay } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -1,21 +1,17 @@
<script setup lang="ts">
import type { DialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogTitle } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogTitle
data-slot="sheet-title"
:class="cn('text-foreground font-semibold', props.class)"
v-bind="delegatedProps"
>
<DialogTitle data-slot="sheet-title" :class="cn('text-foreground font-semibold', props.class)" v-bind="delegatedProps">
<slot />
</DialogTitle>
</template>
<script setup lang="ts">
import type { DialogTitleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DialogTitle } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>

View File

@@ -1,15 +1,12 @@
<script setup lang="ts">
import type { DialogTriggerProps } from "reka-ui"
import { DialogTrigger } from "reka-ui"
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger
data-slot="sheet-trigger"
v-bind="props"
>
<DialogTrigger data-slot="sheet-trigger" v-bind="props">
<slot />
</DialogTrigger>
</template>
<script setup lang="ts">
import type { DialogTriggerProps } from 'reka-ui'
import { DialogTrigger } from 'reka-ui'
const props = defineProps<DialogTriggerProps>()
</script>

View File

@@ -1,8 +1,8 @@
export { default as Sheet } from "./Sheet.vue"
export { default as SheetClose } from "./SheetClose.vue"
export { default as SheetContent } from "./SheetContent.vue"
export { default as SheetDescription } from "./SheetDescription.vue"
export { default as SheetFooter } from "./SheetFooter.vue"
export { default as SheetHeader } from "./SheetHeader.vue"
export { default as SheetTitle } from "./SheetTitle.vue"
export { default as SheetTrigger } from "./SheetTrigger.vue"
export { default as Sheet } from './Sheet.vue'
export { default as SheetClose } from './SheetClose.vue'
export { default as SheetContent } from './SheetContent.vue'
export { default as SheetDescription } from './SheetDescription.vue'
export { default as SheetFooter } from './SheetFooter.vue'
export { default as SheetHeader } from './SheetHeader.vue'
export { default as SheetTitle } from './SheetTitle.vue'
export { default as SheetTrigger } from './SheetTrigger.vue'

View File

@@ -1,17 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
interface SkeletonProps {
class?: HTMLAttributes["class"]
}
const props = defineProps<SkeletonProps>()
</script>
<template>
<div
data-slot="skeleton"
:class="cn('animate-pulse rounded-md bg-primary/10', props.class)"
/>
<div data-slot="skeleton" :class="cn('animate-pulse rounded-md bg-primary/10', props.class)" />
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
interface SkeletonProps {
class?: HTMLAttributes['class']
}
const props = defineProps<SkeletonProps>()
</script>

View File

@@ -1 +1 @@
export { default as Skeleton } from "./Skeleton.vue"
export { default as Skeleton } from './Skeleton.vue'