commit
740b95b79a
27 changed files with 8474 additions and 0 deletions
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs |
||||
.output |
||||
.data |
||||
.nuxt |
||||
.nitro |
||||
.cache |
||||
dist |
||||
|
||||
# Node dependencies |
||||
node_modules |
||||
|
||||
# Logs |
||||
logs |
||||
*.log |
||||
|
||||
# Misc |
||||
.DS_Store |
||||
.fleet |
||||
.idea |
||||
|
||||
# Local env files |
||||
.env |
||||
.env.* |
||||
!.env.example |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
{ |
||||
"$schema": "https://json.schemastore.org/prettierrc", |
||||
"semi": false, |
||||
"singleQuote": true, |
||||
"printWidth": 100, |
||||
"plugins": ["prettier-plugin-tailwindcss"], |
||||
"tailwindStylesheet": "./assets/styles/main.css", |
||||
"tailwindConfig": "./tailwind.config.js", |
||||
"tailwindFunctions": ["clsx"] |
||||
} |
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
# Nuxt Minimal Starter |
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. |
||||
|
||||
## Setup |
||||
|
||||
Make sure to install dependencies: |
||||
|
||||
```bash |
||||
# npm |
||||
npm install |
||||
|
||||
# pnpm |
||||
pnpm install |
||||
|
||||
# yarn |
||||
yarn install |
||||
|
||||
# bun |
||||
bun install |
||||
``` |
||||
|
||||
## Development Server |
||||
|
||||
Start the development server on `http://localhost:3000`: |
||||
|
||||
```bash |
||||
# npm |
||||
npm run dev |
||||
|
||||
# pnpm |
||||
pnpm dev |
||||
|
||||
# yarn |
||||
yarn dev |
||||
|
||||
# bun |
||||
bun run dev |
||||
``` |
||||
|
||||
## Production |
||||
|
||||
Build the application for production: |
||||
|
||||
```bash |
||||
# npm |
||||
npm run build |
||||
|
||||
# pnpm |
||||
pnpm build |
||||
|
||||
# yarn |
||||
yarn build |
||||
|
||||
# bun |
||||
bun run build |
||||
``` |
||||
|
||||
Locally preview production build: |
||||
|
||||
```bash |
||||
# npm |
||||
npm run preview |
||||
|
||||
# pnpm |
||||
pnpm preview |
||||
|
||||
# yarn |
||||
yarn preview |
||||
|
||||
# bun |
||||
bun run preview |
||||
``` |
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
<template> |
||||
<NuxtLayout> |
||||
<NuxtPage /> |
||||
</NuxtLayout> |
||||
</template> |
After Width: | Height: | Size: 2.4 MiB |
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
@tailwind base; |
||||
@tailwind components; |
||||
@tailwind utilities; |
||||
@layer base { |
||||
:root { |
||||
--background: 0 0% 100%; |
||||
--foreground: 0 0% 3.9%; |
||||
|
||||
--card: 0 0% 100%; |
||||
--card-foreground: 0 0% 3.9%; |
||||
|
||||
--popover: 0 0% 100%; |
||||
--popover-foreground: 0 0% 3.9%; |
||||
|
||||
--primary: 0 0% 9%; |
||||
--primary-foreground: 0 0% 98%; |
||||
|
||||
--secondary: 0 0% 96.1%; |
||||
--secondary-foreground: 0 0% 9%; |
||||
|
||||
--muted: 0 0% 96.1%; |
||||
--muted-foreground: 0 0% 45.1%; |
||||
|
||||
--accent: 0 0% 96.1%; |
||||
--accent-foreground: 0 0% 9%; |
||||
|
||||
--destructive: 0 84.2% 60.2%; |
||||
--destructive-foreground: 0 0% 98%; |
||||
|
||||
--border: 0 0% 89.8%; |
||||
--input: 0 0% 89.8%; |
||||
--ring: 0 0% 3.9%; |
||||
--radius: 0.5rem; |
||||
--chart-1: 12 76% 61%; |
||||
--chart-2: 173 58% 39%; |
||||
--chart-3: 197 37% 24%; |
||||
--chart-4: 43 74% 66%; |
||||
--chart-5: 27 87% 67%; |
||||
} |
||||
|
||||
.dark { |
||||
--background: 0 0% 3.9%; |
||||
--foreground: 0 0% 98%; |
||||
|
||||
--card: 0 0% 3.9%; |
||||
--card-foreground: 0 0% 98%; |
||||
|
||||
--popover: 0 0% 3.9%; |
||||
--popover-foreground: 0 0% 98%; |
||||
|
||||
--primary: 0 0% 98%; |
||||
--primary-foreground: 0 0% 9%; |
||||
|
||||
--secondary: 0 0% 14.9%; |
||||
--secondary-foreground: 0 0% 98%; |
||||
|
||||
--muted: 0 0% 14.9%; |
||||
--muted-foreground: 0 0% 63.9%; |
||||
|
||||
--accent: 0 0% 14.9%; |
||||
--accent-foreground: 0 0% 98%; |
||||
|
||||
--destructive: 0 62.8% 30.6%; |
||||
--destructive-foreground: 0 0% 98%; |
||||
|
||||
--border: 0 0% 14.9%; |
||||
--input: 0 0% 14.9%; |
||||
--ring: 0 0% 83.1%; |
||||
--chart-1: 220 70% 50%; |
||||
--chart-2: 160 60% 45%; |
||||
--chart-3: 30 80% 55%; |
||||
--chart-4: 280 65% 60%; |
||||
--chart-5: 340 75% 55%; |
||||
} |
||||
} |
||||
@layer base { |
||||
* { |
||||
@apply border-border; |
||||
} |
||||
body { |
||||
@apply bg-background text-foreground; |
||||
} |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
@import './base.css'; |
||||
|
||||
html { |
||||
@apply w-full h-full; |
||||
} |
||||
|
||||
body { |
||||
@apply w-full h-full; |
||||
} |
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
{ |
||||
"$schema": "https://shadcn-vue.com/schema.json", |
||||
"style": "new-york", |
||||
"typescript": true, |
||||
"tailwind": { |
||||
"config": "tailwind.config.js", |
||||
"css": "assets/styles/base.css", |
||||
"baseColor": "neutral", |
||||
"cssVariables": true, |
||||
"prefix": "" |
||||
}, |
||||
"aliases": { |
||||
"components": "@/components", |
||||
"composables": "@/composables", |
||||
"utils": "@/lib/utils", |
||||
"ui": "@/components/ui", |
||||
"lib": "@/lib" |
||||
}, |
||||
"iconLibrary": "lucide" |
||||
} |
@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
|
||||
<template> |
||||
<div |
||||
:class=" |
||||
cn( |
||||
'relative z-0 flex min-h-screen w-full flex-col items-center justify-center overflow-hidden rounded-md bg-slate-950', |
||||
$props.class, |
||||
) |
||||
" |
||||
> |
||||
<div class="relative isolate z-0 flex w-full flex-1 scale-y-125 items-center justify-center"> |
||||
<!-- Conic Gradient --> |
||||
<div |
||||
:style="{ |
||||
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`, |
||||
}" |
||||
class="animate-conic-gradient bg-gradient-conic absolute inset-auto right-1/2 h-56 w-60 overflow-visible from-cyan-500 via-transparent to-transparent text-white opacity-50 [--conic-position:from_70deg_at_center_top]" |
||||
> |
||||
<div |
||||
class="absolute bottom-0 left-0 z-20 h-40 w-full bg-slate-950 [mask-image:linear-gradient(to_top,white,transparent)]" |
||||
/> |
||||
<div |
||||
class="absolute bottom-0 left-0 z-20 h-full w-40 bg-slate-950 [mask-image:linear-gradient(to_right,white,transparent)]" |
||||
/> |
||||
</div> |
||||
|
||||
<div |
||||
:style="{ |
||||
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`, |
||||
}" |
||||
class="animate-conic-gradient bg-gradient-conic absolute inset-auto left-1/2 h-56 w-60 from-transparent via-transparent to-cyan-500 text-white opacity-50 [--conic-position:from_290deg_at_center_top]" |
||||
> |
||||
<div |
||||
class="absolute bottom-0 right-0 z-20 h-full w-40 bg-slate-950 [mask-image:linear-gradient(to_left,white,transparent)]" |
||||
/> |
||||
<div |
||||
class="absolute bottom-0 right-0 z-20 h-40 w-full bg-slate-950 [mask-image:linear-gradient(to_top,white,transparent)]" |
||||
/> |
||||
</div> |
||||
|
||||
<div |
||||
class="absolute top-1/2 h-48 w-full translate-y-12 scale-x-150 bg-slate-950 blur-2xl" |
||||
></div> |
||||
|
||||
<div |
||||
class="absolute top-1/2 z-50 h-48 w-full bg-transparent opacity-10 backdrop-blur-md" |
||||
></div> |
||||
|
||||
<div |
||||
class="absolute inset-auto z-50 h-36 w-[28rem] -translate-y-1/2 rounded-full bg-cyan-500 opacity-50 blur-3xl" |
||||
></div> |
||||
|
||||
<!-- Spotlight --> |
||||
<div |
||||
class="animate-spotlight absolute inset-auto z-30 h-36 w-32 -translate-y-24 rounded-full bg-cyan-400 blur-2xl" |
||||
></div> |
||||
|
||||
<!-- Glowing Line --> |
||||
<div |
||||
class="animate-glowing-line absolute inset-auto z-50 h-0.5 w-60 -translate-y-28 bg-cyan-400" |
||||
></div> |
||||
|
||||
<div class="absolute inset-auto z-40 h-44 w-full translate-y-[-12.5rem] bg-slate-950"></div> |
||||
</div> |
||||
|
||||
<div class="relative z-50 flex -translate-y-80 flex-col items-center px-5"> |
||||
<slot /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { computed, type HTMLAttributes } from 'vue' |
||||
import { cn } from '@/lib/utils' |
||||
interface LampEffectProps { |
||||
delay?: number |
||||
duration?: number |
||||
class?: HTMLAttributes['class'] |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<LampEffectProps>(), { |
||||
delay: 0.5, |
||||
duration: 0.8, |
||||
}) |
||||
|
||||
const durationInSeconds = computed(() => `${props.duration}s`) |
||||
const delayInSeconds = computed(() => `${props.delay}s`) |
||||
</script> |
||||
|
||||
<style scoped> |
||||
/* Spotlight Animation */ |
||||
.animate-spotlight { |
||||
animation: spotlight-anim ease-in-out v-bind(durationInSeconds) forwards; |
||||
animation-delay: v-bind(delayInSeconds); |
||||
} |
||||
|
||||
/* Glowing Line Animation */ |
||||
.animate-glowing-line { |
||||
animation: glowing-line-anim ease-in-out v-bind(durationInSeconds) forwards; |
||||
animation-delay: v-bind(delayInSeconds); |
||||
} |
||||
|
||||
/* Conic Gradient Animation */ |
||||
.animate-conic-gradient { |
||||
animation: conic-gradient-anim ease-in-out v-bind(durationInSeconds) forwards; |
||||
animation-delay: v-bind(delayInSeconds); |
||||
} |
||||
|
||||
/* Keyframes for Spotlight */ |
||||
@keyframes spotlight-anim { |
||||
from { |
||||
width: 8rem; |
||||
} |
||||
to { |
||||
width: 16rem; |
||||
} |
||||
} |
||||
|
||||
/* Keyframes for Glowing Line */ |
||||
@keyframes glowing-line-anim { |
||||
from { |
||||
width: 15rem; |
||||
} |
||||
to { |
||||
width: 30rem; |
||||
} |
||||
} |
||||
|
||||
/* Keyframes for Conic Gradient */ |
||||
@keyframes conic-gradient-anim { |
||||
from { |
||||
opacity: 0.5; |
||||
width: 15rem; |
||||
} |
||||
to { |
||||
opacity: 1; |
||||
width: 30rem; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,253 @@
@@ -0,0 +1,253 @@
|
||||
<template> |
||||
<div :class="cn('relative h-full w-full', props.containerClass)"> |
||||
<Motion |
||||
ref="containerRef" |
||||
as="div" |
||||
:initial="{ opacity: 0 }" |
||||
:animate="{ opacity: 1 }" |
||||
class="absolute inset-0 z-0 flex size-full items-center justify-center bg-transparent" |
||||
> |
||||
<canvas ref="canvasRef"></canvas> |
||||
</Motion> |
||||
|
||||
<div :class="cn('relative z-10', props.class)"> |
||||
<slot /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { createNoise3D } from 'simplex-noise' |
||||
import { onMounted, onUnmounted } from 'vue' |
||||
import { templateRef, useDebounceFn } from '@vueuse/core' |
||||
import { cn } from '@/lib/utils' |
||||
const TAU = 2 * Math.PI |
||||
const BASE_TTL = 50 |
||||
const RANGE_TTL = 150 |
||||
const PARTICLE_PROP_COUNT = 9 |
||||
const RANGE_HUE = 100 |
||||
const NOISE_STEPS = 3 |
||||
const X_OFF = 0.00125 |
||||
const Y_OFF = 0.00125 |
||||
const Z_OFF = 0.0005 |
||||
|
||||
interface VortexProps { |
||||
class?: string |
||||
containerClass?: string |
||||
particleCount?: number |
||||
rangeY?: number |
||||
baseHue?: number |
||||
baseSpeed?: number |
||||
rangeSpeed?: number |
||||
baseRadius?: number |
||||
rangeRadius?: number |
||||
backgroundColor?: string |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<VortexProps>(), { |
||||
particleCount: 700, |
||||
rangeY: 100, |
||||
baseSpeed: 0.0, |
||||
rangeSpeed: 1.5, |
||||
baseRadius: 1, |
||||
rangeRadius: 2, |
||||
baseHue: 220, |
||||
backgroundColor: '#000000', |
||||
}) |
||||
|
||||
const tick = ref<number>(0) |
||||
const animationFrame = ref<number | null>(null) |
||||
const particleProps = shallowRef<Float32Array | null>(null) |
||||
const center = ref<[number, number]>([0, 0]) |
||||
const ctx = shallowRef<CanvasRenderingContext2D | null>(null) |
||||
|
||||
const canvasRef = templateRef<HTMLCanvasElement | null>('canvasRef') |
||||
const containerRef = templateRef<HTMLElement | null>('containerRef') |
||||
|
||||
const particleCache = { |
||||
x: 0, |
||||
y: 0, |
||||
vx: 0, |
||||
vy: 0, |
||||
life: 0, |
||||
ttl: 0, |
||||
speed: 0, |
||||
radius: 0, |
||||
hue: 0, |
||||
} |
||||
|
||||
const noise3D = createNoise3D() |
||||
|
||||
function rand(n: number) { |
||||
return n * Math.random() |
||||
} |
||||
function randRange(n: number): number { |
||||
return n - rand(2 * n) |
||||
} |
||||
function fadeInOut(t: number, m: number): number { |
||||
const hm = 0.5 * m |
||||
return Math.abs(((t + hm) % m) - hm) / hm |
||||
} |
||||
function lerp(n1: number, n2: number, speed: number): number { |
||||
return (1 - speed) * n1 + speed * n2 |
||||
} |
||||
|
||||
function initParticle(i: number) { |
||||
if (!particleProps.value || !canvasRef.value) return |
||||
|
||||
const canvas = canvasRef.value |
||||
particleCache.x = rand(canvas.width) |
||||
particleCache.y = center.value[1] + randRange(props.rangeY) |
||||
particleCache.vx = 0 |
||||
particleCache.vy = 0 |
||||
particleCache.life = 0 |
||||
particleCache.ttl = BASE_TTL + rand(RANGE_TTL) |
||||
particleCache.speed = props.baseSpeed + rand(props.rangeSpeed) |
||||
particleCache.radius = props.baseRadius + rand(props.rangeRadius) |
||||
particleCache.hue = props.baseHue + rand(RANGE_HUE) |
||||
|
||||
particleProps.value.set( |
||||
[ |
||||
particleCache.x, |
||||
particleCache.y, |
||||
particleCache.vx, |
||||
particleCache.vy, |
||||
particleCache.life, |
||||
particleCache.ttl, |
||||
particleCache.speed, |
||||
particleCache.radius, |
||||
particleCache.hue, |
||||
], |
||||
i, |
||||
) |
||||
} |
||||
|
||||
function updateParticle(i: number) { |
||||
if (!particleProps.value || !canvasRef.value || !ctx.value) return |
||||
|
||||
const canvas = canvasRef.value |
||||
const props = particleProps.value |
||||
const context = ctx.value |
||||
|
||||
particleCache.x = props[i] |
||||
particleCache.y = props[i + 1] |
||||
particleCache.vx = props[i + 2] |
||||
particleCache.vy = props[i + 3] |
||||
particleCache.life = props[i + 4] |
||||
particleCache.ttl = props[i + 5] |
||||
particleCache.speed = props[i + 6] |
||||
particleCache.radius = props[i + 7] |
||||
particleCache.hue = props[i + 8] |
||||
|
||||
const n = |
||||
noise3D(particleCache.x * X_OFF, particleCache.y * Y_OFF, tick.value * Z_OFF) * |
||||
NOISE_STEPS * |
||||
TAU |
||||
|
||||
const nextVx = lerp(particleCache.vx, Math.cos(n), 0.5) |
||||
const nextVy = lerp(particleCache.vy, Math.sin(n), 0.5) |
||||
const nextX = particleCache.x + nextVx * particleCache.speed |
||||
const nextY = particleCache.y + nextVy * particleCache.speed |
||||
|
||||
context.save() |
||||
context.lineCap = 'round' |
||||
context.lineWidth = particleCache.radius |
||||
context.strokeStyle = `hsla(${particleCache.hue},100%,60%,${fadeInOut( |
||||
particleCache.life, |
||||
particleCache.ttl, |
||||
)})` |
||||
context.beginPath() |
||||
context.moveTo(particleCache.x, particleCache.y) |
||||
context.lineTo(nextX, nextY) |
||||
context.stroke() |
||||
context.restore() |
||||
|
||||
props[i] = nextX |
||||
props[i + 1] = nextY |
||||
props[i + 2] = nextVx |
||||
props[i + 3] = nextVy |
||||
props[i + 4] = particleCache.life + 1 |
||||
|
||||
if ( |
||||
nextX > canvas.width || |
||||
nextX < 0 || |
||||
nextY > canvas.height || |
||||
nextY < 0 || |
||||
particleCache.life > particleCache.ttl |
||||
) { |
||||
initParticle(i) |
||||
} |
||||
} |
||||
|
||||
function draw() { |
||||
if (!canvasRef.value || !ctx.value || !particleProps.value) return |
||||
|
||||
const canvas = canvasRef.value |
||||
const context = ctx.value |
||||
|
||||
tick.value++ |
||||
|
||||
context.fillStyle = props.backgroundColor |
||||
context.fillRect(0, 0, canvas.width, canvas.height) |
||||
|
||||
for (let i = 0; i < particleProps.value.length; i += PARTICLE_PROP_COUNT) { |
||||
updateParticle(i) |
||||
} |
||||
|
||||
context.save() |
||||
context.filter = 'blur(8px) brightness(200%)' |
||||
context.globalCompositeOperation = 'lighter' |
||||
context.drawImage(canvas, 0, 0) |
||||
context.restore() |
||||
|
||||
context.save() |
||||
context.filter = 'blur(4px) brightness(200%)' |
||||
context.globalCompositeOperation = 'lighter' |
||||
context.drawImage(canvas, 0, 0) |
||||
context.restore() |
||||
|
||||
animationFrame.value = requestAnimationFrame(draw) |
||||
} |
||||
|
||||
const handleResize = useDebounceFn(() => { |
||||
if (!canvasRef.value) return |
||||
|
||||
const canvas = canvasRef.value |
||||
const { innerWidth, innerHeight } = window |
||||
canvas.width = innerWidth |
||||
canvas.height = innerHeight |
||||
center.value = [0.5 * canvas.width, 0.5 * canvas.height] |
||||
}, 150) |
||||
|
||||
onMounted(() => { |
||||
const canvas = canvasRef.value |
||||
if (!canvas) return |
||||
|
||||
ctx.value = canvas.getContext('2d') |
||||
if (!ctx.value) return |
||||
|
||||
canvas.width = window.innerWidth |
||||
canvas.height = window.innerHeight |
||||
center.value = [0.5 * canvas.width, 0.5 * canvas.height] |
||||
|
||||
const particlePropsLength = props.particleCount * PARTICLE_PROP_COUNT |
||||
particleProps.value = new Float32Array(particlePropsLength) |
||||
|
||||
for (let i = 0; i < particlePropsLength; i += PARTICLE_PROP_COUNT) { |
||||
initParticle(i) |
||||
} |
||||
|
||||
draw() |
||||
window.addEventListener('resize', handleResize) |
||||
}) |
||||
|
||||
onUnmounted(() => { |
||||
if (animationFrame.value) { |
||||
cancelAnimationFrame(animationFrame.value) |
||||
} |
||||
window.removeEventListener('resize', handleResize) |
||||
|
||||
ctx.value = null |
||||
particleProps.value = null |
||||
}) |
||||
</script> |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts"> |
||||
import type { HTMLAttributes } from 'vue' |
||||
import { cn } from '@/lib/utils' |
||||
import { Primitive, type PrimitiveProps } from 'reka-ui' |
||||
import { type ButtonVariants, buttonVariants } from '.' |
||||
|
||||
interface Props extends PrimitiveProps { |
||||
variant?: ButtonVariants['variant'] |
||||
size?: ButtonVariants['size'] |
||||
class?: HTMLAttributes['class'] |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<Props>(), { |
||||
as: 'button', |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<Primitive |
||||
:as="as" |
||||
:as-child="asChild" |
||||
:class="cn(buttonVariants({ variant, size }), props.class)" |
||||
> |
||||
<slot /> |
||||
</Primitive> |
||||
</template> |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority' |
||||
|
||||
export { default as Button } from './Button.vue' |
||||
|
||||
export const buttonVariants = cva( |
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', |
||||
{ |
||||
variants: { |
||||
variant: { |
||||
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', |
||||
destructive: |
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', |
||||
outline: |
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground active:bg-accent/900', |
||||
secondary: |
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', |
||||
ghost: 'hover:bg-accent hover:text-accent-foreground', |
||||
link: 'text-primary underline-offset-4 hover:underline', |
||||
}, |
||||
size: { |
||||
default: 'h-9 px-4 py-2', |
||||
xs: 'h-7 rounded px-2', |
||||
sm: 'h-8 rounded-md px-3 text-xs', |
||||
lg: 'h-10 rounded-md px-8', |
||||
icon: 'h-9 w-9', |
||||
}, |
||||
}, |
||||
defaultVariants: { |
||||
variant: 'default', |
||||
size: 'default', |
||||
}, |
||||
}, |
||||
) |
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants> |
@ -0,0 +1,238 @@
@@ -0,0 +1,238 @@
|
||||
<template> |
||||
<div> |
||||
<header |
||||
class="z-1000 fixed left-0 right-0 top-[-1] w-full bg-transparent duration-300" |
||||
:class="{ 'backdrop-blur-2xl': isScrolled || isNavbarOpen, 'h-full': isNavbarOpen }" |
||||
> |
||||
<div class="header-bg flex h-13 w-full items-center justify-between px-6 sm:px-4 md:px-8"> |
||||
<h1 class="text-2xl font-bold text-[#52AC63]">BTDK</h1> |
||||
<nav class="absolute left-0 right-0 top-0"> |
||||
<ul class="flex justify-center gap-8 leading-13 text-white sm:hidden md:hidden"> |
||||
<li |
||||
v-for="nav in navItems" |
||||
:key="nav.path" |
||||
class="relative h-full text-inherit after:absolute after:bottom-2 after:left-0 after:right-0 after:h-[1px] after:w-full after:origin-top-right after:scale-x-0 after:bg-[#52AC63] after:transition-[transform] after:duration-300 after:content-[''] hover:after:origin-top-left hover:after:scale-x-100" |
||||
> |
||||
<NuxtLink :to="nav.path" class="text-inherit"> |
||||
{{ nav.name }} |
||||
</NuxtLink> |
||||
</li> |
||||
</ul> |
||||
</nav> |
||||
<Menu as="div" class="hidden h-full sm:block md:block" v-slot="{ open: menuOpen }"> |
||||
<MenuButton class="h-full"> |
||||
<div |
||||
class="navbar-control" |
||||
:class="{ open: isNavbarOpen }" |
||||
@click="toggleNavbar(menuOpen)" |
||||
> |
||||
<span class="control-icon"></span> |
||||
<span class="control-icon"></span> |
||||
<span class="control-icon"></span> |
||||
</div> |
||||
</MenuButton> |
||||
<transition |
||||
enter-active-class="transition-all duration-300 overflow-hidden" |
||||
enter-from-class="max-h-0 opacity-70" |
||||
enter-to-class="h-full opacity-100" |
||||
leave-active-class="transition-all duration-200 overflow-hidden" |
||||
leave-from-class="h-full opacity-100" |
||||
leave-to-class="max-h-0 opacity-70" |
||||
> |
||||
<!-- lg:ml-[-110px] flex justify-center md:justify-start md:flex-col md:text-center md:overflow-auto md:grow-[1] --> |
||||
<div v-show="isNavbarOpen"> |
||||
<MenuItems |
||||
as="ul" |
||||
class="header-bg z-1000 border-t-1 absolute inset-0 top-13 overflow-hidden border-t-[1px] border-[hsla(0,0%,100%,.06)] text-white backdrop-blur-2xl" |
||||
static |
||||
> |
||||
<template v-for="nav in navItems" :key="nav.path"> |
||||
<MenuItem |
||||
as="li" |
||||
v-slot="{ close }" |
||||
class="cursor-pointer text-nowrap text-center" |
||||
@click="onNavClick(nav, menuOpen)" |
||||
> |
||||
<Disclosure as="div" class="size-full" v-slot="{ open }"> |
||||
<DisclosureButton as="p" class="relative py-7"> |
||||
<span>{{ nav.name }} </span> |
||||
<Icon |
||||
name="mingcute:up-line" |
||||
:class="open ? '' : 'rotate-180 transform'" |
||||
class="absolute right-5 top-1/2 -translate-y-1/2 text-lg transition-transform duration-300" |
||||
:ssr="true" |
||||
v-if="!!nav.children?.length" |
||||
/> |
||||
</DisclosureButton> |
||||
<transition |
||||
enter-active-class="transition-all duration-700 overflow-hidden" |
||||
enter-from-class="max-h-0 opacity-70" |
||||
enter-to-class="max-h-[300px] opacity-100" |
||||
leave-active-class="transition-all duration-200 overflow-hidden" |
||||
leave-from-class="max-h-[300px] opacity-100" |
||||
leave-to-class="max-h-0 opacity-70" |
||||
> |
||||
<DisclosurePanel as="div" v-if="!!nav.children?.length"> |
||||
<ul class="flex flex-col bg-[hsla(0,0%,100%,.06)]"> |
||||
<li |
||||
class="text-nowrap py-7 text-center" |
||||
v-for="child in nav.children" |
||||
:key="child.path" |
||||
@click="onNavClick(child, menuOpen)" |
||||
> |
||||
<span> |
||||
{{ child.name }} |
||||
</span> |
||||
</li> |
||||
</ul> |
||||
</DisclosurePanel> |
||||
</transition> |
||||
</Disclosure> |
||||
</MenuItem> |
||||
</template> |
||||
</MenuItems> |
||||
</div> |
||||
</transition> |
||||
</Menu> |
||||
</div> |
||||
</header> |
||||
<slot /> |
||||
<footer> |
||||
<p>版权所有 © 2025 比特电科</p> |
||||
</footer> |
||||
</div> |
||||
</template> |
||||
<script setup lang="ts"> |
||||
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' |
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' |
||||
|
||||
const isScrolled = ref(false) |
||||
const router = useRouter() |
||||
const checkScroll = () => { |
||||
isScrolled.value = window.scrollY > 20 |
||||
} |
||||
|
||||
onMounted(() => { |
||||
if (typeof window !== 'undefined') { |
||||
checkScroll() |
||||
window.addEventListener('scroll', checkScroll) |
||||
} |
||||
}) |
||||
|
||||
onUnmounted(() => { |
||||
if (typeof window !== 'undefined') { |
||||
window.removeEventListener('scroll', checkScroll) |
||||
} |
||||
}) |
||||
|
||||
const isNavbarOpen = ref(false) |
||||
const toggleNavbar = (open: boolean) => { |
||||
isNavbarOpen.value = !open |
||||
} |
||||
|
||||
function onNavClick(nav: any, menuOpen: boolean) { |
||||
if (!!nav.children?.length) { |
||||
return |
||||
} |
||||
router.push(nav.path) |
||||
isNavbarOpen.value = false |
||||
} |
||||
|
||||
const navItems = [ |
||||
{ |
||||
name: '首页', |
||||
path: '/', |
||||
}, |
||||
{ |
||||
name: '产品中心', |
||||
path: '/products', |
||||
children: [ |
||||
{ |
||||
name: 'EM系列储能边缘智能网关', |
||||
path: '/products/em-series-energy-storage-edge-intelligent-gateway', |
||||
}, |
||||
{ |
||||
name: 'EM系列储能边缘智能网关', |
||||
path: '/products/em-series-energy-storage-edge-intelligent-gateway', |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
name: '技术资料', |
||||
path: '/technical-materials', |
||||
}, |
||||
{ |
||||
name: '在线商城', |
||||
path: '/online-store', |
||||
}, |
||||
{ |
||||
name: '关于我们', |
||||
path: '/about', |
||||
}, |
||||
] |
||||
</script> |
||||
<style scoped> |
||||
.header-bg { |
||||
@apply bg-[rgba(92,92,92,0.4)] bg-gradient-to-r from-black/40 to-black/40; |
||||
} |
||||
|
||||
/* 子菜单展开收缩动画 */ |
||||
.transition-all { |
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); |
||||
} |
||||
|
||||
/* CSS */ |
||||
.navbar-control { |
||||
cursor: pointer; |
||||
height: 100%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 6px; |
||||
justify-content: center; |
||||
} |
||||
|
||||
.control-icon { |
||||
transition: all 0.3s ease; |
||||
} |
||||
|
||||
/* 每个 control-icon 的伪元素 */ |
||||
.control-icon:before { |
||||
background-color: #fff; |
||||
border-bottom-left-radius: 1px; |
||||
border-top-right-radius: 1px; |
||||
content: ''; |
||||
display: block; |
||||
height: 1px; |
||||
transition: |
||||
transform 0.3s ease 0.2s, |
||||
background-color 0.4s ease 0s; |
||||
width: 22px; |
||||
opacity: 0.9; |
||||
} |
||||
.navbar-control.open .control-icon:before { |
||||
/* background-color: #000; */ |
||||
} |
||||
.navbar-control.open .control-icon:nth-child(1) { |
||||
transform: translateY(7px); |
||||
} |
||||
.navbar-control.open .control-icon:nth-child(2) { |
||||
opacity: 0; |
||||
transition-duration: 0.3s; |
||||
} |
||||
|
||||
.navbar-control .control-icon:nth-child(2) { |
||||
transition-duration: 0.6s; |
||||
} |
||||
|
||||
.navbar-control.open .control-icon:nth-child(3) { |
||||
transform: translateY(-7px); |
||||
} |
||||
|
||||
/* open 状态下的动画 */ |
||||
.navbar-control.open .control-icon:nth-child(1):before { |
||||
transform: rotate(45deg); |
||||
} |
||||
.navbar-control.open .control-icon:nth-child(3):before { |
||||
transform: rotate(-45deg); |
||||
} |
||||
</style> |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
import type { Updater } from '@tanstack/vue-table' |
||||
import type { Ref } from 'vue' |
||||
import { type ClassValue, clsx } from 'clsx' |
||||
import { twMerge } from 'tailwind-merge' |
||||
|
||||
export function cn(...inputs: ClassValue[]) { |
||||
return twMerge(clsx(inputs)) |
||||
} |
||||
|
||||
export function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) { |
||||
ref.value |
||||
= typeof updaterOrValue === 'function' |
||||
? updaterOrValue(ref.value) |
||||
: updaterOrValue |
||||
} |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
export default defineNuxtConfig({ |
||||
compatibilityDate: '2024-11-01', |
||||
modules: ['motion-v/nuxt', '@nuxtjs/tailwindcss', 'shadcn-nuxt', '@nuxt/icon'], |
||||
devtools: { enabled: true }, |
||||
postcss: { |
||||
plugins: { |
||||
tailwindcss: {}, |
||||
autoprefixer: {}, |
||||
}, |
||||
}, |
||||
css: ['~/assets/styles/main.css'], |
||||
vite: { |
||||
plugins: [ |
||||
], |
||||
}, |
||||
app: { |
||||
head: { |
||||
title: 'EM系列储能边缘智能网关-比特电科', |
||||
meta: [ |
||||
|
||||
{ |
||||
name: 'description', |
||||
content: 'EM系列储能边缘智能网关-比特电科', |
||||
}, |
||||
{ |
||||
name: 'keywords', |
||||
content: 'EM系列储能边缘智能网关-比特电科', |
||||
}, |
||||
{ |
||||
name: 'viewport', |
||||
content: 'width=device-width, initial-scale=1.0', |
||||
}, |
||||
], |
||||
link: [ |
||||
{ |
||||
rel: 'icon', |
||||
type: 'image/x-icon', |
||||
href: '/favicon.ico', |
||||
}, |
||||
], |
||||
noscript: [ |
||||
{ |
||||
innerHTML: 'JavaScript is required', |
||||
}, |
||||
], |
||||
}, |
||||
}, |
||||
devServer: { |
||||
port: 3300, // 设置开发服务器端口
|
||||
host: '0.0.0.0', // 可选:允许外部访问
|
||||
}, |
||||
}) |
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
{ |
||||
"name": "nuxt-app", |
||||
"private": true, |
||||
"type": "module", |
||||
"scripts": { |
||||
"build": "nuxt build", |
||||
"dev": "nuxt dev", |
||||
"generate": "nuxt generate", |
||||
"preview": "nuxt preview", |
||||
"postinstall": "nuxt prepare" |
||||
}, |
||||
"dependencies": { |
||||
"@headlessui/vue": "^1.7.23", |
||||
"@vueuse/core": "^13.0.0", |
||||
"class-variance-authority": "^0.7.1", |
||||
"lucide-vue-next": "^0.486.0", |
||||
"motion-v": "1.0.0-alpha.1", |
||||
"nuxt": "^3.16.1", |
||||
"reka-ui": "^2.1.1", |
||||
"shadcn-nuxt": "1.0.3", |
||||
"simplex-noise": "^4.0.3", |
||||
"vue": "^3.5.13", |
||||
"vue-router": "^4.5.0" |
||||
}, |
||||
"devDependencies": { |
||||
"@iconify-json/mingcute": "^1.2.3", |
||||
"@inspira-ui/plugins": "^0.0.1", |
||||
"@nuxt/icon": "^1.11.0", |
||||
"@nuxtjs/tailwindcss": "^6.13.2", |
||||
"autoprefixer": "^10.4.21", |
||||
"clsx": "^2.1.1", |
||||
"postcss": "^8.5.3", |
||||
"prettier": "^3.5.3", |
||||
"prettier-plugin-tailwindcss": "^0.6.11", |
||||
"tailwind-merge": "^3.1.0", |
||||
"tailwindcss-animate": "^1.0.7" |
||||
}, |
||||
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a" |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<template> |
||||
<div> |
||||
<h1>404</h1> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"></script> |
||||
|
||||
<style scoped></style> |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
<template> |
||||
<div></div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
useSeoMeta({ |
||||
title: '关于我们-比特电科', |
||||
description: '关于我们-比特电科', |
||||
}) |
||||
</script> |
||||
|
||||
<style scoped></style> |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
<template> |
||||
<main class="h-screen w-full"> |
||||
<img src="../assets/images/bg.jpg" class="h-[32rem] w-full object-cover" /> |
||||
</main> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' |
||||
</script> |
||||
|
||||
<style scoped></style> |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
<template> |
||||
<div></div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
useSeoMeta({ |
||||
title: '产品中心-比特电科', |
||||
description: '产品中心-比特电科', |
||||
}) |
||||
</script> |
||||
|
||||
<style scoped></style> |
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
{ |
||||
"extends": "../.nuxt/tsconfig.server.json" |
||||
} |
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
import animate from 'tailwindcss-animate' |
||||
import { setupInspiraUI } from '@inspira-ui/plugins' |
||||
|
||||
/** @type {import('tailwindcss').Config} */ |
||||
export default { |
||||
darkMode: ['selector', 'class'], |
||||
safelist: ['dark'], |
||||
content: [ |
||||
'./components/**/*.{js,vue,ts}', |
||||
'./layouts/**/*.vue', |
||||
'./pages/**/*.vue', |
||||
'./plugins/**/*.{js,ts}', |
||||
'./app.vue', |
||||
'./error.vue', |
||||
], |
||||
theme: { |
||||
extend: { |
||||
screens: { |
||||
sm: { |
||||
max: '479px', |
||||
}, |
||||
md: { |
||||
min: '480px', |
||||
max: '760px', |
||||
}, |
||||
}, |
||||
spacing: { |
||||
13: '3.25rem', |
||||
15: '3.75rem', |
||||
17: '4.25rem', |
||||
19: '4.75rem', |
||||
21: '5.25rem', |
||||
23: '5.75rem', |
||||
25: '6.25rem', |
||||
}, |
||||
lineHeight: { |
||||
13: '3.25rem', |
||||
14: '3.5rem', |
||||
15: '3.75rem', |
||||
16: '4rem', |
||||
}, |
||||
|
||||
colors: { |
||||
border: 'hsl(var(--border))', |
||||
input: 'hsl(var(--input))', |
||||
ring: 'hsl(var(--ring))', |
||||
background: 'hsl(var(--background))', |
||||
foreground: 'hsl(var(--foreground))', |
||||
primary: { |
||||
DEFAULT: 'hsl(var(--primary))', |
||||
foreground: 'hsl(var(--primary-foreground))', |
||||
}, |
||||
secondary: { |
||||
DEFAULT: 'hsl(var(--secondary))', |
||||
foreground: 'hsl(var(--secondary-foreground))', |
||||
}, |
||||
destructive: { |
||||
DEFAULT: 'hsl(var(--destructive))', |
||||
foreground: 'hsl(var(--destructive-foreground))', |
||||
}, |
||||
muted: { |
||||
DEFAULT: 'hsl(var(--muted))', |
||||
foreground: 'hsl(var(--muted-foreground))', |
||||
}, |
||||
accent: { |
||||
DEFAULT: 'hsl(var(--accent))', |
||||
foreground: 'hsl(var(--accent-foreground))', |
||||
}, |
||||
popover: { |
||||
DEFAULT: 'hsl(var(--popover))', |
||||
foreground: 'hsl(var(--popover-foreground))', |
||||
}, |
||||
card: { |
||||
DEFAULT: 'hsl(var(--card))', |
||||
foreground: 'hsl(var(--card-foreground))', |
||||
}, |
||||
chart: { |
||||
1: 'hsl(var(--chart-1))', |
||||
2: 'hsl(var(--chart-2))', |
||||
3: 'hsl(var(--chart-3))', |
||||
4: 'hsl(var(--chart-4))', |
||||
5: 'hsl(var(--chart-5))', |
||||
}, |
||||
}, |
||||
borderRadius: { |
||||
xl: 'calc(var(--radius) + 4px)', |
||||
lg: 'var(--radius)', |
||||
md: 'calc(var(--radius) - 2px)', |
||||
sm: 'calc(var(--radius) - 4px)', |
||||
}, |
||||
}, |
||||
}, |
||||
plugins: [animate, setupInspiraUI, require('tailwindcss-animate')], |
||||
} |
Loading…
Reference in new issue