feat: InspiraUI
This commit is contained in:
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
// https://oxc.rs/docs/guide/usage/linter/rules/unicorn/no-new-array.html
|
// https://oxc.rs/docs/guide/usage/linter/rules/unicorn/no-new-array.html
|
||||||
// "rules": {
|
"rules": {
|
||||||
// "unicorn/no-new-array": "warn"
|
"unicorn/no-useless-spread": "off"
|
||||||
// },
|
},
|
||||||
"ignorePatterns": ["src/shadcn/**", "src/components/InspiraUI/**"]
|
"ignorePatterns": ["src/shadcn/**", "src/components/InspiraUI/**"]
|
||||||
}
|
}
|
||||||
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -3,9 +3,9 @@
|
|||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.fixAll.stylelint": "explicit",
|
"source.fixAll.stylelint": "explicit",
|
||||||
"source.fixAll.oxc": "explicit",
|
"source.fixAll.oxc": "explicit",
|
||||||
"source.fixAll.eslint": "never",
|
// "source.fixAll.eslint": "never",
|
||||||
"source.fixAll.stylelint": "never",
|
// "source.fixAll.stylelint": "never",
|
||||||
"source.fixAll.oxc": "never",
|
// "source.fixAll.oxc": "never",
|
||||||
"source.organizeImports": "never"
|
"source.organizeImports": "never"
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
|
@ -89,6 +89,8 @@
|
|||||||
"@eslint/compat": "^1.2.7",
|
"@eslint/compat": "^1.2.7",
|
||||||
"@faker-js/faker": "^9.6.0",
|
"@faker-js/faker": "^9.6.0",
|
||||||
"@iconify-json/carbon": "^1.2.8",
|
"@iconify-json/carbon": "^1.2.8",
|
||||||
|
"@iconify-json/logos": "^1.2.4",
|
||||||
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@iconify/utils": "^2.3.0",
|
"@iconify/utils": "^2.3.0",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@primevue/auto-import-resolver": "^4.3.3",
|
"@primevue/auto-import-resolver": "^4.3.3",
|
||||||
|
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@ -149,6 +149,12 @@ importers:
|
|||||||
'@iconify-json/carbon':
|
'@iconify-json/carbon':
|
||||||
specifier: ^1.2.8
|
specifier: ^1.2.8
|
||||||
version: 1.2.8
|
version: 1.2.8
|
||||||
|
'@iconify-json/logos':
|
||||||
|
specifier: ^1.2.4
|
||||||
|
version: 1.2.4
|
||||||
|
'@iconify-json/mdi':
|
||||||
|
specifier: ^1.2.3
|
||||||
|
version: 1.2.3
|
||||||
'@iconify/utils':
|
'@iconify/utils':
|
||||||
specifier: ^2.3.0
|
specifier: ^2.3.0
|
||||||
version: 2.3.0
|
version: 2.3.0
|
||||||
@ -915,6 +921,12 @@ packages:
|
|||||||
'@iconify-json/carbon@1.2.8':
|
'@iconify-json/carbon@1.2.8':
|
||||||
resolution: {integrity: sha512-6xh4YiFBz6qoSnB3XMe23WvjTJroDFXB17J1MbiT7nATFe+70+em1acRXr8hgP/gYpwFMHFc4IvjA/IPTPnTzg==}
|
resolution: {integrity: sha512-6xh4YiFBz6qoSnB3XMe23WvjTJroDFXB17J1MbiT7nATFe+70+em1acRXr8hgP/gYpwFMHFc4IvjA/IPTPnTzg==}
|
||||||
|
|
||||||
|
'@iconify-json/logos@1.2.4':
|
||||||
|
resolution: {integrity: sha512-XC4If5D/hbaZvUkTV8iaZuGlQCyG6CNOlaAaJaGa13V5QMYwYjgtKk3vPP8wz3wtTVNVEVk3LRx1fOJz+YnSMw==}
|
||||||
|
|
||||||
|
'@iconify-json/mdi@1.2.3':
|
||||||
|
resolution: {integrity: sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==}
|
||||||
|
|
||||||
'@iconify/types@2.0.0':
|
'@iconify/types@2.0.0':
|
||||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||||
|
|
||||||
@ -5314,6 +5326,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
|
'@iconify-json/logos@1.2.4':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
|
'@iconify-json/mdi@1.2.3':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
'@iconify/types@2.0.0': {}
|
'@iconify/types@2.0.0': {}
|
||||||
|
|
||||||
'@iconify/utils@2.3.0':
|
'@iconify/utils@2.3.0':
|
||||||
|
@ -20,7 +20,6 @@ const themeConfig = computed(() => {
|
|||||||
</a-config-provider>
|
</a-config-provider>
|
||||||
|
|
||||||
<DynamicDialog /> <ConfirmDialog /> <Toast />
|
<DynamicDialog /> <ConfirmDialog /> <Toast />
|
||||||
<FluidCursor />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,22 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="bubbleParentContainer"
|
||||||
|
class="relative h-72 w-full overflow-hidden"
|
||||||
|
>
|
||||||
|
<div ref="bubbleCanvasContainer"></div>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'--bubbles-blur': `${blur}px`,
|
||||||
|
}"
|
||||||
|
class="absolute inset-0 z-[2] size-full backdrop-blur-[--bubbles-blur]"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
Clock,
|
|
||||||
Color,
|
|
||||||
MathUtils,
|
|
||||||
Mesh,
|
|
||||||
PerspectiveCamera,
|
|
||||||
Scene,
|
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
SphereGeometry,
|
SphereGeometry,
|
||||||
Vector3,
|
Vector3,
|
||||||
|
Color,
|
||||||
|
MathUtils,
|
||||||
|
Mesh,
|
||||||
|
Clock,
|
||||||
WebGLRenderer,
|
WebGLRenderer,
|
||||||
} from 'three';
|
Scene,
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
PerspectiveCamera,
|
||||||
|
} from "three";
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
blur: {
|
blur: {
|
||||||
default: 0,
|
|
||||||
type: Number,
|
type: Number,
|
||||||
|
default: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -42,14 +59,16 @@ const SPHERE_COUNT = 250;
|
|||||||
const SPHERE_SCALE_COEFF = 3;
|
const SPHERE_SCALE_COEFF = 3;
|
||||||
const ORBIT_MIN = SPHERE_SCALE_COEFF + 2;
|
const ORBIT_MIN = SPHERE_SCALE_COEFF + 2;
|
||||||
const ORBIT_MAX = ORBIT_MIN + 10;
|
const ORBIT_MAX = ORBIT_MIN + 10;
|
||||||
const RAND_SEED = 898_211_544;
|
const RAND_SEED = 898211544;
|
||||||
|
|
||||||
const rand = seededRandom(RAND_SEED);
|
const rand = seededRandom(RAND_SEED);
|
||||||
|
|
||||||
const { cos, PI, sin } = Math;
|
const { PI, cos, sin } = Math;
|
||||||
const PI2 = PI * 2;
|
const PI2 = PI * 2;
|
||||||
const sizes = new Array(SPHERE_COUNT).fill(0).map(() => randRange(1) * Math.pow(randRange(), 3));
|
const sizes = new Array(SPHERE_COUNT).fill(0).map(() => randRange(1) * Math.pow(randRange(), 3));
|
||||||
const orbitRadii = new Array(SPHERE_COUNT).fill(0).map(() => MathUtils.lerp(ORBIT_MIN, ORBIT_MAX, randRange()));
|
const orbitRadii = new Array(SPHERE_COUNT)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => MathUtils.lerp(ORBIT_MIN, ORBIT_MAX, randRange()));
|
||||||
const thetas = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
|
const thetas = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
|
||||||
const phis = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
|
const phis = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
|
||||||
const positions: [number, number, number][] = orbitRadii.map((rad, i) => [
|
const positions: [number, number, number][] = orbitRadii.map((rad, i) => [
|
||||||
@ -76,28 +95,90 @@ const bgMaterial = getGradientMaterial(
|
|||||||
);
|
);
|
||||||
bgMaterial.uniforms.uTemperatureVariancePeriod.value = new Vector3(0, 0, 0.1);
|
bgMaterial.uniforms.uTemperatureVariancePeriod.value = new Vector3(0, 0, 0.1);
|
||||||
|
|
||||||
function animate() {
|
function seededRandom(a: number) {
|
||||||
requestAnimationFrame(animate);
|
return function () {
|
||||||
|
a |= 0;
|
||||||
const elapsed = clock.getElapsedTime();
|
a = (a + 0x9e3779b9) | 0;
|
||||||
const temperature = sin(elapsed * 0.5) * 0.5 + 0.5;
|
var t = a ^ (a >>> 16);
|
||||||
|
t = Math.imul(t, 0x21f0aaad);
|
||||||
bgMaterial.uniforms.uTemperature.value = temperature;
|
t = t ^ (t >>> 15);
|
||||||
bgMaterial.uniforms.uElapsedTime.value = elapsed;
|
t = Math.imul(t, 0x735a2d97);
|
||||||
|
return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296;
|
||||||
sphereMaterial.uniforms.uTemperature.value = temperature;
|
};
|
||||||
sphereMaterial.uniforms.uElapsedTime.value = elapsed;
|
|
||||||
|
|
||||||
// Floating effect for spheres
|
|
||||||
for (const [index, sphere] of spheres.entries()) {
|
|
||||||
const basePosition = positions[index];
|
|
||||||
const floatFactor = 2; // Adjust this value to control float intensity
|
|
||||||
const speed = 0.3; // Adjust this value to control float speed
|
|
||||||
const floatY = sin(elapsed * speed + index) * floatFactor;
|
|
||||||
sphere.position.y = basePosition[1] + floatY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.render(scene, camera);
|
function randRange(n = 1) {
|
||||||
|
return rand() * n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgb(r: number, g: number, b: number) {
|
||||||
|
return new Color(r / 255, g / 255, b / 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGradientMaterial(
|
||||||
|
colorBottomWarm: Color,
|
||||||
|
colorTopWarm: Color,
|
||||||
|
colorBottomCool: Color,
|
||||||
|
colorTopCool: Color,
|
||||||
|
) {
|
||||||
|
return new ShaderMaterial({
|
||||||
|
uniforms: {
|
||||||
|
colorBottomWarm: {
|
||||||
|
value: new Color().copy(colorBottomWarm),
|
||||||
|
},
|
||||||
|
colorTopWarm: {
|
||||||
|
value: new Color().copy(colorTopWarm),
|
||||||
|
},
|
||||||
|
colorBottomCool: {
|
||||||
|
value: new Color().copy(colorBottomCool),
|
||||||
|
},
|
||||||
|
colorTopCool: {
|
||||||
|
value: new Color().copy(colorTopCool),
|
||||||
|
},
|
||||||
|
uTemperature: {
|
||||||
|
value: 0.0,
|
||||||
|
},
|
||||||
|
uTemperatureVariancePeriod: {
|
||||||
|
value: new Vector3(0.08, 0.1, 0.2),
|
||||||
|
},
|
||||||
|
uElapsedTime: {
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vertexShader: `
|
||||||
|
uniform vec4 uTemperatureVariancePeriod;
|
||||||
|
uniform float uTemperature;
|
||||||
|
uniform float uElapsedTime;
|
||||||
|
varying float topBottomMix;
|
||||||
|
varying float warmCoolMix;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
|
||||||
|
topBottomMix = normal.y;
|
||||||
|
warmCoolMix = 0.6 * uTemperature +
|
||||||
|
0.4 * (sin(
|
||||||
|
(uElapsedTime + gl_Position.x) * uTemperatureVariancePeriod.x +
|
||||||
|
(uElapsedTime + gl_Position.y) * uTemperatureVariancePeriod.y +
|
||||||
|
(uElapsedTime + gl_Position.z) * uTemperatureVariancePeriod.z) * 0.5 + 0.5);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fragmentShader: `
|
||||||
|
uniform vec3 colorBottomWarm;
|
||||||
|
uniform vec3 colorTopWarm;
|
||||||
|
uniform vec3 colorBottomCool;
|
||||||
|
uniform vec3 colorTopCool;
|
||||||
|
|
||||||
|
varying float topBottomMix;
|
||||||
|
varying float warmCoolMix;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = vec4(mix(
|
||||||
|
mix(colorTopCool, colorTopWarm, warmCoolMix),
|
||||||
|
mix(colorBottomCool, colorBottomWarm, warmCoolMix),
|
||||||
|
topBottomMix), 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createScene() {
|
function createScene() {
|
||||||
@ -119,7 +200,7 @@ function createScene() {
|
|||||||
sphereMaterial.depthTest = true; // Keep this true for depth sorting
|
sphereMaterial.depthTest = true; // Keep this true for depth sorting
|
||||||
|
|
||||||
if (bubbleCanvasContainer.value) {
|
if (bubbleCanvasContainer.value) {
|
||||||
bubbleCanvasContainer.value.append(renderer.domElement);
|
bubbleCanvasContainer.value.appendChild(renderer.domElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the background mesh
|
// Create the background mesh
|
||||||
@ -138,12 +219,18 @@ function createScene() {
|
|||||||
const frustumWidth = frustumHeight * aspect;
|
const frustumWidth = frustumHeight * aspect;
|
||||||
|
|
||||||
// Scale the background geometry to match the camera's frustum size
|
// Scale the background geometry to match the camera's frustum size
|
||||||
bgMesh.scale.set(frustumWidth / bgGeometry.parameters.radius, frustumHeight / bgGeometry.parameters.radius, 1);
|
bgMesh.scale.set(
|
||||||
|
frustumWidth / bgGeometry.parameters.radius,
|
||||||
|
frustumHeight / bgGeometry.parameters.radius,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
scene.add(bgMesh); // Add the backgrou
|
scene.add(bgMesh); // Add the backgrou
|
||||||
|
|
||||||
// Create sphere meshes
|
// Create sphere meshes
|
||||||
const orbitRadii = new Array(SPHERE_COUNT).fill(0).map(() => MathUtils.lerp(ORBIT_MIN, ORBIT_MAX, randRange()));
|
const orbitRadii = new Array(SPHERE_COUNT)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => MathUtils.lerp(ORBIT_MIN, ORBIT_MAX, randRange()));
|
||||||
const thetas = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
|
const thetas = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
|
||||||
const phis = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
|
const phis = new Array(SPHERE_COUNT).fill(0).map(() => randRange(PI2));
|
||||||
const positions = orbitRadii.map((rad, i) => [
|
const positions = orbitRadii.map((rad, i) => [
|
||||||
@ -165,85 +252,28 @@ function createScene() {
|
|||||||
clock = new Clock();
|
clock = new Clock();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGradientMaterial(colorBottomWarm: Color, colorTopWarm: Color, colorBottomCool: Color, colorTopCool: Color) {
|
function animate() {
|
||||||
return new ShaderMaterial({
|
requestAnimationFrame(animate);
|
||||||
fragmentShader: `
|
|
||||||
uniform vec3 colorBottomWarm;
|
|
||||||
uniform vec3 colorTopWarm;
|
|
||||||
uniform vec3 colorBottomCool;
|
|
||||||
uniform vec3 colorTopCool;
|
|
||||||
|
|
||||||
varying float topBottomMix;
|
const elapsed = clock.getElapsedTime();
|
||||||
varying float warmCoolMix;
|
const temperature = sin(elapsed * 0.5) * 0.5 + 0.5;
|
||||||
|
|
||||||
void main() {
|
bgMaterial.uniforms.uTemperature.value = temperature;
|
||||||
gl_FragColor = vec4(mix(
|
bgMaterial.uniforms.uElapsedTime.value = elapsed;
|
||||||
mix(colorTopCool, colorTopWarm, warmCoolMix),
|
|
||||||
mix(colorBottomCool, colorBottomWarm, warmCoolMix),
|
|
||||||
topBottomMix), 1.0);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
uniforms: {
|
|
||||||
colorBottomCool: {
|
|
||||||
value: new Color().copy(colorBottomCool),
|
|
||||||
},
|
|
||||||
colorBottomWarm: {
|
|
||||||
value: new Color().copy(colorBottomWarm),
|
|
||||||
},
|
|
||||||
colorTopCool: {
|
|
||||||
value: new Color().copy(colorTopCool),
|
|
||||||
},
|
|
||||||
colorTopWarm: {
|
|
||||||
value: new Color().copy(colorTopWarm),
|
|
||||||
},
|
|
||||||
uElapsedTime: {
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
uTemperature: {
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
uTemperatureVariancePeriod: {
|
|
||||||
value: new Vector3(0.08, 0.1, 0.2),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
vertexShader: `
|
|
||||||
uniform vec4 uTemperatureVariancePeriod;
|
|
||||||
uniform float uTemperature;
|
|
||||||
uniform float uElapsedTime;
|
|
||||||
varying float topBottomMix;
|
|
||||||
varying float warmCoolMix;
|
|
||||||
|
|
||||||
void main() {
|
sphereMaterial.uniforms.uTemperature.value = temperature;
|
||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
|
sphereMaterial.uniforms.uElapsedTime.value = elapsed;
|
||||||
topBottomMix = normal.y;
|
|
||||||
warmCoolMix = 0.6 * uTemperature +
|
// Floating effect for spheres
|
||||||
0.4 * (sin(
|
spheres.forEach((sphere, index) => {
|
||||||
(uElapsedTime + gl_Position.x) * uTemperatureVariancePeriod.x +
|
const basePosition = positions[index];
|
||||||
(uElapsedTime + gl_Position.y) * uTemperatureVariancePeriod.y +
|
const floatFactor = 2; // Adjust this value to control float intensity
|
||||||
(uElapsedTime + gl_Position.z) * uTemperatureVariancePeriod.z) * 0.5 + 0.5);
|
const speed = 0.3; // Adjust this value to control float speed
|
||||||
}
|
const floatY = sin(elapsed * speed + index) * floatFactor;
|
||||||
`,
|
sphere.position.y = basePosition[1] + floatY;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function randRange(n = 1) {
|
renderer.render(scene, camera);
|
||||||
return rand() * n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rgb(r: number, g: number, b: number) {
|
|
||||||
return new Color(r / 255, g / 255, b / 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
function seededRandom(a: number) {
|
|
||||||
return function () {
|
|
||||||
a = Math.trunc(a);
|
|
||||||
a = Math.trunc(a + 0x9e_37_79_b9);
|
|
||||||
let t = a ^ (a >>> 16);
|
|
||||||
t = Math.imul(t, 0x21_f0_aa_ad);
|
|
||||||
t = t ^ (t >>> 15);
|
|
||||||
t = Math.imul(t, 0x73_5a_2d_97);
|
|
||||||
return ((t = t ^ (t >>> 15)) >>> 0) / 4_294_967_296;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRendererSize() {
|
function updateRendererSize() {
|
||||||
@ -261,34 +291,26 @@ function updateRendererSize() {
|
|||||||
const frustumWidth = frustumHeight * camera.aspect;
|
const frustumWidth = frustumHeight * camera.aspect;
|
||||||
|
|
||||||
// Get the background mesh and update its scale
|
// Get the background mesh and update its scale
|
||||||
const bgMesh = scene.children.find((obj) => obj instanceof Mesh && obj.geometry === bgGeometry) as Mesh;
|
const bgMesh = scene.children.find(
|
||||||
|
(obj) => obj instanceof Mesh && obj.geometry === bgGeometry,
|
||||||
|
) as Mesh;
|
||||||
if (bgMesh) {
|
if (bgMesh) {
|
||||||
bgMesh.scale.set(frustumWidth / bgGeometry.parameters.radius, frustumHeight / bgGeometry.parameters.radius, 1);
|
bgMesh.scale.set(
|
||||||
|
frustumWidth / bgGeometry.parameters.radius,
|
||||||
|
frustumHeight / bgGeometry.parameters.radius,
|
||||||
|
1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
createScene();
|
createScene();
|
||||||
updateRendererSize();
|
updateRendererSize();
|
||||||
window.addEventListener('resize', updateRendererSize);
|
window.addEventListener("resize", updateRendererSize);
|
||||||
animate();
|
animate();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('resize', updateRendererSize); // Cleanup on component unmount
|
window.removeEventListener("resize", updateRendererSize); // Cleanup on component unmount
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<div ref="bubbleParentContainer" class="relative h-72 w-full overflow-hidden">
|
|
||||||
<div ref="bubbleCanvasContainer"></div>
|
|
||||||
<div
|
|
||||||
:style="{
|
|
||||||
'--bubbles-blur': `${blur}px`,
|
|
||||||
}"
|
|
||||||
class="absolute inset-0 z-[2] size-full backdrop-blur-[--bubbles-blur]"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
1
src/components/InspiraUI/bg-bubbles/index.ts
Normal file
1
src/components/InspiraUI/bg-bubbles/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as BubblesBg } from "./BubblesBg.vue";
|
@ -1,22 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<canvas
|
||||||
|
ref="starsCanvas"
|
||||||
|
:class="cn('absolute inset-0 w-full h-full', $props.class)"
|
||||||
|
></canvas>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from '@/shadcn/lib/utils';
|
import { cn } from "@/shadcn/lib/utils";
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from "vue";
|
||||||
|
|
||||||
interface Star {
|
interface Star {
|
||||||
speed: number;
|
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
z: number;
|
z: number;
|
||||||
|
speed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
class?: string;
|
|
||||||
color?: string;
|
color?: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
|
class?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
color: '#FFF',
|
color: "#FFF",
|
||||||
count: 200,
|
count: 200,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -30,7 +37,7 @@ onMounted(() => {
|
|||||||
const canvas = starsCanvas.value;
|
const canvas = starsCanvas.value;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
|
||||||
window.addEventListener('resize', resizeCanvas);
|
window.addEventListener("resize", resizeCanvas);
|
||||||
resizeCanvas(); // Call it initially to set correct size
|
resizeCanvas(); // Call it initially to set correct size
|
||||||
|
|
||||||
perspective = canvas.width / 2;
|
perspective = canvas.width / 2;
|
||||||
@ -39,41 +46,39 @@ onMounted(() => {
|
|||||||
// Initialize stars
|
// Initialize stars
|
||||||
for (let i = 0; i < props.count; i++) {
|
for (let i = 0; i < props.count; i++) {
|
||||||
stars.push({
|
stars.push({
|
||||||
speed: Math.random() * 5 + 2, // Speed for falling effect
|
|
||||||
x: (Math.random() - 0.5) * 2 * canvas.width,
|
x: (Math.random() - 0.5) * 2 * canvas.width,
|
||||||
y: (Math.random() - 0.5) * 2 * canvas.height,
|
y: (Math.random() - 0.5) * 2 * canvas.height,
|
||||||
z: Math.random() * canvas.width,
|
z: Math.random() * canvas.width,
|
||||||
|
speed: Math.random() * 5 + 2, // Speed for falling effect
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
animate(); // Start animation
|
animate(); // Start animation
|
||||||
});
|
});
|
||||||
|
|
||||||
// Function to animate the stars
|
function hexToRgb() {
|
||||||
function animate() {
|
let hex = props.color.replace(/^#/, "");
|
||||||
const canvas = starsCanvas.value;
|
|
||||||
if (!canvas) return;
|
|
||||||
|
|
||||||
ctx = canvas.getContext('2d');
|
// If the hex code is 3 characters, expand it to 6 characters
|
||||||
if (!ctx) return;
|
if (hex.length === 3) {
|
||||||
|
hex = hex
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear canvas for each frame
|
.split("")
|
||||||
|
.map((char) => char + char)
|
||||||
for (const star of stars) {
|
.join("");
|
||||||
drawStar(star);
|
|
||||||
|
|
||||||
// Move star towards the screen (decrease z)
|
|
||||||
star.z -= star.speed;
|
|
||||||
|
|
||||||
// Reset star when it reaches the viewer (z = 0)
|
|
||||||
if (star.z <= 0) {
|
|
||||||
star.z = canvas.width;
|
|
||||||
star.x = (Math.random() - 0.5) * 2 * canvas.width;
|
|
||||||
star.y = (Math.random() - 0.5) * 2 * canvas.height;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(animate); // Continue animation
|
// 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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to draw a star with a sharp line and blurred trail
|
// Function to draw a star with a sharp line and blurred trail
|
||||||
@ -81,7 +86,7 @@ function drawStar(star: Star) {
|
|||||||
const canvas = starsCanvas.value;
|
const canvas = starsCanvas.value;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
|
||||||
ctx = canvas.getContext('2d');
|
ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
const scale = perspective / (perspective + star.z); // 3D perspective scale
|
const scale = perspective / (perspective + star.z); // 3D perspective scale
|
||||||
@ -123,26 +128,31 @@ function drawStar(star: Star) {
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hexToRgb() {
|
// Function to animate the stars
|
||||||
let hex = props.color.replace(/^#/, '');
|
function animate() {
|
||||||
|
const canvas = starsCanvas.value;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
// If the hex code is 3 characters, expand it to 6 characters
|
ctx = canvas.getContext("2d");
|
||||||
if (hex.length === 3) {
|
if (!ctx) return;
|
||||||
hex = [...hex].map((char) => char + char).join('');
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear canvas for each frame
|
||||||
|
|
||||||
|
stars.forEach((star) => {
|
||||||
|
drawStar(star);
|
||||||
|
|
||||||
|
// Move star towards the screen (decrease z)
|
||||||
|
star.z -= star.speed;
|
||||||
|
|
||||||
|
// Reset star when it reaches the viewer (z = 0)
|
||||||
|
if (star.z <= 0) {
|
||||||
|
star.z = canvas.width;
|
||||||
|
star.x = (Math.random() - 0.5) * 2 * canvas.width;
|
||||||
|
star.y = (Math.random() - 0.5) * 2 * canvas.height;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Parse the r, g, b values from the hex string
|
requestAnimationFrame(animate); // Continue animation
|
||||||
const bigint = Number.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 {
|
|
||||||
b,
|
|
||||||
g,
|
|
||||||
r,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set canvas to full screen
|
// Set canvas to full screen
|
||||||
@ -154,7 +164,3 @@ function resizeCanvas() {
|
|||||||
canvas.height = canvas.clientHeight;
|
canvas.height = canvas.clientHeight;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<canvas ref="starsCanvas" :class="cn('absolute inset-0 w-full h-full', $props.class)"></canvas>
|
|
||||||
</template>
|
|
1
src/components/InspiraUI/bg-falling-stars/index.ts
Normal file
1
src/components/InspiraUI/bg-falling-stars/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as FallingStarsBg } from "./FallingStarsBg.vue";
|
65
src/components/InspiraUI/border-beam/BorderBeam.vue
Normal file
65
src/components/InspiraUI/border-beam/BorderBeam.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'border-beam',
|
||||||
|
'pointer-events-none absolute inset-0 rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]',
|
||||||
|
'![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]',
|
||||||
|
'after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] animate-border-beam after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from "@/shadcn/lib/utils";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
interface BorderBeamProps {
|
||||||
|
class?: string;
|
||||||
|
size?: number;
|
||||||
|
duration?: number;
|
||||||
|
borderWidth?: number;
|
||||||
|
anchor?: number;
|
||||||
|
colorFrom?: string;
|
||||||
|
colorTo?: string;
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<BorderBeamProps>(), {
|
||||||
|
size: 200,
|
||||||
|
duration: 15000,
|
||||||
|
anchor: 90,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
colorFrom: "#ffaa40",
|
||||||
|
colorTo: "#9c40ff",
|
||||||
|
delay: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationInSeconds = computed(() => `${props.duration}s`);
|
||||||
|
const delayInSeconds = computed(() => `${props.delay}s`);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.border-beam {
|
||||||
|
--size: v-bind(size);
|
||||||
|
--duration: v-bind(durationInSeconds);
|
||||||
|
--anchor: v-bind(anchor);
|
||||||
|
--border-width: v-bind(borderWidth);
|
||||||
|
--color-from: v-bind(colorFrom);
|
||||||
|
--color-to: v-bind(colorTo);
|
||||||
|
--delay: v-bind(delayInSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-border-beam::after {
|
||||||
|
content: "";
|
||||||
|
animation: border-beam-anim var(--duration) infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes border-beam-anim {
|
||||||
|
to {
|
||||||
|
offset-distance: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
1
src/components/InspiraUI/border-beam/index.ts
Normal file
1
src/components/InspiraUI/border-beam/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as BorderBeam } from "./BorderBeam.vue";
|
@ -1,54 +1,3 @@
|
|||||||
<!-- https://inspira-ui.com/components/cards/card-spotlight -->
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { cn } from '@/shadcn/lib/utils';
|
|
||||||
import { computed, type HTMLAttributes, onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
class?: HTMLAttributes['class'];
|
|
||||||
gradientColor?: string;
|
|
||||||
gradientOpacity?: number;
|
|
||||||
gradientSize?: number;
|
|
||||||
slotClass?: HTMLAttributes['class'];
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
class: '',
|
|
||||||
gradientColor: '#262626',
|
|
||||||
gradientOpacity: 0.8,
|
|
||||||
gradientSize: 200,
|
|
||||||
slotClass: '',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mouseX = ref(-props.gradientSize * 10);
|
|
||||||
const mouseY = ref(-props.gradientSize * 10);
|
|
||||||
|
|
||||||
function handleMouseLeave() {
|
|
||||||
mouseX.value = -props.gradientSize * 10;
|
|
||||||
mouseY.value = -props.gradientSize * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMouseMove(e: MouseEvent) {
|
|
||||||
const target = e.currentTarget as HTMLElement;
|
|
||||||
const rect = target.getBoundingClientRect();
|
|
||||||
mouseX.value = e.clientX - rect.left;
|
|
||||||
mouseY.value = e.clientY - rect.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
mouseX.value = -props.gradientSize * 10;
|
|
||||||
mouseY.value = -props.gradientSize * 10;
|
|
||||||
});
|
|
||||||
|
|
||||||
const backgroundStyle = computed(() => {
|
|
||||||
return `radial-gradient(
|
|
||||||
circle at ${mouseX.value}px ${mouseY.value}px,
|
|
||||||
${props.gradientColor} 0%,
|
|
||||||
rgba(0, 0, 0, 0) 70%
|
|
||||||
)`;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
@ -70,3 +19,53 @@ const backgroundStyle = computed(() => {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from "@/shadcn/lib/utils";
|
||||||
|
import { ref, computed, onMounted, type HTMLAttributes } from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
slotClass?: HTMLAttributes["class"];
|
||||||
|
gradientSize?: number;
|
||||||
|
gradientColor?: string;
|
||||||
|
gradientOpacity?: number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
class: "",
|
||||||
|
slotClass: "",
|
||||||
|
gradientSize: 200,
|
||||||
|
gradientColor: "#262626",
|
||||||
|
gradientOpacity: 0.8,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mouseX = ref(-props.gradientSize * 10);
|
||||||
|
const mouseY = ref(-props.gradientSize * 10);
|
||||||
|
|
||||||
|
function handleMouseMove(e: MouseEvent) {
|
||||||
|
const target = e.currentTarget as HTMLElement;
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
mouseX.value = e.clientX - rect.left;
|
||||||
|
mouseY.value = e.clientY - rect.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
mouseX.value = -props.gradientSize * 10;
|
||||||
|
mouseY.value = -props.gradientSize * 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
mouseX.value = -props.gradientSize * 10;
|
||||||
|
mouseY.value = -props.gradientSize * 10;
|
||||||
|
});
|
||||||
|
|
||||||
|
const backgroundStyle = computed(() => {
|
||||||
|
return `radial-gradient(
|
||||||
|
circle at ${mouseX.value}px ${mouseY.value}px,
|
||||||
|
${props.gradientColor} 0%,
|
||||||
|
rgba(0, 0, 0, 0) 70%
|
||||||
|
)`;
|
||||||
|
});
|
||||||
|
</script>
|
1
src/components/InspiraUI/card-spotlight/index.ts
Normal file
1
src/components/InspiraUI/card-spotlight/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as CardSpotlight } from "./CardSpotlight.vue";
|
76
src/components/InspiraUI/dock/Dock.vue
Normal file
76
src/components/InspiraUI/dock/Dock.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="dockRef"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 mx-auto mt-8 flex h-[58px] w-max rounded-2xl border p-2 backdrop-blur-md transition-all gap-4',
|
||||||
|
orientation === 'vertical' && 'flex-col w-[58px] h-max',
|
||||||
|
props.class,
|
||||||
|
dockClass,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@mousemove="onMouseMove"
|
||||||
|
@mouseleave="onMouseLeave"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DataOrientation, Direction } from "./types";
|
||||||
|
import {
|
||||||
|
MOUSE_X_INJECTION_KEY,
|
||||||
|
MOUSE_Y_INJECTION_KEY,
|
||||||
|
MAGNIFICATION_INJECTION_KEY,
|
||||||
|
DISTANCE_INJECTION_KEY,
|
||||||
|
ORIENTATION_INJECTION_KEY,
|
||||||
|
} from "./injectionKeys";
|
||||||
|
import { cn } from "@/shadcn/lib/utils";
|
||||||
|
import type { HTMLAttributes } from "vue";
|
||||||
|
|
||||||
|
interface DockProps {
|
||||||
|
class?: HTMLAttributes["class"];
|
||||||
|
magnification?: number;
|
||||||
|
distance?: number;
|
||||||
|
direction?: Direction;
|
||||||
|
orientation?: DataOrientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<DockProps>(), {
|
||||||
|
magnification: 60,
|
||||||
|
distance: 140,
|
||||||
|
direction: "middle",
|
||||||
|
orientation: "horizontal",
|
||||||
|
});
|
||||||
|
|
||||||
|
const dockRef = ref<HTMLElement | null>(null);
|
||||||
|
const mouseX = ref(Infinity);
|
||||||
|
const mouseY = ref(Infinity);
|
||||||
|
const magnification = computed(() => props.magnification);
|
||||||
|
const distance = computed(() => props.distance);
|
||||||
|
|
||||||
|
const dockClass = computed(() => ({
|
||||||
|
"items-start": props.direction === "top",
|
||||||
|
"items-center": props.direction === "middle",
|
||||||
|
"items-end": props.direction === "bottom",
|
||||||
|
}));
|
||||||
|
|
||||||
|
function onMouseMove(e: MouseEvent) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
mouseX.value = e.pageX;
|
||||||
|
mouseY.value = e.pageY;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseLeave() {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
mouseX.value = Infinity;
|
||||||
|
mouseY.value = Infinity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
provide(MOUSE_X_INJECTION_KEY, mouseX);
|
||||||
|
provide(MOUSE_Y_INJECTION_KEY, mouseY);
|
||||||
|
provide(ORIENTATION_INJECTION_KEY, props.orientation);
|
||||||
|
provide(MAGNIFICATION_INJECTION_KEY, magnification);
|
||||||
|
provide(DISTANCE_INJECTION_KEY, distance);
|
||||||
|
</script>
|
61
src/components/InspiraUI/dock/DockIcon.vue
Normal file
61
src/components/InspiraUI/dock/DockIcon.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="iconRef"
|
||||||
|
class="flex aspect-square cursor-pointer items-center justify-center rounded-full transition-all duration-200 ease-out"
|
||||||
|
:style="{
|
||||||
|
width: `${iconWidth}px`,
|
||||||
|
height: `${iconWidth}px`,
|
||||||
|
}"
|
||||||
|
:hovered="{
|
||||||
|
marginLeft: margin,
|
||||||
|
marginRight: margin,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
MOUSE_X_INJECTION_KEY,
|
||||||
|
MOUSE_Y_INJECTION_KEY,
|
||||||
|
MAGNIFICATION_INJECTION_KEY,
|
||||||
|
DISTANCE_INJECTION_KEY,
|
||||||
|
ORIENTATION_INJECTION_KEY,
|
||||||
|
} from "./injectionKeys";
|
||||||
|
|
||||||
|
const iconRef = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const mouseX = inject(MOUSE_X_INJECTION_KEY, ref(Infinity));
|
||||||
|
const mouseY = inject(MOUSE_Y_INJECTION_KEY, ref(Infinity));
|
||||||
|
const distance = inject(DISTANCE_INJECTION_KEY);
|
||||||
|
const orientation = inject(ORIENTATION_INJECTION_KEY, "vertical");
|
||||||
|
const magnification = inject(MAGNIFICATION_INJECTION_KEY);
|
||||||
|
const isVertical = computed(() => orientation === "vertical");
|
||||||
|
|
||||||
|
const margin = ref(0);
|
||||||
|
|
||||||
|
function calculateDistance(val: number) {
|
||||||
|
if (isVertical.value) {
|
||||||
|
const bounds = iconRef.value?.getBoundingClientRect() || {
|
||||||
|
y: 0,
|
||||||
|
height: 0,
|
||||||
|
};
|
||||||
|
return val - bounds.y - bounds.height / 2;
|
||||||
|
}
|
||||||
|
const bounds = iconRef.value?.getBoundingClientRect() || { x: 0, width: 0 };
|
||||||
|
return val - bounds.x - bounds.width / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconWidth = computed(() => {
|
||||||
|
const distanceCalc = isVertical.value
|
||||||
|
? calculateDistance(mouseY.value)
|
||||||
|
: calculateDistance(mouseX.value);
|
||||||
|
if (!distance?.value || !magnification?.value) return 40;
|
||||||
|
if (Math.abs(distanceCalc) < distance?.value) {
|
||||||
|
return (1 - Math.abs(distanceCalc) / distance?.value) * magnification?.value + 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 40;
|
||||||
|
});
|
||||||
|
</script>
|
14
src/components/InspiraUI/dock/DockSeparator.vue
Normal file
14
src/components/InspiraUI/dock/DockSeparator.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn('relative block bg-secondary', orientation === 'vertical' ? 'w-4/5 h-0.5' : 'h-4/5 w-0.5')
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from "@/shadcn/lib/utils";
|
||||||
|
import { ORIENTATION_INJECTION_KEY } from "./injectionKeys";
|
||||||
|
|
||||||
|
const orientation = inject(ORIENTATION_INJECTION_KEY, "vertical");
|
||||||
|
</script>
|
5
src/components/InspiraUI/dock/index.ts
Normal file
5
src/components/InspiraUI/dock/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { default as Dock } from "./Dock.vue";
|
||||||
|
export { default as DockIcon } from "./DockIcon.vue";
|
||||||
|
export { default as DockSeparator } from "./DockSeparator.vue";
|
||||||
|
|
||||||
|
export type DataOrientation = "vertical" | "horizontal";
|
11
src/components/InspiraUI/dock/injectionKeys.ts
Normal file
11
src/components/InspiraUI/dock/injectionKeys.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { Ref, InjectionKey, ComputedRef } from "vue";
|
||||||
|
import type { DataOrientation } from "./types";
|
||||||
|
|
||||||
|
export const MOUSE_X_INJECTION_KEY = Symbol() as InjectionKey<Ref<number>>;
|
||||||
|
export const MOUSE_Y_INJECTION_KEY = Symbol() as InjectionKey<Ref<number>>;
|
||||||
|
|
||||||
|
export const MAGNIFICATION_INJECTION_KEY = Symbol() as InjectionKey<ComputedRef<number>>;
|
||||||
|
|
||||||
|
export const DISTANCE_INJECTION_KEY = Symbol() as InjectionKey<ComputedRef<number>>;
|
||||||
|
|
||||||
|
export const ORIENTATION_INJECTION_KEY = Symbol() as InjectionKey<DataOrientation>;
|
2
src/components/InspiraUI/dock/types.ts
Normal file
2
src/components/InspiraUI/dock/types.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export type DataOrientation = "vertical" | "horizontal";
|
||||||
|
export type Direction = "top" | "middle" | "bottom";
|
@ -1,34 +1,3 @@
|
|||||||
<!-- https://inspira-ui.com/components/buttons/gradient-button -->
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { cn } from '@/shadcn/lib/utils';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
interface GradientButtonProps {
|
|
||||||
bgColor?: string;
|
|
||||||
blur?: number;
|
|
||||||
borderRadius?: number;
|
|
||||||
borderWidth?: number;
|
|
||||||
class?: string;
|
|
||||||
colors?: string[];
|
|
||||||
duration?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<GradientButtonProps>(), {
|
|
||||||
bgColor: '#000',
|
|
||||||
blur: 4,
|
|
||||||
borderRadius: 8,
|
|
||||||
borderWidth: 2,
|
|
||||||
colors: () => ['#FF0000', '#FFA500', '#FFFF00', '#008000', '#0000FF', '#4B0082', '#EE82EE', '#FF0000'],
|
|
||||||
duration: 2500,
|
|
||||||
});
|
|
||||||
|
|
||||||
const durationInMilliseconds = computed(() => `${props.duration}ms`);
|
|
||||||
const allColors = computed(() => props.colors.join(', '));
|
|
||||||
const borderWidthInPx = computed(() => `${props.borderWidth}px`);
|
|
||||||
const borderRadiusInPx = computed(() => `${props.borderRadius}px`);
|
|
||||||
const blurPx = computed(() => `${props.blur}px`);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
:class="
|
:class="
|
||||||
@ -44,9 +13,48 @@ const blurPx = computed(() => `${props.blur}px`);
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { cn } from "@/shadcn/lib/utils";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
interface GradientButtonProps {
|
||||||
|
borderWidth?: number;
|
||||||
|
colors?: string[];
|
||||||
|
duration?: number;
|
||||||
|
borderRadius?: number;
|
||||||
|
blur?: number;
|
||||||
|
class?: string;
|
||||||
|
bgColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<GradientButtonProps>(), {
|
||||||
|
colors: () => [
|
||||||
|
"#FF0000",
|
||||||
|
"#FFA500",
|
||||||
|
"#FFFF00",
|
||||||
|
"#008000",
|
||||||
|
"#0000FF",
|
||||||
|
"#4B0082",
|
||||||
|
"#EE82EE",
|
||||||
|
"#FF0000",
|
||||||
|
],
|
||||||
|
duration: 2500,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 8,
|
||||||
|
blur: 4,
|
||||||
|
bgColor: "#000",
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationInMilliseconds = computed(() => `${props.duration}ms`);
|
||||||
|
const allColors = computed(() => props.colors.join(", "));
|
||||||
|
const borderWidthInPx = computed(() => `${props.borderWidth}px`);
|
||||||
|
const borderRadiusInPx = computed(() => `${props.borderRadius}px`);
|
||||||
|
const blurPx = computed(() => `${props.blur}px`);
|
||||||
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.animate-rainbow::before {
|
.animate-rainbow::before {
|
||||||
content: '';
|
content: "";
|
||||||
background: conic-gradient(v-bind(allColors));
|
background: conic-gradient(v-bind(allColors));
|
||||||
animation: rotate-rainbow v-bind(durationInMilliseconds) linear infinite;
|
animation: rotate-rainbow v-bind(durationInMilliseconds) linear infinite;
|
||||||
filter: blur(v-bind(blurPx));
|
filter: blur(v-bind(blurPx));
|
1
src/components/InspiraUI/gradient-button/index.ts
Normal file
1
src/components/InspiraUI/gradient-button/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as GradientButton } from "./GradientButton.vue";
|
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
ref="buttonRef"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'group relative w-auto cursor-pointer overflow-hidden rounded-full border bg-background p-2 px-6 text-center font-semibold',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="size-2 scale-100 rounded-lg bg-primary transition-all duration-300 group-hover:scale-[100.8]"
|
||||||
|
></div>
|
||||||
|
<span
|
||||||
|
class="inline-block whitespace-nowrap transition-all duration-300 group-hover:translate-x-12 group-hover:opacity-0"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute top-0 z-10 flex size-full translate-x-12 items-center justify-center gap-2 text-primary-foreground opacity-0 transition-all duration-300 group-hover:-translate-x-5 group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
<span class="whitespace-nowrap">{{ text }}</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-arrow-right"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
<path d="m12 5 7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { cn } from "@/shadcn/lib/utils";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text?: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
text: "Button",
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonRef = ref<HTMLButtonElement>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
@ -0,0 +1 @@
|
|||||||
|
export { default as InteractiveHoverButton } from "./InteractiveHoverButton.vue";
|
107
src/components/InspiraUI/shimmer-button/ShimmerButton.vue
Normal file
107
src/components/InspiraUI/shimmer-button/ShimmerButton.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden whitespace-nowrap border border-white/10 px-6 py-3 text-white [background:var(--bg)] [border-radius:var(--radius)] dark:text-black',
|
||||||
|
'transform-gpu transition-transform duration-300 ease-in-out active:translate-y-px',
|
||||||
|
$props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:style="{
|
||||||
|
'--spread': '90deg',
|
||||||
|
'--shimmer-color': shimmerColor,
|
||||||
|
'--radius': borderRadius,
|
||||||
|
'--speed': shimmerDuration,
|
||||||
|
'--cut': shimmerSize,
|
||||||
|
'--bg': background,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div :class="cn('-z-30 blur-[2px]', 'absolute inset-0 overflow-visible [container-type:size]')">
|
||||||
|
<div
|
||||||
|
class="animate-shimmer-btn-shimmer-slide absolute inset-0 h-[100cqh] [aspect-ratio:1] [border-radius:0] [mask:none]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="animate-shimmer-btn-spin-around absolute -inset-full w-auto rotate-0 [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))] [translate:0_0]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'insert-0 absolute size-full',
|
||||||
|
|
||||||
|
'rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#ffffff1f]',
|
||||||
|
|
||||||
|
// transition
|
||||||
|
'transform-gpu transition-all duration-300 ease-in-out',
|
||||||
|
|
||||||
|
// on hover
|
||||||
|
'group-hover:shadow-[inset_0_-6px_10px_#ffffff3f]',
|
||||||
|
|
||||||
|
// on click
|
||||||
|
'group-active:shadow-[inset_0_-10px_10px_#ffffff3f]',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute -z-20 [background:var(--bg)] [border-radius:var(--radius)] [inset:var(--cut)]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { cn } from '@/shadcn/lib/utils';
|
||||||
|
|
||||||
|
type ShimmerButtonProps = {
|
||||||
|
shimmerColor?: string;
|
||||||
|
shimmerSize?: string;
|
||||||
|
borderRadius?: string;
|
||||||
|
shimmerDuration?: string;
|
||||||
|
background?: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
withDefaults(defineProps<ShimmerButtonProps>(), {
|
||||||
|
shimmerColor: "#ffffff",
|
||||||
|
shimmerSize: "0.05em",
|
||||||
|
shimmerDuration: "3s",
|
||||||
|
borderRadius: "100px",
|
||||||
|
background: "rgba(0, 0, 0, 1)",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes shimmer-btn-shimmer-slide {
|
||||||
|
to {
|
||||||
|
transform: translate(calc(100cqw - 100%), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer-btn-spin-around {
|
||||||
|
0% {
|
||||||
|
transform: translateZ(0) rotate(0);
|
||||||
|
}
|
||||||
|
15%,
|
||||||
|
35% {
|
||||||
|
transform: translateZ(0) rotate(90deg);
|
||||||
|
}
|
||||||
|
65%,
|
||||||
|
85% {
|
||||||
|
transform: translateZ(0) rotate(270deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateZ(0) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shimmer-btn-shimmer-slide {
|
||||||
|
animation: shimmer-btn-shimmer-slide var(--speed) ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shimmer-btn-spin-around {
|
||||||
|
animation: shimmer-btn-spin-around calc(var(--speed) * 2) infinite linear;
|
||||||
|
}
|
||||||
|
</style>
|
1
src/components/InspiraUI/shimmer-button/index.ts
Normal file
1
src/components/InspiraUI/shimmer-button/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as ShimmerButton } from "./ShimmerButton.vue";
|
159
src/components/InspiraUI/sparkles/Sparkles.vue
Normal file
159
src/components/InspiraUI/sparkles/Sparkles.vue
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="containerRef"
|
||||||
|
class="relative size-full overflow-hidden will-change-transform"
|
||||||
|
:style="{ background }"
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref="canvasRef"
|
||||||
|
class="absolute inset-0 size-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRafFn, templateRef } from "@vueuse/core";
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
background?: string;
|
||||||
|
particleColor?: string;
|
||||||
|
minSize?: number;
|
||||||
|
maxSize?: number;
|
||||||
|
speed?: number;
|
||||||
|
particleDensity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
size: number;
|
||||||
|
opacity: number;
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
phase: number;
|
||||||
|
phaseSpeed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
background: "#0d47a1",
|
||||||
|
particleColor: "#ffffff",
|
||||||
|
minSize: 1,
|
||||||
|
maxSize: 3,
|
||||||
|
speed: 4,
|
||||||
|
particleDensity: 120,
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerRef = templateRef<HTMLElement | null>("containerRef");
|
||||||
|
const canvasRef = templateRef<HTMLCanvasElement | null>("canvasRef");
|
||||||
|
const particles = ref<Particle[]>([]);
|
||||||
|
const ctx = ref<CanvasRenderingContext2D | null>(null);
|
||||||
|
|
||||||
|
// Adjust canvas size on mount and resize
|
||||||
|
function resizeCanvas() {
|
||||||
|
if (!canvasRef.value || !containerRef.value) return;
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const rect = containerRef.value.getBoundingClientRect();
|
||||||
|
|
||||||
|
canvasRef.value.width = rect.width * dpr;
|
||||||
|
canvasRef.value.height = rect.height * dpr;
|
||||||
|
|
||||||
|
if (ctx.value) {
|
||||||
|
ctx.value.scale(dpr, dpr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateParticles(): void {
|
||||||
|
const newParticles: Particle[] = [];
|
||||||
|
const count = props.particleDensity;
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const baseSpeed = 0.05;
|
||||||
|
const speedVariance = Math.random() * 0.3 + 0.7;
|
||||||
|
|
||||||
|
newParticles.push({
|
||||||
|
x: Math.random() * 100,
|
||||||
|
y: Math.random() * 100,
|
||||||
|
size: Math.random() * (props.maxSize - props.minSize) + props.minSize,
|
||||||
|
opacity: Math.random() * 0.5 + 0.3,
|
||||||
|
vx: (Math.random() - 0.5) * baseSpeed * speedVariance * props.speed,
|
||||||
|
vy: ((Math.random() - 0.5) * baseSpeed - baseSpeed * 0.3) * speedVariance * props.speed,
|
||||||
|
phase: Math.random() * Math.PI * 2,
|
||||||
|
phaseSpeed: 0.015,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
particles.value = newParticles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAndDrawParticles() {
|
||||||
|
if (!ctx.value || !canvasRef.value) return;
|
||||||
|
|
||||||
|
const canvas = canvasRef.value;
|
||||||
|
ctx.value.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
particles.value = particles.value.map((particle) => {
|
||||||
|
let newX = particle.x + particle.vx;
|
||||||
|
let newY = particle.y + particle.vy;
|
||||||
|
|
||||||
|
if (newX < -2) newX = 102;
|
||||||
|
if (newX > 102) newX = -2;
|
||||||
|
if (newY < -2) newY = 102;
|
||||||
|
if (newY > 102) newY = -2;
|
||||||
|
|
||||||
|
const newPhase = (particle.phase + particle.phaseSpeed) % (Math.PI * 2);
|
||||||
|
const opacity = 0.3 + (Math.sin(newPhase) * 0.3 + 0.3);
|
||||||
|
|
||||||
|
// Draw particle
|
||||||
|
ctx.value!.beginPath();
|
||||||
|
ctx.value!.arc(
|
||||||
|
(newX * canvas.width) / 100,
|
||||||
|
(newY * canvas.height) / 100,
|
||||||
|
particle.size,
|
||||||
|
0,
|
||||||
|
Math.PI * 2,
|
||||||
|
);
|
||||||
|
ctx.value!.fillStyle = `${props.particleColor}${Math.floor(opacity * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
ctx.value!.fill();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...particle,
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
phase: newPhase,
|
||||||
|
opacity,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pause, resume } = useRafFn(updateAndDrawParticles, { immediate: false });
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
let resizeObserver: ResizeObserver | undefined;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!canvasRef.value) return;
|
||||||
|
|
||||||
|
ctx.value = canvasRef.value.getContext("2d");
|
||||||
|
resizeCanvas();
|
||||||
|
generateParticles();
|
||||||
|
|
||||||
|
// Set up resize observer
|
||||||
|
resizeObserver = new ResizeObserver(resizeCanvas);
|
||||||
|
if (containerRef.value) {
|
||||||
|
resizeObserver.observe(containerRef.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
resume();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
pause();
|
||||||
|
if (resizeObserver && containerRef.value) {
|
||||||
|
resizeObserver.unobserve(containerRef.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
1
src/components/InspiraUI/sparkles/index.ts
Normal file
1
src/components/InspiraUI/sparkles/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Sparkles } from "./Sparkles.vue";
|
59
src/pages/Home.page.vue
Normal file
59
src/pages/Home.page.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
const VITE_BUILD_COMMIT = import.meta.env.VITE_BUILD_COMMIT;
|
||||||
|
import { routes } from 'vue-router/auto-routes';
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
name: 'Home',
|
||||||
|
meta: {
|
||||||
|
title: '首页',
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
import { useHead, useSeoMeta } from '@unhead/vue';
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
bodyAttrs: { class: { overflow: true } },
|
||||||
|
title: 'Hello World',
|
||||||
|
titleTemplate: (title) => `${title} | My App`,
|
||||||
|
// Deduping
|
||||||
|
// script: [{ key: '123', src: '/script.js' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'vue-ts-example 首页',
|
||||||
|
description: 'vue-ts-example 首页',
|
||||||
|
});
|
||||||
|
|
||||||
|
consola.info('routes', routes);
|
||||||
|
|
||||||
|
const FComponent: import('vue').FunctionalComponent<{ prop: string }> = (props /* context */) => (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
class="green"
|
||||||
|
href="https://cn.vuejs.org/guide/extras/render-function#typing-functional-components"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
函数式组件: https://cn.vuejs.org/guide/extras/render-function#typing-functional-components
|
||||||
|
</a>
|
||||||
|
<p>函数式组件,接收到的 prop 值为:</p>
|
||||||
|
<pre>{JSON.stringify(props, null, 2)}</pre>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div b="1px solid pink" mt-2 p-2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<router-link class="green" :to="{ name: 'DataLoadersId', params: { id: 520 } }">Data Loaders</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div b="1px solid pink" mt-2 p-2>
|
||||||
|
<FComponent prop="Hello World" style="margin-top: 1rem"></FComponent>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SendSms class="mt-2!" />
|
||||||
|
<div b="1px solid pink" mt-2 p-2>commit: {{ VITE_BUILD_COMMIT }}</div>
|
||||||
|
</template>
|
@ -8,20 +8,20 @@ import SomeIcon from '~icons/svg/pacman';
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div b="1px solid pink" mt-2 text-4 p-2 space-y-2>
|
<div b="1px solid pink" mt-2 text-4 p-2 space-y-2>
|
||||||
<!-- <div b="1px solid pink">
|
<div b="1px solid pink">
|
||||||
<div>@iconify-json/carbon/icons.json</div>
|
<div>@iconify-json/carbon/icons.json</div>
|
||||||
<div i-carbon-face-cool text-orange />
|
<div i-carbon-face-cool text-orange />
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
<div b="1px solid pink">
|
<div b="1px solid pink">
|
||||||
<div>Icons({ autoInstall: true })</div>
|
<div>Icons({ autoInstall: true })</div>
|
||||||
<icon-carbon-face-cool class="text-yellow" w-8 h-8 />
|
<icon-carbon-face-cool class="text-yellow" w-8 h-8 />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div b="1px solid pink">
|
<div b="1px solid pink">
|
||||||
<div>pacman.svg</div>
|
<div>pacman.svg</div>
|
||||||
<div class="icon:pacman text-(pink)" />
|
<div class="icon:pacman text-(pink)" />
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
<div b="1px solid pink">
|
<div b="1px solid pink">
|
||||||
<div>pacman.svg</div>
|
<div>pacman.svg</div>
|
||||||
|
@ -19,4 +19,20 @@ const bgColor = computed(() => (isDarkTheme.value ? '#000' : '#fff'));
|
|||||||
Card Spotlight
|
Card Spotlight
|
||||||
</CardSpotlight>
|
</CardSpotlight>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dock class="mb-6">
|
||||||
|
<DockIcon>
|
||||||
|
<Icon-mdi-github class="size-full" />
|
||||||
|
</DockIcon>
|
||||||
|
<DockSeparator />
|
||||||
|
<DockIcon>
|
||||||
|
<Icon-mdi-google-drive class="size-full" />
|
||||||
|
</DockIcon>
|
||||||
|
<DockIcon>
|
||||||
|
<Icon-logos-notion-icon class="size-full" />
|
||||||
|
</DockIcon>
|
||||||
|
<DockIcon>
|
||||||
|
<Icon-logos-whatsapp-icon class="size-full" />
|
||||||
|
</DockIcon>
|
||||||
|
</Dock>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
PATTERN_BACKGROUND_DIRECTION,
|
|
||||||
PATTERN_BACKGROUND_SPEED,
|
|
||||||
PATTERN_BACKGROUND_VARIANT,
|
|
||||||
} from '@/components/InspiraUI/pattern-background';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="p-4 h-screen w-screen">
|
|
||||||
<PatternBackground
|
|
||||||
:animate="true"
|
|
||||||
:direction="PATTERN_BACKGROUND_DIRECTION.TopRight"
|
|
||||||
:variant="PATTERN_BACKGROUND_VARIANT.Dot"
|
|
||||||
class="flex h-full w-full flex-col items-center gap-4 overflow-hidden rounded-lg border px-4 py-8"
|
|
||||||
:speed="PATTERN_BACKGROUND_SPEED.Slow"
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
class="relative z-20 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text py-8 text-3xl font-bold text-transparent"
|
|
||||||
>
|
|
||||||
Spline
|
|
||||||
</h3>
|
|
||||||
<Spline scene="https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode" />
|
|
||||||
</PatternBackground>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
||||||
<route lang="yaml">
|
|
||||||
meta:
|
|
||||||
layout: false
|
|
||||||
</route>
|
|
@ -1,53 +1,85 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="ts">
|
||||||
const VITE_BUILD_COMMIT = import.meta.env.VITE_BUILD_COMMIT;
|
import {
|
||||||
import { routes } from 'vue-router/auto-routes';
|
PATTERN_BACKGROUND_DIRECTION,
|
||||||
|
PATTERN_BACKGROUND_SPEED,
|
||||||
|
PATTERN_BACKGROUND_VARIANT,
|
||||||
|
} from '@/components/InspiraUI/pattern-background';
|
||||||
|
import { useLayout } from '@/layouts/sakai-vue/composables/layout';
|
||||||
|
|
||||||
definePage({ meta: { title: '首页' } });
|
const { isDarkTheme, toggleDarkMode } = useLayout();
|
||||||
import { useHead, useSeoMeta } from '@unhead/vue';
|
const GradientButton_bgColor = computed(() => (isDarkTheme.value ? '#000' : '#fff'));
|
||||||
|
const particlesColor = computed(() => (isDarkTheme.value ? '#FFFFFF' : '#000000'));
|
||||||
useHead({
|
|
||||||
bodyAttrs: { class: { overflow: true } },
|
|
||||||
title: 'Hello World',
|
|
||||||
titleTemplate: (title) => `${title} | My App`,
|
|
||||||
// Deduping
|
|
||||||
// script: [{ key: '123', src: '/script.js' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
useSeoMeta({
|
|
||||||
description: '首页',
|
|
||||||
title: 'Hello World',
|
|
||||||
});
|
|
||||||
|
|
||||||
consola.info('routes', routes);
|
|
||||||
|
|
||||||
const FComponent: import('vue').FunctionalComponent<{ prop: string }> = (props /* context */) => (
|
|
||||||
<>
|
|
||||||
<a
|
|
||||||
class="green"
|
|
||||||
href="https://cn.vuejs.org/guide/extras/render-function#typing-functional-components"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
函数式组件: https://cn.vuejs.org/guide/extras/render-function#typing-functional-components
|
|
||||||
</a>
|
|
||||||
<p>函数式组件,接收到的 prop 值为:</p>
|
|
||||||
<pre>{JSON.stringify(props, null, 2)}</pre>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div b="1px solid pink" mt-2 p-2>
|
<div class="p-4 h-screen w-screen relative">
|
||||||
<ul>
|
<PatternBackground
|
||||||
<li>
|
:animate="true"
|
||||||
<router-link class="green" :to="{ name: 'DataLoadersId', params: { id: 520 } }">Data Loaders</router-link>
|
:direction="PATTERN_BACKGROUND_DIRECTION.TopRight"
|
||||||
</li>
|
:variant="PATTERN_BACKGROUND_VARIANT.Dot"
|
||||||
</ul>
|
class="flex h-full w-full flex-col items-center gap-4 overflow-hidden border rounded-lg px-4 py-8"
|
||||||
|
:speed="PATTERN_BACKGROUND_SPEED.Slow"
|
||||||
|
shadow-2xl
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="relative z-20 bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text py-0 text-3xl font-bold text-transparent"
|
||||||
|
>
|
||||||
|
Spline
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<RouterLink :to="{ name: 'Home' }">
|
||||||
|
<GradientButton :bg-color="GradientButton_bgColor">Zooooooooooom 🚀 </GradientButton>
|
||||||
|
<!-- <InteractiveHoverButton text="Zooooooooooom 🚀" /> -->
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<ShimmerButton class="isolate shadow-2xl" shimmer-size="2px" @click="toggleDarkMode">
|
||||||
|
<span
|
||||||
|
class="whitespace-pre-wrap text-center text-sm font-medium leading-none tracking-tight text-white lg:text-lg dark:from-white dark:to-slate-900/10"
|
||||||
|
>
|
||||||
|
{{ isDarkTheme ? '🌜' : '🌞' }}
|
||||||
|
</span>
|
||||||
|
</ShimmerButton>
|
||||||
|
|
||||||
|
<div class="relative h-40 w-[40rem]">
|
||||||
|
<div
|
||||||
|
class="absolute inset-x-20 top-0 h-[2px] w-3/4 bg-gradient-to-r from-transparent via-indigo-500 to-transparent blur-sm"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute inset-x-20 top-0 h-px w-3/4 bg-gradient-to-r from-transparent via-indigo-500 to-transparent"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute inset-x-60 top-0 h-[5px] w-1/4 bg-gradient-to-r from-transparent via-sky-500 to-transparent blur-sm"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute inset-x-60 top-0 h-px w-1/4 bg-gradient-to-r from-transparent via-sky-500 to-transparent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Sparkles
|
||||||
|
background="transparent"
|
||||||
|
:min-size="0.4"
|
||||||
|
:max-size="1.4"
|
||||||
|
:particle-density="1200"
|
||||||
|
class="size-full"
|
||||||
|
:particle-color="particlesColor"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="absolute inset-0 size-full [mask-image:radial-gradient(350px_200px_at_top,transparent_20%,white)]">
|
||||||
|
<!-- bg-white dark:bg-black -->
|
||||||
</div>
|
</div>
|
||||||
<div b="1px solid pink" mt-2 p-2>
|
|
||||||
<FComponent prop="Hello World" style="margin-top: 1rem"></FComponent>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SendSms class="mt-2!" />
|
<Spline scene="https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode" />
|
||||||
<div b="1px solid pink" mt-2 p-2>commit: {{ VITE_BUILD_COMMIT }}</div>
|
|
||||||
|
<BorderBeam :size="250" :duration="12" :delay="9" :border-width="2" />
|
||||||
|
</PatternBackground>
|
||||||
|
|
||||||
|
<FluidCursor />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
layout: false
|
||||||
|
</route>
|
||||||
|
@ -22,9 +22,9 @@ router.onError((error) => {
|
|||||||
export { router, setupLayoutsResult };
|
export { router, setupLayoutsResult };
|
||||||
export function install({ app }: { app: import('vue').App<Element> }) {
|
export function install({ app }: { app: import('vue').App<Element> }) {
|
||||||
app
|
app
|
||||||
// Register the plugin before the router
|
// 在路由之前注册插件
|
||||||
.use(DataLoaderPlugin, { router })
|
.use(DataLoaderPlugin, { router })
|
||||||
// adding the router will trigger the initial navigation
|
// 添加路由会触发初始导航
|
||||||
.use(router);
|
.use(router);
|
||||||
}
|
}
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import 'vue-router';
|
|
||||||
|
|
||||||
// declare module 'vue-router' {
|
|
||||||
// interface RouteMeta {
|
|
||||||
// // fromList?: RouteLocationNormalized[]
|
|
||||||
// }
|
|
||||||
|
|
||||||
// interface Router {
|
|
||||||
// stack: {
|
|
||||||
// // list: RouteLocationNormalizedLoaded[]
|
|
||||||
// // currentStackIndex: number
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// interface RouteLocationNormalizedGeneric {
|
|
||||||
// // from: RouteLocationNormalizedLoaded
|
|
||||||
// // stackIndex: number
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
export {};
|
|
2
typed-router.d.ts
vendored
2
typed-router.d.ts
vendored
@ -24,7 +24,7 @@ declare module 'vue-router/auto-routes' {
|
|||||||
'DataLoadersId': RouteRecordInfo<'DataLoadersId', '/data-loaders/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
'DataLoadersId': RouteRecordInfo<'DataLoadersId', '/data-loaders/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||||
'DataLoadersIdSub1UserId': RouteRecordInfo<'DataLoadersIdSub1UserId', '/data-loaders/:id/sub-1/:userId', { id: ParamValue<true>, userId: ParamValue<true> }, { id: ParamValue<false>, userId: ParamValue<false> }>,
|
'DataLoadersIdSub1UserId': RouteRecordInfo<'DataLoadersIdSub1UserId', '/data-loaders/:id/sub-1/:userId', { id: ParamValue<true>, userId: ParamValue<true> }, { id: ParamValue<false>, userId: ParamValue<false> }>,
|
||||||
'FlowbiteSidebar': RouteRecordInfo<'FlowbiteSidebar', '/FlowbiteSidebar', Record<never, never>, Record<never, never>>,
|
'FlowbiteSidebar': RouteRecordInfo<'FlowbiteSidebar', '/FlowbiteSidebar', Record<never, never>, Record<never, never>>,
|
||||||
'IndexNew': RouteRecordInfo<'IndexNew', '/index-new', Record<never, never>, Record<never, never>>,
|
'Home': RouteRecordInfo<'Home', '/Home', Record<never, never>, Record<never, never>>,
|
||||||
'PageAPI': RouteRecordInfo<'PageAPI', '/Page/API', Record<never, never>, Record<never, never>>,
|
'PageAPI': RouteRecordInfo<'PageAPI', '/Page/API', Record<never, never>, Record<never, never>>,
|
||||||
'PageFonts': RouteRecordInfo<'PageFonts', '/Page/fonts', Record<never, never>, Record<never, never>>,
|
'PageFonts': RouteRecordInfo<'PageFonts', '/Page/fonts', Record<never, never>, Record<never, never>>,
|
||||||
'PageIcons': RouteRecordInfo<'PageIcons', '/Page/Icons', Record<never, never>, Record<never, never>>,
|
'PageIcons': RouteRecordInfo<'PageIcons', '/Page/Icons', Record<never, never>, Record<never, never>>,
|
||||||
|
@ -37,6 +37,9 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
// // 'vendor/nprogress': ['nprogress'],
|
// // 'vendor/nprogress': ['nprogress'],
|
||||||
// // 'vendor/formkit': ['@formkit/auto-animate'],
|
// // 'vendor/formkit': ['@formkit/auto-animate'],
|
||||||
// },
|
// },
|
||||||
|
manualChunks: {
|
||||||
|
'vendor/Cesium': ['cesium'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sourcemap: mode !== 'production' || env.VITE_SOURCE_MAP === 'true',
|
sourcemap: mode !== 'production' || env.VITE_SOURCE_MAP === 'true',
|
||||||
|
Reference in New Issue
Block a user