cynx-lib 1.3.84 → 1.3.85
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/gen.js +77 -77
- package/package.json +5 -1
- package/ui/components/Drawer.jsx +208 -208
- package/ui/components/Pagination.jsx +145 -145
- package/ui/components/PrivacyBlur.jsx +603 -603
- package/utils/index.cjs +5 -0
- package/utils/index.js +22 -20
- package/utils/utils/duration.js +139 -139
- package/utils/utils/oidc.js +214 -214
- package/utils/utils/realtime.js +1634 -1634
- package/utils/utils/session.cjs +15 -2
- package/utils/utils/session.js +67 -58
- package/utils/utils/session_manager.cjs +490 -0
- package/utils/utils/session_manager.js +549 -0
- package/utils/utils/storage.js +393 -393
- package/utils/utils/ui.js +111 -111
- package/vue/components/Badge.vue +79 -79
- package/vue/components/Button.vue +209 -209
- package/vue/components/Checkbox.vue +103 -103
- package/vue/components/CircularProgress.vue +383 -383
- package/vue/components/Divider.vue +94 -94
- package/vue/components/Drawer.vue +243 -243
- package/vue/components/InfoBadge.vue +223 -223
- package/vue/components/Input.vue +341 -341
- package/vue/components/LinearProgress.vue +268 -268
- package/vue/components/Modal.vue +226 -226
- package/vue/components/Pagination.vue +199 -199
- package/vue/components/Select.vue +713 -713
- package/vue/components/Sidebar.vue +955 -955
- package/vue/components/Skeleton.vue +91 -91
- package/vue/components/Spinner.vue +263 -263
- package/vue/components/StatusTag.vue +89 -89
- package/vue/components/TabBar.vue +176 -176
- package/vue/components/Table.vue +388 -388
- package/vue/components/TimePicker.vue +399 -399
- package/vue/components/Toggle.vue +115 -115
- package/vue/components/XTable.vue +119 -119
- package/vue/composables/useAutoSkeletonRows.ts +71 -71
- package/vue/index.d.ts +67 -67
- package/vue/index.js +86 -86
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed } from "vue";
|
|
3
|
-
|
|
4
|
-
const STATUS_COLORS: Record<string, string> = {
|
|
5
|
-
DR: "var(--drive, #28C76F)",
|
|
6
|
-
SB: "var(--break, #FFB200)",
|
|
7
|
-
ON: "var(--shift, #0F60FF)",
|
|
8
|
-
OFF: "var(--muted, #8B909A)",
|
|
9
|
-
PC: "var(--muted, #8B909A)",
|
|
10
|
-
YM: "var(--shift, #0F60FF)",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const props = withDefaults(
|
|
14
|
-
defineProps<{
|
|
15
|
-
status?: string | null;
|
|
16
|
-
color?: string;
|
|
17
|
-
width?: number | string;
|
|
18
|
-
height?: number | string;
|
|
19
|
-
radius?: number | string;
|
|
20
|
-
plain?: boolean;
|
|
21
|
-
}>(),
|
|
22
|
-
{
|
|
23
|
-
status: "",
|
|
24
|
-
width: 36,
|
|
25
|
-
height: 20,
|
|
26
|
-
radius: 6,
|
|
27
|
-
plain: false,
|
|
28
|
-
}
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const normalizedKey = computed(() =>
|
|
32
|
-
(props.status || "").replace(/\s+/g, "_").toUpperCase().trim()
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const isPrimaryStatus = computed(() => {
|
|
36
|
-
if (props.color) return true;
|
|
37
|
-
return Boolean(STATUS_COLORS[normalizedKey.value]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const statusColor = computed(() => {
|
|
41
|
-
if (props.color) return props.color;
|
|
42
|
-
return STATUS_COLORS[normalizedKey.value] || "var(--muted, #8B909A)";
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const isShortStatus = computed(() => (props.status || "").trim().length <= 3);
|
|
46
|
-
|
|
47
|
-
const boxWidth = computed(() =>
|
|
48
|
-
typeof props.width === "number" ? `${props.width}px` : props.width
|
|
49
|
-
);
|
|
50
|
-
const boxHeight = computed(() =>
|
|
51
|
-
typeof props.height === "number" ? `${props.height}px` : props.height
|
|
52
|
-
);
|
|
53
|
-
const borderRadius = computed(() =>
|
|
54
|
-
typeof props.radius === "number" ? `${props.radius}px` : props.radius
|
|
55
|
-
);
|
|
56
|
-
</script>
|
|
57
|
-
|
|
58
|
-
<template>
|
|
59
|
-
<span
|
|
60
|
-
v-if="status && plain"
|
|
61
|
-
class="inline-flex items-center text-[11.5px] font-bold select-none whitespace-nowrap tracking-wide"
|
|
62
|
-
:style="{
|
|
63
|
-
color: statusColor,
|
|
64
|
-
}"
|
|
65
|
-
>
|
|
66
|
-
{{ status }}
|
|
67
|
-
</span>
|
|
68
|
-
<span
|
|
69
|
-
v-else-if="status && isPrimaryStatus"
|
|
70
|
-
class="inline-flex items-center justify-center text-[11px] font-bold text-white tracking-wide select-none shadow-sm flex-shrink-0"
|
|
71
|
-
:style="{
|
|
72
|
-
width: isShortStatus ? boxWidth : 'auto',
|
|
73
|
-
minWidth: boxWidth,
|
|
74
|
-
padding: isShortStatus ? '0' : '0 6px',
|
|
75
|
-
height: boxHeight,
|
|
76
|
-
borderRadius: borderRadius,
|
|
77
|
-
backgroundColor: statusColor,
|
|
78
|
-
}"
|
|
79
|
-
>
|
|
80
|
-
{{ status }}
|
|
81
|
-
</span>
|
|
82
|
-
<span
|
|
83
|
-
v-else-if="status"
|
|
84
|
-
class="text-[12px] font-semibold text-[var(--muted,#8B909A)] select-none truncate"
|
|
85
|
-
>
|
|
86
|
-
{{ status }}
|
|
87
|
-
</span>
|
|
88
|
-
<span v-else class="text-slate-400 select-none">-</span>
|
|
89
|
-
</template>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
|
|
4
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
5
|
+
DR: "var(--drive, #28C76F)",
|
|
6
|
+
SB: "var(--break, #FFB200)",
|
|
7
|
+
ON: "var(--shift, #0F60FF)",
|
|
8
|
+
OFF: "var(--muted, #8B909A)",
|
|
9
|
+
PC: "var(--muted, #8B909A)",
|
|
10
|
+
YM: "var(--shift, #0F60FF)",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(
|
|
14
|
+
defineProps<{
|
|
15
|
+
status?: string | null;
|
|
16
|
+
color?: string;
|
|
17
|
+
width?: number | string;
|
|
18
|
+
height?: number | string;
|
|
19
|
+
radius?: number | string;
|
|
20
|
+
plain?: boolean;
|
|
21
|
+
}>(),
|
|
22
|
+
{
|
|
23
|
+
status: "",
|
|
24
|
+
width: 36,
|
|
25
|
+
height: 20,
|
|
26
|
+
radius: 6,
|
|
27
|
+
plain: false,
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const normalizedKey = computed(() =>
|
|
32
|
+
(props.status || "").replace(/\s+/g, "_").toUpperCase().trim()
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const isPrimaryStatus = computed(() => {
|
|
36
|
+
if (props.color) return true;
|
|
37
|
+
return Boolean(STATUS_COLORS[normalizedKey.value]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const statusColor = computed(() => {
|
|
41
|
+
if (props.color) return props.color;
|
|
42
|
+
return STATUS_COLORS[normalizedKey.value] || "var(--muted, #8B909A)";
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const isShortStatus = computed(() => (props.status || "").trim().length <= 3);
|
|
46
|
+
|
|
47
|
+
const boxWidth = computed(() =>
|
|
48
|
+
typeof props.width === "number" ? `${props.width}px` : props.width
|
|
49
|
+
);
|
|
50
|
+
const boxHeight = computed(() =>
|
|
51
|
+
typeof props.height === "number" ? `${props.height}px` : props.height
|
|
52
|
+
);
|
|
53
|
+
const borderRadius = computed(() =>
|
|
54
|
+
typeof props.radius === "number" ? `${props.radius}px` : props.radius
|
|
55
|
+
);
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<span
|
|
60
|
+
v-if="status && plain"
|
|
61
|
+
class="inline-flex items-center text-[11.5px] font-bold select-none whitespace-nowrap tracking-wide"
|
|
62
|
+
:style="{
|
|
63
|
+
color: statusColor,
|
|
64
|
+
}"
|
|
65
|
+
>
|
|
66
|
+
{{ status }}
|
|
67
|
+
</span>
|
|
68
|
+
<span
|
|
69
|
+
v-else-if="status && isPrimaryStatus"
|
|
70
|
+
class="inline-flex items-center justify-center text-[11px] font-bold text-white tracking-wide select-none shadow-sm flex-shrink-0"
|
|
71
|
+
:style="{
|
|
72
|
+
width: isShortStatus ? boxWidth : 'auto',
|
|
73
|
+
minWidth: boxWidth,
|
|
74
|
+
padding: isShortStatus ? '0' : '0 6px',
|
|
75
|
+
height: boxHeight,
|
|
76
|
+
borderRadius: borderRadius,
|
|
77
|
+
backgroundColor: statusColor,
|
|
78
|
+
}"
|
|
79
|
+
>
|
|
80
|
+
{{ status }}
|
|
81
|
+
</span>
|
|
82
|
+
<span
|
|
83
|
+
v-else-if="status"
|
|
84
|
+
class="text-[12px] font-semibold text-[var(--muted,#8B909A)] select-none truncate"
|
|
85
|
+
>
|
|
86
|
+
{{ status }}
|
|
87
|
+
</span>
|
|
88
|
+
<span v-else class="text-slate-400 select-none">-</span>
|
|
89
|
+
</template>
|
|
@@ -1,176 +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>
|
|
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>
|