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.
- package/package.json +6 -1
- package/ui/components/Layout.jsx +138 -831
- package/ui/components/Sidebar.jsx +820 -0
- package/ui/index.cjs +3 -0
- package/ui/index.js +2 -0
- package/utils/utils/db.cjs +40 -2
- package/utils/utils/db.js +43 -2
- package/vue/components/Badge.vue +64 -0
- package/vue/components/Button.vue +161 -0
- package/vue/components/Checkbox.vue +97 -0
- package/vue/components/Input.vue +341 -0
- package/vue/components/Pagination.vue +127 -0
- package/vue/components/Select.vue +755 -0
- package/vue/components/Sidebar.vue +954 -0
- package/vue/components/Skeleton.vue +91 -0
- package/vue/components/Spinner.vue +146 -0
- package/vue/components/TabBar.vue +176 -0
- package/vue/components/Table.vue +323 -0
- package/vue/components/XTable.vue +119 -0
- package/vue/composables/useAutoSkeletonRows.ts +71 -0
- package/vue/index.d.ts +58 -0
- package/vue/index.js +56 -0
|
@@ -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>
|