cynx-lib 1.3.50 → 1.3.52

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.
@@ -0,0 +1,91 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ width?: string | number;
7
+ height?: string | number;
8
+ circle?: boolean;
9
+ size?: string | number;
10
+ radius?: string | number;
11
+ className?: string;
12
+ }>(),
13
+ {
14
+ width: "100%",
15
+ height: 14,
16
+ circle: false,
17
+ size: undefined,
18
+ radius: 4,
19
+ className: "",
20
+ }
21
+ );
22
+
23
+ const formatSize = (val?: string | number) => {
24
+ if (val === undefined || val === null) return undefined;
25
+ if (typeof val === "number") return `${val}px`;
26
+ return val;
27
+ };
28
+
29
+ const computedWidth = computed(() => {
30
+ if (props.circle) return formatSize(props.size || props.height || 24);
31
+ return formatSize(props.width);
32
+ });
33
+
34
+ const computedHeight = computed(() => {
35
+ if (props.circle) return formatSize(props.size || props.height || 24);
36
+ return formatSize(props.height);
37
+ });
38
+
39
+ const computedRadius = computed(() => {
40
+ if (props.circle) return "50%";
41
+ return formatSize(props.radius);
42
+ });
43
+ </script>
44
+
45
+ <template>
46
+ <div
47
+ :class="['cynx-skeleton', className]"
48
+ :style="{
49
+ width: computedWidth,
50
+ height: computedHeight,
51
+ borderRadius: computedRadius,
52
+ }"
53
+ />
54
+ </template>
55
+
56
+ <style>
57
+ @keyframes cynx-sk-shimmer {
58
+ 0% {
59
+ background-position: -600px 0;
60
+ }
61
+ 100% {
62
+ background-position: 600px 0;
63
+ }
64
+ }
65
+
66
+ .cynx-skeleton {
67
+ background: linear-gradient(
68
+ 90deg,
69
+ var(--grey-1, #f0f1f5) 25%,
70
+ var(--border-light, #e4e5ec) 50%,
71
+ var(--grey-1, #f0f1f5) 75%
72
+ );
73
+ background-size: 600px 100%;
74
+ animation: cynx-sk-shimmer 1.4s ease infinite;
75
+ flex-shrink: 0;
76
+ display: inline-block;
77
+ vertical-align: middle;
78
+ }
79
+
80
+ :root.dark1 .cynx-skeleton,
81
+ :root.dark2 .cynx-skeleton,
82
+ .dark .cynx-skeleton {
83
+ background: linear-gradient(
84
+ 90deg,
85
+ var(--grey-1, #1c1f36) 25%,
86
+ var(--border-light, #2b304f) 50%,
87
+ var(--grey-1, #1c1f36) 75%
88
+ );
89
+ background-size: 600px 100%;
90
+ }
91
+ </style>
@@ -0,0 +1,146 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ size?: number;
7
+ inline?: boolean;
8
+ color?: string;
9
+ fullPage?: boolean;
10
+ variant?: "default" | "classic";
11
+ }>(),
12
+ {
13
+ size: 20,
14
+ inline: false,
15
+ color: "currentColor",
16
+ fullPage: false,
17
+ variant: "default",
18
+ }
19
+ );
20
+
21
+ const s = computed(() => (props.inline ? 14 : props.size));
22
+ </script>
23
+
24
+ <template>
25
+ <!-- Fullpage Loading Container -->
26
+ <div
27
+ v-if="fullPage"
28
+ class="fixed inset-0 bg-[var(--bg,#f8fafc)] flex flex-col items-center justify-center gap-4 z-[9999]"
29
+ >
30
+ <div
31
+ class="v-progress-circular v-progress-circular--indeterminate relative"
32
+ :style="{ width: `${s}px`, height: `${s}px` }"
33
+ >
34
+ <svg
35
+ viewBox="0 0 42.42 42.42"
36
+ class="w-full h-full absolute inset-0 -rotate-90"
37
+ >
38
+ <circle
39
+ fill="transparent"
40
+ cx="50%"
41
+ cy="50%"
42
+ r="20"
43
+ stroke="var(--grey-2,#e0e0e0)"
44
+ stroke-width="2.42"
45
+ stroke-dasharray="125.66"
46
+ stroke-dashoffset="0"
47
+ />
48
+ <circle
49
+ fill="transparent"
50
+ cx="50%"
51
+ cy="50%"
52
+ r="20"
53
+ :stroke="color === 'currentColor' ? 'var(--accent,#12675D)' : color"
54
+ stroke-width="2.42"
55
+ class="v-progress-circular__overlay"
56
+ />
57
+ </svg>
58
+ </div>
59
+ <div class="text-xs font-mono text-[var(--muted,#797b8d)]">Loading...</div>
60
+ </div>
61
+
62
+ <!-- Inline / Normal Spinner -->
63
+ <div
64
+ v-else
65
+ :class="[inline ? 'inline-flex items-center' : 'flex items-center justify-center py-12']"
66
+ >
67
+ <!-- Classic spinner variant -->
68
+ <svg
69
+ v-if="variant === 'classic'"
70
+ class="animate-spin"
71
+ :width="s"
72
+ :height="s"
73
+ viewBox="0 0 24 24"
74
+ fill="none"
75
+ :stroke="color"
76
+ stroke-width="2.5"
77
+ stroke-linecap="round"
78
+ >
79
+ <path
80
+ d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"
81
+ />
82
+ </svg>
83
+
84
+ <!-- Circular spinner variant (default) -->
85
+ <div
86
+ v-else
87
+ class="v-progress-circular v-progress-circular--indeterminate relative"
88
+ :style="{ width: `${s}px`, height: `${s}px` }"
89
+ >
90
+ <svg
91
+ viewBox="0 0 42.42 42.42"
92
+ class="w-full h-full absolute inset-0 -rotate-90"
93
+ >
94
+ <circle
95
+ fill="transparent"
96
+ cx="50%"
97
+ cy="50%"
98
+ r="20"
99
+ stroke="var(--grey-2,#e0e0e0)"
100
+ stroke-width="2.42"
101
+ stroke-dasharray="125.66"
102
+ stroke-dashoffset="0"
103
+ />
104
+ <circle
105
+ fill="transparent"
106
+ cx="50%"
107
+ cy="50%"
108
+ r="20"
109
+ :stroke="color === 'currentColor' ? 'var(--accent,#12675D)' : color"
110
+ stroke-width="2.42"
111
+ class="v-progress-circular__overlay"
112
+ />
113
+ </svg>
114
+ </div>
115
+ </div>
116
+ </template>
117
+
118
+ <style scoped>
119
+ .v-progress-circular--indeterminate > svg {
120
+ animation: progress-circular-rotate 0.8s linear infinite;
121
+ transform-origin: center center;
122
+ }
123
+ @keyframes progress-circular-rotate {
124
+ 100% {
125
+ transform: rotate(270deg);
126
+ }
127
+ }
128
+ @keyframes progress-circular-dash {
129
+ 0% {
130
+ stroke-dasharray: 1, 200;
131
+ stroke-dashoffset: 0;
132
+ }
133
+ 50% {
134
+ stroke-dasharray: 89, 200;
135
+ stroke-dashoffset: -35px;
136
+ }
137
+ 100% {
138
+ stroke-dasharray: 89, 200;
139
+ stroke-dashoffset: -124px;
140
+ }
141
+ }
142
+ .v-progress-circular--indeterminate .v-progress-circular__overlay {
143
+ animation: progress-circular-dash 1.4s ease-in-out infinite;
144
+ stroke-linecap: round;
145
+ }
146
+ </style>
@@ -0,0 +1,176 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, onMounted, nextTick } from "vue";
3
+
4
+ export interface TabItem {
5
+ id: string | number;
6
+ label?: string;
7
+ name?: string;
8
+ icon?: any;
9
+ badge?: string;
10
+ count?: number;
11
+ disabled?: boolean;
12
+ }
13
+
14
+ const props = withDefaults(
15
+ defineProps<{
16
+ tabs: TabItem[];
17
+ modelValue?: string | number;
18
+ active?: string | number;
19
+ variant?: "underline" | "pill";
20
+ padding?: string;
21
+ capitalize?: boolean;
22
+ }>(),
23
+ {
24
+ variant: "underline",
25
+ capitalize: false,
26
+ }
27
+ );
28
+
29
+ const emit = defineEmits<{
30
+ (e: "update:modelValue", value: string | number): void;
31
+ (e: "change", value: string | number): void;
32
+ (e: "tab-click", tab: TabItem): void;
33
+ }>();
34
+
35
+ const barRef = ref<HTMLElement | null>(null);
36
+ const indRef = ref<HTMLElement | null>(null);
37
+ const currentActive = ref(props.modelValue ?? props.active ?? props.tabs?.[0]?.id);
38
+
39
+ function updateIndicator() {
40
+ if (props.variant !== "underline") return;
41
+ nextTick(() => {
42
+ const bar = barRef.value;
43
+ const ind = indRef.value;
44
+ if (!bar || !ind) return;
45
+
46
+ const targetId = String(currentActive.value ?? "");
47
+ const activeBtn =
48
+ (bar.querySelector(`[data-tabid="${targetId}"]`) as HTMLElement) ||
49
+ (bar.querySelector(`[data-active="true"]`) as HTMLElement);
50
+
51
+ if (!activeBtn) return;
52
+
53
+ const barRect = bar.getBoundingClientRect();
54
+ const btnRect = activeBtn.getBoundingClientRect();
55
+ const x = btnRect.left - barRect.left;
56
+ const w = btnRect.width;
57
+
58
+ if (w > 0) {
59
+ ind.style.width = `${w}px`;
60
+ ind.style.transform = `translateX(${x}px)`;
61
+ ind.style.opacity = "1";
62
+ }
63
+ });
64
+ }
65
+
66
+ watch(
67
+ () => [props.modelValue, props.active, props.tabs],
68
+ ([newM, newA]) => {
69
+ if (newM !== undefined && newM !== null) currentActive.value = newM;
70
+ else if (newA !== undefined && newA !== null) currentActive.value = newA;
71
+ else if (props.tabs?.length && (currentActive.value === undefined || currentActive.value === null)) {
72
+ currentActive.value = props.tabs[0]?.id;
73
+ }
74
+ updateIndicator();
75
+ },
76
+ { deep: true, immediate: true }
77
+ );
78
+
79
+ onMounted(() => {
80
+ updateIndicator();
81
+ nextTick(() => {
82
+ requestAnimationFrame(updateIndicator);
83
+ setTimeout(updateIndicator, 50);
84
+ setTimeout(updateIndicator, 200);
85
+ });
86
+ window.addEventListener("resize", updateIndicator);
87
+ });
88
+
89
+ const selectTab = (tab: TabItem) => {
90
+ if (tab.disabled) return;
91
+ currentActive.value = tab.id;
92
+ emit("update:modelValue", tab.id);
93
+ emit("change", tab.id);
94
+ emit("tab-click", tab);
95
+ updateIndicator();
96
+ };
97
+ </script>
98
+
99
+ <template>
100
+ <div
101
+ ref="barRef"
102
+ :class="[
103
+ 'relative flex items-center',
104
+ variant === 'underline' ? 'overflow-visible' : 'gap-1 bg-slate-100 dark:bg-slate-800 p-1 rounded-md'
105
+ ]"
106
+ >
107
+ <!-- Underline Indicator (cynx-lib smooth bezier) -->
108
+ <div
109
+ v-if="variant === 'underline'"
110
+ ref="indRef"
111
+ class="pointer-events-none z-[2]"
112
+ style="
113
+ position: absolute;
114
+ bottom: -1px;
115
+ left: 0;
116
+ height: 2px;
117
+ width: 0px;
118
+ opacity: 0;
119
+ background: var(--accent, #12675D);
120
+ border-radius: 2px 2px 0 0;
121
+ transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1), width 0.22s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s ease;
122
+ "
123
+ />
124
+
125
+ <!-- Tab Items -->
126
+ <button
127
+ v-for="t in tabs"
128
+ :key="t.id"
129
+ type="button"
130
+ :data-tabid="t.id"
131
+ :data-active="String(currentActive) === String(t.id)"
132
+ :disabled="t.disabled"
133
+ :class="[
134
+ 'inline-flex items-center gap-2 text-xs font-semibold whitespace-nowrap transition-colors relative z-[1] select-none',
135
+ String(currentActive) === String(t.id)
136
+ ? 'text-[#12675D] dark:text-teal-400 font-bold'
137
+ : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white font-medium',
138
+ t.disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'
139
+ ]"
140
+ :style="{
141
+ padding: padding || (variant === 'underline' ? '10px 16px' : '6px 12px'),
142
+ border: 'none',
143
+ background: 'none',
144
+ textTransform: capitalize ? 'capitalize' : 'none',
145
+ }"
146
+ @click="selectTab(t)"
147
+ >
148
+ <slot name="tab" :tab="t" :is-active="String(currentActive) === String(t.id)">
149
+ <component
150
+ :is="t.icon"
151
+ v-if="t.icon"
152
+ class="h-4 w-4"
153
+ :class="String(currentActive) === String(t.id) ? 'text-[#12675D] dark:text-teal-400' : 'text-slate-400'"
154
+ />
155
+ <span>{{ t.label || t.name || t.id }}</span>
156
+ <span
157
+ v-if="t.badge"
158
+ class="px-1.5 py-0.5 rounded-full text-[9px] font-bold bg-[#12675D] text-white leading-none"
159
+ >
160
+ {{ t.badge }}
161
+ </span>
162
+ <span
163
+ v-if="t.count != null && t.count > 0"
164
+ :class="[
165
+ 'px-1.5 py-0.5 rounded-full text-[10px] font-bold leading-none',
166
+ String(currentActive) === String(t.id)
167
+ ? 'bg-[#12675D]/10 text-[#12675D]'
168
+ : 'bg-slate-100 text-slate-500'
169
+ ]"
170
+ >
171
+ {{ t.count }}
172
+ </span>
173
+ </slot>
174
+ </button>
175
+ </div>
176
+ </template>