rimelight-components 1.1.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.mjs CHANGED
@@ -56,7 +56,6 @@ const module = defineNuxtModule({
56
56
  global: true
57
57
  });
58
58
  addImportsDir(resolver.resolve("./runtime/composables"));
59
- addImportsDir(resolver.resolve("./runtime/utils"));
60
59
  },
61
60
  onInstall() {
62
61
  console.log("Setting up rimelight-components for the first time!");
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ circleStrokeWidth?: number;
3
+ duration?: number;
4
+ }
5
+ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {
6
+ circleStrokeWidth: number;
7
+ duration: number;
8
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,97 @@
1
+ <script setup>
2
+ import { computed, onMounted, onUnmounted, ref } from "vue";
3
+ const props = defineProps({
4
+ circleStrokeWidth: { type: Number, required: false, default: 4 },
5
+ duration: { type: Number, required: false, default: 0.1 }
6
+ });
7
+ const scrollPercentage = ref(0);
8
+ const minScrollThreshold = 15;
9
+ const isVisible = computed(() => scrollPercentage.value >= minScrollThreshold);
10
+ function updatePageScroll() {
11
+ if (typeof window === "undefined" || typeof document === "undefined") {
12
+ return;
13
+ }
14
+ const scrollY = window.scrollY;
15
+ const maxScroll = document.body.scrollHeight - window.innerHeight;
16
+ if (maxScroll <= 0) {
17
+ scrollPercentage.value = 0;
18
+ return;
19
+ }
20
+ scrollPercentage.value = Math.min(scrollY / maxScroll * 100, 100);
21
+ }
22
+ function scrollToTop() {
23
+ if (typeof window === "undefined") return;
24
+ window.scrollTo({
25
+ top: 0,
26
+ behavior: "smooth"
27
+ });
28
+ }
29
+ onMounted(() => {
30
+ if (typeof window === "undefined") return;
31
+ window.addEventListener("scroll", updatePageScroll, { passive: true });
32
+ updatePageScroll();
33
+ });
34
+ onUnmounted(() => {
35
+ if (typeof window === "undefined") return;
36
+ window.removeEventListener("scroll", updatePageScroll);
37
+ });
38
+ const circumference = 2 * Math.PI * 45;
39
+ const percentPx = circumference / 100;
40
+ const currentPercent = computed(
41
+ () => (scrollPercentage.value - 0) / 100 * 100
42
+ );
43
+ const percentageInPx = computed(() => `${percentPx}px`);
44
+ const durationInSeconds = computed(() => `${props.duration}s`);
45
+ </script>
46
+
47
+ <template>
48
+ <Transition
49
+ name="fade"
50
+ enter-active-class="transition-opacity duration-500 ease-in"
51
+ leave-active-class="transition-opacity duration-500 ease-out"
52
+ enter-from-class="opacity-0"
53
+ leave-to-class="opacity-0"
54
+ >
55
+ <div v-if="isVisible">
56
+ <UButton
57
+ variant="ghost"
58
+ class="fixed right-4 bottom-4 z-50 size-20 lg:size-16"
59
+ @click="scrollToTop"
60
+ >
61
+ <div class="progress-circle-base size-full">
62
+ <svg class="size-full" viewBox="0 0 100 100">
63
+ <circle
64
+ cx="50"
65
+ cy="50"
66
+ r="45"
67
+ fill="var(--color-primary-950)"
68
+ :stroke-width="props.circleStrokeWidth"
69
+ stroke-dashoffset="0"
70
+ stroke-linecap="round"
71
+ class="gauge-secondary-stroke opacity-100"
72
+ />
73
+ <circle
74
+ cx="50"
75
+ cy="50"
76
+ r="45"
77
+ fill="transparent"
78
+ :stroke-width="props.circleStrokeWidth"
79
+ stroke-dashoffset="0"
80
+ stroke-linecap="round"
81
+ class="gauge-primary-stroke opacity-100"
82
+ />
83
+ </svg>
84
+ <div
85
+ class="absolute inset-0 flex items-center justify-center text-center"
86
+ >
87
+ <UIcon name="lucide:arrow-up" class="size-6 text-white" />
88
+ </div>
89
+ </div>
90
+ </UButton>
91
+ </div>
92
+ </Transition>
93
+ </template>
94
+
95
+ <style scoped>
96
+ .progress-circle-base{--circle-size:100px;--circumference:v-bind(circumference);--percent-to-px:v-bind(percentageInPx);transform:translateZ(0)}.gauge-primary-stroke{stroke:var(--color-primary-500);--stroke-percent:v-bind(currentPercent);stroke-dasharray:calc(var(--stroke-percent)*var(--percent-to-px)) var(--circumference);transition:stroke-dasharray v-bind(durationInSeconds) ease,stroke v-bind(durationInSeconds) ease}.gauge-primary-stroke,.gauge-secondary-stroke{transform:rotate(-90deg);transform-origin:center}.gauge-secondary-stroke{stroke:var(--color-primary-900);stroke-dasharray:var(--circumference)}
97
+ </style>
@@ -0,0 +1,10 @@
1
+ interface Props {
2
+ circleStrokeWidth?: number;
3
+ duration?: number;
4
+ }
5
+ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {
6
+ circleStrokeWidth: number;
7
+ duration: number;
8
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,23 @@
1
+ interface Cards {
2
+ icon: string;
3
+ href?: string;
4
+ iconClass?: string;
5
+ }
6
+ interface Props {
7
+ class?: string;
8
+ textGlowStartColor?: string;
9
+ perspective?: number;
10
+ textGlowEndColor?: string;
11
+ cards: Cards[];
12
+ rotateX?: number;
13
+ rotateY?: number;
14
+ }
15
+ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {
16
+ textGlowStartColor: string;
17
+ perspective: number;
18
+ textGlowEndColor: string;
19
+ rotateX: number;
20
+ rotateY: number;
21
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
+ declare const _default: typeof __VLS_export;
23
+ export default _default;
@@ -0,0 +1,84 @@
1
+ <script setup>
2
+ import { onMounted, ref, watch } from "vue";
3
+ import { cn } from "../../utils";
4
+ import { useMouseInElement, useDebounceFn } from "@vueuse/core";
5
+ const card = ref();
6
+ const props = defineProps({
7
+ class: { type: String, required: false },
8
+ textGlowStartColor: { type: String, required: false, default: "var(--color-primary-300, #38ef7d)" },
9
+ perspective: { type: Number, required: false, default: 750 },
10
+ textGlowEndColor: { type: String, required: false, default: "var(--color-primary-500, #38ef7d)" },
11
+ cards: { type: Array, required: true },
12
+ rotateX: { type: Number, required: false, default: -1 },
13
+ rotateY: { type: Number, required: false, default: -15 }
14
+ });
15
+ function adjacentCardItems(i) {
16
+ return [i - 1, i + 1, i - 4, i + 4].filter((index) => {
17
+ if (index < 0 || index > 15) return false;
18
+ if (i % 4 === 0 && index === i - 1) return false;
19
+ return !(i % 4 === 3 && index === i + 1);
20
+ }).map((index) => card.value?.[index]);
21
+ }
22
+ function removeCardClasses(el, adjacentCards) {
23
+ el.classList.remove("card-raised-big");
24
+ adjacentCards.forEach((adjacentCard) => {
25
+ adjacentCard?.classList.remove("card-raised-small");
26
+ });
27
+ }
28
+ onMounted(() => {
29
+ card.value?.forEach((el, i) => {
30
+ const { isOutside } = useMouseInElement(el);
31
+ const adjacentCards = adjacentCardItems(i);
32
+ const removeClasses = useDebounceFn(
33
+ () => removeCardClasses(el, adjacentCards),
34
+ 200
35
+ );
36
+ watch(isOutside, (isOutside2) => {
37
+ if (!isOutside2) {
38
+ el.classList.add("card-raised-big");
39
+ adjacentCards.forEach((adjacentCard) => {
40
+ adjacentCard?.classList.add("card-raised-small");
41
+ });
42
+ } else {
43
+ removeClasses();
44
+ }
45
+ });
46
+ });
47
+ });
48
+ </script>
49
+
50
+ <template>
51
+ <div :class="cn('relative block', props.class)">
52
+ <div
53
+ :class="
54
+ cn(
55
+ 'relative grid w-full max-w-full items-center justify-center gap-2',
56
+ props.cards.length < 4 ? `grid-cols-${props.cards.length}` : 'grid-cols-4'
57
+ )
58
+ "
59
+ :style="{
60
+ transform: `perspective(${props.perspective}px) rotateX(${props.rotateX}deg) rotateY(${props.rotateY}deg)`
61
+ }"
62
+ >
63
+ <div
64
+ v-for="(item, index) in props.cards"
65
+ :key="index"
66
+ ref="card"
67
+ class="card block rounded border border-transparent px-2 py-2 transition-all duration-200"
68
+ :style="{ zIndex: index + 1 }"
69
+ >
70
+ <NuxtLink :to="item.href" target="_blank" :index="index">
71
+ <UIcon
72
+ :name="item.icon"
73
+ class="icon mx-auto h-16 w-auto p-3"
74
+ :class="item.iconClass"
75
+ />
76
+ </NuxtLink>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </template>
81
+
82
+ <style scoped>
83
+ .icon svg{display:block;margin:0 auto;max-height:100%;max-width:100%}.grid-transform:before{background:radial-gradient(circle,#d9fbe8 0,#fff 70%,transparent 100%);content:"";height:120%;left:-10%;opacity:.5;position:absolute;top:-10%;width:150%;z-index:-1}.dark .grid-transform:before{background:radial-gradient(circle,var(--color-neutral-700) 0,var(--color-neutral-800) 70%,transparent 100%)}.card{box-shadow:2px 2px 5px var(--color-primary-300,#38ef7d),3px 3px 10px var(--color-primary-400,#38ef7d),6px 6px 20px var(--color-primary-500,#38ef7d)}.dark .card{box-shadow:2px 2px 5px oklch(from var(--color-primary-900) l c h/.25),3px 3px 10px oklch(from var(--color-primary-900) l c h/.25),6px 6px 20px oklch(from var(--color-primary-900) l c h/.25)}.card svg{opacity:.7;transition:.2s}.dark .card:hover{box-shadow:3px 3px 5px #1f2937,5px 5px 10px #1f2937,10px 10px 20px #1f2937}.card:hover svg{opacity:1}.card svg{shape-rendering:geometricPrecision}.card-raised-small{animation:text-glow-small 1.5s ease-in-out infinite alternate;border:1px solid var(--color-primary-300);transform:scale(1.05) translateX(-5px) translateY(-5px) translateZ(0)}.card-raised-big{animation:text-glow 1.5s ease-in-out infinite alternate;background-color:#fff;transform:scale(1.15) translateX(-20px) translateY(-20px) translateZ(15px)}.card-raised-big,.dark .card-raised-big{border:1px solid v-bind(textGlowStartColor)}.dark .card-raised-big{background-color:oklch(from var(--color-primary-500) l c h/.5)}.dark .card-raised-small{border:1px solid v-bind(textGlowStartColor);transform:scale(1.05) translateX(-5px) translateY(-5px) translateZ(0)}@keyframes text-glow{0%{filter:drop-shadow(0 0 2px v-bind(textGlowStartColor))}to{filter:drop-shadow(0 1px 8px v-bind(textGlowEndColor))}}@keyframes text-glow-small{0%{filter:drop-shadow(0 0 2px v-bind(textGlowEndColor))}to{filter:drop-shadow(0 1px 4px v-bind(textGlowEndColor))}}
84
+ </style>
@@ -0,0 +1,23 @@
1
+ interface Cards {
2
+ icon: string;
3
+ href?: string;
4
+ iconClass?: string;
5
+ }
6
+ interface Props {
7
+ class?: string;
8
+ textGlowStartColor?: string;
9
+ perspective?: number;
10
+ textGlowEndColor?: string;
11
+ cards: Cards[];
12
+ rotateX?: number;
13
+ rotateY?: number;
14
+ }
15
+ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {
16
+ textGlowStartColor: string;
17
+ perspective: number;
18
+ textGlowEndColor: string;
19
+ rotateX: number;
20
+ rotateY: number;
21
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
+ declare const _default: typeof __VLS_export;
23
+ export default _default;
@@ -0,0 +1,19 @@
1
+ interface FlickeringGridProps {
2
+ squareSize?: number;
3
+ gridGap?: number;
4
+ flickerChance?: number;
5
+ color?: string;
6
+ width?: number;
7
+ height?: number;
8
+ class?: string;
9
+ maxOpacity?: number;
10
+ }
11
+ declare const __VLS_export: import("vue").DefineComponent<FlickeringGridProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<FlickeringGridProps> & Readonly<{}>, {
12
+ color: string;
13
+ squareSize: number;
14
+ gridGap: number;
15
+ flickerChance: number;
16
+ maxOpacity: number;
17
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ declare const _default: typeof __VLS_export;
19
+ export default _default;
@@ -0,0 +1,130 @@
1
+ <script setup>
2
+ import { cn } from "../../utils";
3
+ import { ref, onMounted, onBeforeUnmount, toRefs, computed } from "vue";
4
+ const props = defineProps({
5
+ squareSize: { type: Number, required: false, default: 4 },
6
+ gridGap: { type: Number, required: false, default: 6 },
7
+ flickerChance: { type: Number, required: false, default: 0.3 },
8
+ color: { type: String, required: false, default: "rgb(0, 0, 0)" },
9
+ width: { type: Number, required: false },
10
+ height: { type: Number, required: false },
11
+ class: { type: String, required: false },
12
+ maxOpacity: { type: Number, required: false, default: 0.3 }
13
+ });
14
+ const { squareSize, gridGap, flickerChance, color, maxOpacity, width, height } = toRefs(props);
15
+ const containerRef = ref();
16
+ const canvasRef = ref();
17
+ const context = ref();
18
+ const isInView = ref(false);
19
+ const canvasSize = ref({ width: 0, height: 0 });
20
+ const computedColor = computed(() => {
21
+ if (!context.value) return "rgba(255, 0, 0,";
22
+ const hex = color.value.replace(/^#/, "");
23
+ const bigint = Number.parseInt(hex, 16);
24
+ const r = bigint >> 16 & 255;
25
+ const g = bigint >> 8 & 255;
26
+ const b = bigint & 255;
27
+ return `rgba(${r}, ${g}, ${b},`;
28
+ });
29
+ function setupCanvas(canvas, width2, height2) {
30
+ const dpr = window.devicePixelRatio || 1;
31
+ canvas.width = width2 * dpr;
32
+ canvas.height = height2 * dpr;
33
+ canvas.style.width = `${width2}px`;
34
+ canvas.style.height = `${height2}px`;
35
+ const cols = Math.floor(width2 / (squareSize.value + gridGap.value));
36
+ const rows = Math.floor(height2 / (squareSize.value + gridGap.value));
37
+ const squares = new Float32Array(cols * rows);
38
+ for (let i = 0; i < squares.length; i++) {
39
+ squares[i] = Math.random() * maxOpacity.value;
40
+ }
41
+ return { cols, rows, squares, dpr };
42
+ }
43
+ function updateSquares(squares, deltaTime) {
44
+ for (let i = 0; i < squares.length; i++) {
45
+ if (Math.random() < flickerChance.value * deltaTime) {
46
+ squares[i] = Math.random() * maxOpacity.value;
47
+ }
48
+ }
49
+ }
50
+ function drawGrid(ctx, width2, height2, cols, rows, squares, dpr) {
51
+ ctx.clearRect(0, 0, width2, height2);
52
+ ctx.fillStyle = "transparent";
53
+ ctx.fillRect(0, 0, width2, height2);
54
+ for (let i = 0; i < cols; i++) {
55
+ for (let j = 0; j < rows; j++) {
56
+ const opacity = squares[i * rows + j];
57
+ ctx.fillStyle = `${computedColor.value}${opacity})`;
58
+ ctx.fillRect(
59
+ i * (squareSize.value + gridGap.value) * dpr,
60
+ j * (squareSize.value + gridGap.value) * dpr,
61
+ squareSize.value * dpr,
62
+ squareSize.value * dpr
63
+ );
64
+ }
65
+ }
66
+ }
67
+ const gridParams = ref();
68
+ function updateCanvasSize() {
69
+ const newWidth = width.value || containerRef.value.clientWidth;
70
+ const newHeight = height.value || containerRef.value.clientHeight;
71
+ canvasSize.value = { width: newWidth, height: newHeight };
72
+ gridParams.value = setupCanvas(canvasRef.value, newWidth, newHeight);
73
+ }
74
+ let animationFrameId;
75
+ let resizeObserver;
76
+ let intersectionObserver;
77
+ let lastTime = 0;
78
+ function animate(time) {
79
+ if (!isInView.value) return;
80
+ const deltaTime = (time - lastTime) / 1e3;
81
+ lastTime = time;
82
+ updateSquares(gridParams.value.squares, deltaTime);
83
+ drawGrid(
84
+ context.value,
85
+ canvasRef.value.width,
86
+ canvasRef.value.height,
87
+ gridParams.value.cols,
88
+ gridParams.value.rows,
89
+ gridParams.value.squares,
90
+ gridParams.value.dpr
91
+ );
92
+ animationFrameId = requestAnimationFrame(animate);
93
+ }
94
+ onMounted(() => {
95
+ if (!canvasRef.value || !containerRef.value) return;
96
+ context.value = canvasRef.value.getContext("2d");
97
+ if (!context.value) return;
98
+ updateCanvasSize();
99
+ resizeObserver = new ResizeObserver(() => {
100
+ updateCanvasSize();
101
+ });
102
+ intersectionObserver = new IntersectionObserver(
103
+ ([entry]) => {
104
+ isInView.value = entry.isIntersecting;
105
+ animationFrameId = requestAnimationFrame(animate);
106
+ },
107
+ { threshold: 0 }
108
+ );
109
+ resizeObserver.observe(containerRef.value);
110
+ intersectionObserver.observe(canvasRef.value);
111
+ });
112
+ onBeforeUnmount(() => {
113
+ if (animationFrameId) {
114
+ cancelAnimationFrame(animationFrameId);
115
+ }
116
+ resizeObserver?.disconnect();
117
+ intersectionObserver?.disconnect();
118
+ });
119
+ </script>
120
+
121
+ <template>
122
+ <div ref="containerRef" :class="cn('h-full w-full', props.class)">
123
+ <canvas
124
+ ref="canvasRef"
125
+ class="pointer-events-none"
126
+ :width="canvasSize.width"
127
+ :height="canvasSize.height"
128
+ />
129
+ </div>
130
+ </template>
@@ -0,0 +1,19 @@
1
+ interface FlickeringGridProps {
2
+ squareSize?: number;
3
+ gridGap?: number;
4
+ flickerChance?: number;
5
+ color?: string;
6
+ width?: number;
7
+ height?: number;
8
+ class?: string;
9
+ maxOpacity?: number;
10
+ }
11
+ declare const __VLS_export: import("vue").DefineComponent<FlickeringGridProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<FlickeringGridProps> & Readonly<{}>, {
12
+ color: string;
13
+ squareSize: number;
14
+ gridGap: number;
15
+ flickerChance: number;
16
+ maxOpacity: number;
17
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
+ declare const _default: typeof __VLS_export;
19
+ export default _default;
@@ -0,0 +1,15 @@
1
+ import { type HTMLAttributes } from "vue";
2
+ interface InteractiveGridPatternProps {
3
+ className?: HTMLAttributes["class"];
4
+ squaresClassName?: HTMLAttributes["class"];
5
+ width?: number;
6
+ height?: number;
7
+ squares?: [number, number];
8
+ }
9
+ declare const __VLS_export: import("vue").DefineComponent<InteractiveGridPatternProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<InteractiveGridPatternProps> & Readonly<{}>, {
10
+ width: number;
11
+ height: number;
12
+ squares: [number, number];
13
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -0,0 +1,58 @@
1
+ <script setup>
2
+ import { cn } from "../../utils";
3
+ import { ref, computed } from "vue";
4
+ const props = defineProps({
5
+ className: { type: null, required: false },
6
+ squaresClassName: { type: null, required: false },
7
+ width: { type: Number, required: false, default: 40 },
8
+ height: { type: Number, required: false, default: 40 },
9
+ squares: { type: Array, required: false, default: () => [24, 24] }
10
+ });
11
+ const horizontal = computed(() => props.squares[0]);
12
+ const vertical = computed(() => props.squares[1]);
13
+ const totalSquares = computed(() => horizontal.value * vertical.value);
14
+ const hoveredSquare = ref(null);
15
+ const gridWidth = computed(() => props.width * horizontal.value);
16
+ const gridHeight = computed(() => props.height * vertical.value);
17
+ function getX(index) {
18
+ return index % horizontal.value * props.width;
19
+ }
20
+ function getY(index) {
21
+ return Math.floor(index / horizontal.value) * props.height;
22
+ }
23
+ const svgClass = computed(
24
+ () => cn(
25
+ "absolute inset-0 h-full w-full border border-neutral-400/30",
26
+ props.className
27
+ )
28
+ );
29
+ function getRectClass(index) {
30
+ return cn(
31
+ "stroke-neutral-400/30 transition-all duration-100 ease-in-out [&:not(:hover)]:duration-1000",
32
+ hoveredSquare.value === index ? "fill-neutral-300/30" : "fill-transparent",
33
+ props.squaresClassName
34
+ );
35
+ }
36
+ function handleMouseEnter(index) {
37
+ hoveredSquare.value = index;
38
+ }
39
+ function handleMouseLeave() {
40
+ hoveredSquare.value = null;
41
+ }
42
+ </script>
43
+
44
+ <template>
45
+ <svg :width="gridWidth" :height="gridHeight" :class="svgClass">
46
+ <rect
47
+ v-for="index in totalSquares"
48
+ :key="index"
49
+ :x="getX(index)"
50
+ :y="getY(index)"
51
+ :width="width"
52
+ :height="height"
53
+ :class="getRectClass(index)"
54
+ @mouseenter="handleMouseEnter(index)"
55
+ @mouseleave="handleMouseLeave"
56
+ />
57
+ </svg>
58
+ </template>
@@ -0,0 +1,15 @@
1
+ import { type HTMLAttributes } from "vue";
2
+ interface InteractiveGridPatternProps {
3
+ className?: HTMLAttributes["class"];
4
+ squaresClassName?: HTMLAttributes["class"];
5
+ width?: number;
6
+ height?: number;
7
+ squares?: [number, number];
8
+ }
9
+ declare const __VLS_export: import("vue").DefineComponent<InteractiveGridPatternProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<InteractiveGridPatternProps> & Readonly<{}>, {
10
+ width: number;
11
+ height: number;
12
+ squares: [number, number];
13
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -0,0 +1,50 @@
1
+ export declare function useFeedbackRatings(): {
2
+ ratingConfig: any;
3
+ getScoreColor: (score: number) => string;
4
+ getRatingFromFeedback: (feedback: {
5
+ rating: FeedbackRating;
6
+ }) => any;
7
+ calculateStats: (feedbacks: {
8
+ rating: FeedbackRating;
9
+ }[]) => {
10
+ total: number;
11
+ positive: number;
12
+ negative: number;
13
+ averageScore: number;
14
+ positivePercentage: number;
15
+ };
16
+ };
17
+ export declare function useFeedbackData(rawFeedback: Ref<FeedbackItem[] | null>): {
18
+ feedbackData: any;
19
+ globalStats: any;
20
+ pageAnalytics: any;
21
+ };
22
+ export declare function useFeedbackModal(): {
23
+ selectedPage: any;
24
+ showFeedbackModal: any;
25
+ currentPage: any;
26
+ itemsPerPage: number;
27
+ paginatedFeedback: any;
28
+ totalPages: any;
29
+ viewPageDetails: (page: PageAnalytic) => void;
30
+ closeFeedbackModal: () => void;
31
+ };
32
+ export declare function useFeedbackDelete(): {
33
+ deleteFeedback: (id: number) => Promise<boolean>;
34
+ };
35
+ interface UseFeedbackFormOptions {
36
+ page: {
37
+ title: string;
38
+ stem: string;
39
+ };
40
+ }
41
+ export declare function useFeedbackForm(options: UseFeedbackFormOptions): {
42
+ formState: any;
43
+ isExpanded: any;
44
+ isSubmitted: any;
45
+ isSubmitting: any;
46
+ handleRatingSelect: (rating: FeedbackRating) => void;
47
+ submitFeedback: () => Promise<void>;
48
+ resetFeedback: () => void;
49
+ };
50
+ export {};
@@ -0,0 +1,237 @@
1
+ export function useFeedbackRatings() {
2
+ const ratingConfig = computed(() => {
3
+ return FEEDBACK_OPTIONS.reduce(
4
+ (acc, option) => {
5
+ acc[option.value] = option;
6
+ return acc;
7
+ },
8
+ {}
9
+ );
10
+ });
11
+ function getScoreColor(score) {
12
+ if (score >= 4) return `text-success`;
13
+ if (score >= 3) return `text-warning`;
14
+ return `text-error`;
15
+ }
16
+ function getRatingFromFeedback(feedback) {
17
+ return ratingConfig.value[feedback.rating];
18
+ }
19
+ function calculateStats(feedbacks) {
20
+ const total = feedbacks.length;
21
+ const positive = feedbacks.filter(
22
+ (f) => [`very-helpful`, `helpful`].includes(f.rating)
23
+ ).length;
24
+ const negative = feedbacks.filter(
25
+ (f) => [`not-helpful`, `confusing`].includes(f.rating)
26
+ ).length;
27
+ const totalScore = feedbacks.reduce(
28
+ (sum, item) => sum + ratingConfig.value[item.rating].score,
29
+ 0
30
+ );
31
+ const averageScore = total > 0 ? Number((totalScore / total).toFixed(1)) : 0;
32
+ const positivePercentage = total > 0 ? Math.round(positive / total * 100) : 0;
33
+ return {
34
+ total,
35
+ positive,
36
+ negative,
37
+ averageScore,
38
+ positivePercentage
39
+ };
40
+ }
41
+ return {
42
+ ratingConfig,
43
+ getScoreColor,
44
+ getRatingFromFeedback,
45
+ calculateStats
46
+ };
47
+ }
48
+ export function useFeedbackData(rawFeedback) {
49
+ const { calculateStats } = useFeedbackRatings();
50
+ const { filterFeedbackByDateRange } = useDateRange();
51
+ const allFeedbackData = computed(
52
+ () => rawFeedback.value?.map((item) => ({
53
+ ...item,
54
+ createdAt: new Date(item.createdAt),
55
+ updatedAt: new Date(item.updatedAt)
56
+ })) || []
57
+ );
58
+ const feedbackData = computed(
59
+ () => filterFeedbackByDateRange(allFeedbackData.value)
60
+ );
61
+ const globalStats = computed(() => calculateStats(feedbackData.value));
62
+ const pageAnalytics = computed(() => {
63
+ const filteredFeedback = filterFeedbackByDateRange(allFeedbackData.value);
64
+ const pageGroups = filteredFeedback.reduce(
65
+ (acc, item) => {
66
+ if (!acc[item.path]) {
67
+ acc[item.path] = [];
68
+ }
69
+ acc[item.path].push(item);
70
+ return acc;
71
+ },
72
+ {}
73
+ );
74
+ return Object.entries(pageGroups).map(([path, feedback]) => {
75
+ const stats = calculateStats(feedback);
76
+ const sortedFeedback = feedback.sort(
77
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
78
+ );
79
+ const oldestFeedback = feedback.reduce((oldest, current) => {
80
+ if (new Date(current.createdAt) < new Date(oldest.createdAt)) {
81
+ return current;
82
+ } else {
83
+ return oldest;
84
+ }
85
+ });
86
+ return {
87
+ path,
88
+ ...stats,
89
+ feedback,
90
+ lastFeedback: sortedFeedback[0],
91
+ createdAt: new Date(oldestFeedback.createdAt),
92
+ updatedAt: new Date(sortedFeedback[0].updatedAt)
93
+ };
94
+ }).sort((a, b) => b.total - a.total);
95
+ });
96
+ return {
97
+ feedbackData,
98
+ globalStats,
99
+ pageAnalytics
100
+ };
101
+ }
102
+ export function useFeedbackModal() {
103
+ const selectedPage = ref(null);
104
+ const showFeedbackModal = ref(false);
105
+ const currentPage = ref(1);
106
+ const itemsPerPage = 5;
107
+ const paginatedFeedback = computed(() => {
108
+ if (!selectedPage.value) return [];
109
+ const sortedFeedback = [...selectedPage.value.feedback].sort(
110
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
111
+ );
112
+ const startIndex = (currentPage.value - 1) * itemsPerPage;
113
+ const endIndex = startIndex + itemsPerPage;
114
+ return sortedFeedback.slice(startIndex, endIndex);
115
+ });
116
+ const totalPages = computed(() => {
117
+ if (!selectedPage.value) return 0;
118
+ return Math.ceil(selectedPage.value.feedback.length / itemsPerPage);
119
+ });
120
+ function viewPageDetails(page) {
121
+ selectedPage.value = page;
122
+ currentPage.value = 1;
123
+ showFeedbackModal.value = true;
124
+ }
125
+ function closeFeedbackModal() {
126
+ showFeedbackModal.value = false;
127
+ selectedPage.value = null;
128
+ }
129
+ return {
130
+ selectedPage: readonly(selectedPage),
131
+ showFeedbackModal,
132
+ currentPage,
133
+ itemsPerPage,
134
+ paginatedFeedback,
135
+ totalPages,
136
+ viewPageDetails,
137
+ closeFeedbackModal
138
+ };
139
+ }
140
+ export function useFeedbackDelete() {
141
+ const toast = useToast();
142
+ async function deleteFeedback(id) {
143
+ try {
144
+ await $fetch(`/api/feedback/${id}`, {
145
+ method: `DELETE`
146
+ });
147
+ toast.add({
148
+ title: `Feedback deleted`,
149
+ description: `The feedback has been successfully removed`,
150
+ color: `success`,
151
+ icon: `i-lucide-check`
152
+ });
153
+ return true;
154
+ } catch (error) {
155
+ console.error(`Failed to delete feedback:`, error);
156
+ toast.add({
157
+ title: `Failed to delete feedback`,
158
+ description: `Please try again later`,
159
+ color: `error`,
160
+ icon: `i-lucide-circle-alert`
161
+ });
162
+ return false;
163
+ }
164
+ }
165
+ return {
166
+ deleteFeedback
167
+ };
168
+ }
169
+ export function useFeedbackForm(options) {
170
+ const route = useRoute();
171
+ const toast = useToast();
172
+ const formState = reactive({
173
+ rating: null,
174
+ feedback: ``
175
+ });
176
+ const isExpanded = ref(false);
177
+ const isSubmitted = ref(false);
178
+ const isSubmitting = ref(false);
179
+ function cancelFeedback() {
180
+ formState.rating = null;
181
+ formState.feedback = ``;
182
+ isExpanded.value = false;
183
+ }
184
+ function handleRatingSelect(rating) {
185
+ if (isSubmitted.value) return;
186
+ if (isExpanded.value && rating === formState.rating) {
187
+ cancelFeedback();
188
+ return;
189
+ }
190
+ formState.rating = rating;
191
+ isExpanded.value = true;
192
+ }
193
+ async function submitFeedback() {
194
+ if (!formState.rating) return;
195
+ isSubmitting.value = true;
196
+ const submission = {
197
+ rating: formState.rating,
198
+ feedback: formState.feedback.trim() || void 0,
199
+ path: route.path,
200
+ title: options.page.title,
201
+ stem: options.page.stem
202
+ };
203
+ try {
204
+ await $fetch(`/api/feedback`, {
205
+ method: `POST`,
206
+ body: submission
207
+ });
208
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
209
+ isSubmitted.value = true;
210
+ } catch {
211
+ toast.add({
212
+ title: `Failed to submit feedback`,
213
+ description: `Please try again later`,
214
+ color: `error`,
215
+ icon: `i-lucide-circle-alert`
216
+ });
217
+ } finally {
218
+ isSubmitting.value = false;
219
+ }
220
+ }
221
+ function resetFeedback() {
222
+ isSubmitted.value = false;
223
+ isExpanded.value = false;
224
+ formState.rating = null;
225
+ formState.feedback = ``;
226
+ }
227
+ watch(route, resetFeedback);
228
+ return {
229
+ formState,
230
+ isExpanded: readonly(isExpanded),
231
+ isSubmitted: readonly(isSubmitted),
232
+ isSubmitting: readonly(isSubmitting),
233
+ handleRatingSelect,
234
+ submitFeedback,
235
+ resetFeedback
236
+ };
237
+ }
@@ -0,0 +1,4 @@
1
+ export declare function useFeedbackExport(): {
2
+ exportFeedbackData: (feedbackData: FeedbackItem[]) => Promise<void>;
3
+ exportPageAnalytics: (pageAnalytics: PageAnalytic[]) => Promise<void>;
4
+ };
@@ -0,0 +1,150 @@
1
+ export function useFeedbackExport() {
2
+ const toast = useToast();
3
+ function formatDateForCSV(date) {
4
+ const d = typeof date === `string` ? new Date(date) : date;
5
+ return d.toLocaleDateString(`en-US`, {
6
+ year: `numeric`,
7
+ month: `2-digit`,
8
+ day: `2-digit`,
9
+ hour: `2-digit`,
10
+ minute: `2-digit`,
11
+ second: `2-digit`
12
+ });
13
+ }
14
+ function convertToCSV(data) {
15
+ if (!data || data.length === 0) {
16
+ return `No data to export`;
17
+ }
18
+ const headers = [
19
+ `ID`,
20
+ `Rating`,
21
+ `Score`,
22
+ `Rating Label`,
23
+ `Feedback`,
24
+ `Page Path`,
25
+ `Page Title`,
26
+ `Page Stem`,
27
+ `Country`,
28
+ `Created At`,
29
+ `Updated At`
30
+ ];
31
+ const rows = data.map((item) => {
32
+ const ratingOption = FEEDBACK_OPTIONS.find(
33
+ (opt) => opt.value === item.rating
34
+ );
35
+ return [
36
+ item.id,
37
+ item.rating,
38
+ ratingOption?.score || 0,
39
+ ratingOption?.label || `Unknown`,
40
+ // Escape quotes
41
+ `"${(item.feedback || ``).replace(/"/g, `""`)}"`,
42
+ item.path,
43
+ // Escape quotes
44
+ `"${item.title.replace(/"/g, `""`)}"`,
45
+ item.stem,
46
+ item.country || ``,
47
+ formatDateForCSV(item.createdAt),
48
+ formatDateForCSV(item.updatedAt)
49
+ ];
50
+ });
51
+ return [headers, ...rows].map((row) => row.join(`,`)).join(`
52
+ `);
53
+ }
54
+ function downloadCSV(csvContent, filename) {
55
+ const blob = new Blob([csvContent], {
56
+ type: `text/csv;charset=utf-8;`
57
+ });
58
+ const link = document.createElement(`a`);
59
+ if (link.download !== void 0) {
60
+ const url = URL.createObjectURL(blob);
61
+ link.setAttribute(`href`, url);
62
+ link.setAttribute(`download`, filename);
63
+ link.style.visibility = `hidden`;
64
+ document.body.appendChild(link);
65
+ link.click();
66
+ document.body.removeChild(link);
67
+ URL.revokeObjectURL(url);
68
+ }
69
+ }
70
+ async function exportFeedbackData(feedbackData) {
71
+ try {
72
+ const csvContent = convertToCSV(feedbackData);
73
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().split(`T`)[0];
74
+ const filename = `feedback-export-${timestamp}.csv`;
75
+ downloadCSV(csvContent, filename);
76
+ toast.add({
77
+ title: `Export successful`,
78
+ description: `${feedbackData.length} feedback entries exported to ${filename}`,
79
+ color: `success`,
80
+ icon: `i-lucide-download`
81
+ });
82
+ } catch (error) {
83
+ console.error(`Export failed:`, error);
84
+ toast.add({
85
+ title: `Export failed`,
86
+ description: `Unable to export feedback data. Please try again.`,
87
+ color: `error`,
88
+ icon: `i-lucide-circle-alert`
89
+ });
90
+ }
91
+ }
92
+ async function exportPageAnalytics(pageAnalytics) {
93
+ try {
94
+ if (!pageAnalytics || pageAnalytics.length === 0) {
95
+ toast.add({
96
+ title: `No data to export`,
97
+ description: `No page analytics data available for export.`,
98
+ color: `warning`,
99
+ icon: `i-lucide-info`
100
+ });
101
+ return;
102
+ }
103
+ const headers = [
104
+ `Page Path`,
105
+ `Page Title`,
106
+ `Total Feedback`,
107
+ `Positive Feedback`,
108
+ `Negative Feedback`,
109
+ `Average Score`,
110
+ `Positive Percentage`,
111
+ `Created At`,
112
+ `Updated At`
113
+ ];
114
+ const rows = pageAnalytics.map((page) => [
115
+ page.path,
116
+ `"${page.lastFeedback.title.replace(/"/g, `""`)}"`,
117
+ page.total,
118
+ page.positive,
119
+ page.negative,
120
+ page.averageScore,
121
+ page.positivePercentage,
122
+ formatDateForCSV(page.createdAt),
123
+ formatDateForCSV(page.updatedAt)
124
+ ]);
125
+ const csvContent = [headers, ...rows].map((row) => row.join(`,`)).join(`
126
+ `);
127
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().split(`T`)[0];
128
+ const filename = `page-analytics-export-${timestamp}.csv`;
129
+ downloadCSV(csvContent, filename);
130
+ toast.add({
131
+ title: `Export successful`,
132
+ description: `${pageAnalytics.length} page analytics exported to ${filename}`,
133
+ color: `success`,
134
+ icon: `i-lucide-download`
135
+ });
136
+ } catch (error) {
137
+ console.error(`Export failed:`, error);
138
+ toast.add({
139
+ title: `Export failed`,
140
+ description: `Unable to export page analytics. Please try again.`,
141
+ color: `error`,
142
+ icon: `i-lucide-circle-alert`
143
+ });
144
+ }
145
+ }
146
+ return {
147
+ exportFeedbackData,
148
+ exportPageAnalytics
149
+ };
150
+ }
@@ -0,0 +1,14 @@
1
+ export interface Notification {
2
+ id: number
3
+ unread?: boolean
4
+ sender: User
5
+ body: string
6
+ date: string
7
+ }
8
+
9
+ export type Period = `daily` | `weekly` | `monthly`
10
+
11
+ export interface Range {
12
+ start: Date
13
+ end: Date
14
+ }
@@ -0,0 +1,3 @@
1
+ import { type ClassValue } from "clsx";
2
+ export declare function cn(...inputs: ClassValue[]): string;
3
+ export type ObjectValues<T> = T[keyof T];
@@ -0,0 +1,5 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ export function cn(...inputs) {
4
+ return twMerge(clsx(inputs));
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rimelight-components",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "My new Nuxt module",
5
5
  "repository": "Rimelight Entertainment/rimelight-components",
6
6
  "license": "MIT",
@@ -9,6 +9,14 @@
9
9
  ".": {
10
10
  "types": "./dist/types.d.mts",
11
11
  "import": "./dist/module.mjs"
12
+ },
13
+ "./runtime/*": "./dist/runtime/*",
14
+ "./components/*": "./dist/runtime/components/*",
15
+ "./composables/*": "./dist/runtime/composables/*",
16
+ "./types/*": "./dist/runtime/types/*",
17
+ "./utils/*": {
18
+ "types": "./dist/runtime/utils/*.d.ts",
19
+ "import": "./dist/runtime/utils/*.js"
12
20
  }
13
21
  },
14
22
  "main": "./dist/module.mjs",
@@ -41,6 +49,7 @@
41
49
  "@nuxt/image": "^1.11.0",
42
50
  "@nuxt/kit": "^4.1.3",
43
51
  "@nuxt/ui": "^4.0.1",
52
+ "@vueuse/core": "^13.9.0",
44
53
  "date-fns": "^4.1.0",
45
54
  "nuxt": "^4.1.3",
46
55
  "tailwind-variants": "^3.1.1",