Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,61 @@
<script setup>
import TabBar from './TabBar.vue';
const defaultTabs = [
{ label: 'All articles', count: 24 },
{ label: 'Mine', count: 13 },
{ label: 'Draft', count: 5 },
{ label: 'Archived', count: 11 },
{ label: 'Unpublished', count: 23 },
{ label: 'Published', count: 100 },
{ label: 'Scheduled', count: 49 },
];
const customTabs = [
{ label: 'Active', count: 10 },
{ label: 'Pending', count: 5 },
{ label: 'Completed', count: 15 },
];
const handleTabChanged = tab => {
console.log('Tab changed:', tab);
};
</script>
<template>
<Story title="Components/TabBar" :layout="{ type: 'grid', width: '920px' }">
<Variant title="Default">
<div class="p-4 bg-n-background">
<TabBar :tabs="defaultTabs" @tab-changed="handleTabChanged" />
</div>
</Variant>
<Variant title="Custom Tabs with Initial Tab">
<div class="p-4 bg-n-background">
<TabBar
:tabs="customTabs"
:initial-active-tab="1"
@tab-changed="handleTabChanged"
/>
</div>
</Variant>
<Variant title="No Counts">
<div class="p-4 bg-n-background">
<TabBar
:tabs="[{ label: 'Tab 1' }, { label: 'Tab 2' }, { label: 'Tab 3' }]"
@tab-changed="handleTabChanged"
/>
</div>
</Variant>
<Variant title="Single Tab">
<div class="p-4 bg-n-background">
<TabBar
:tabs="[{ label: 'Single Tab', count: 42 }]"
@tab-changed="handleTabChanged"
/>
</div>
</Variant>
</Story>
</template>

View File

@@ -0,0 +1,105 @@
<script setup>
import { computed, ref, onMounted, nextTick, watch } from 'vue';
import { useResizeObserver } from '@vueuse/core';
const props = defineProps({
initialActiveTab: {
type: Number,
default: 0,
},
tabs: {
type: Array,
required: true,
validator: value => {
return value.every(
tab =>
typeof tab.label === 'string' &&
(tab.count ? typeof tab.count === 'number' : true)
);
},
},
});
const emit = defineEmits(['tabChanged']);
const activeTab = computed(() => props.initialActiveTab);
const tabRefs = ref([]);
const indicatorStyle = ref({});
const enableTransition = ref(false);
const activeElement = computed(() => tabRefs.value[activeTab.value]);
const updateIndicator = () => {
nextTick(() => {
if (!activeElement.value) return;
indicatorStyle.value = {
left: `${activeElement.value.offsetLeft}px`,
width: `${activeElement.value.offsetWidth}px`,
};
});
};
useResizeObserver(activeElement, updateIndicator);
// Watch for prop/tabs changes to update indicator position
watch([() => props.initialActiveTab, () => props.tabs], updateIndicator, {
immediate: true,
});
onMounted(() => {
nextTick(() => {
enableTransition.value = true;
});
});
const selectTab = index => {
emit('tabChanged', props.tabs[index]);
};
const showDivider = index => {
return (
// Show dividers after the active tab, but not after the last tab
(index > activeTab.value && index < props.tabs.length - 1) ||
// Show dividers before the active tab, but not immediately before it and not before the first tab
(index < activeTab.value - 1 && index > -1)
);
};
</script>
<template>
<div
class="relative flex items-center h-8 rounded-lg bg-n-alpha-1 dark:bg-n-solid-1 w-fit transition-all duration-200 ease-out has-[button:active]:scale-[1.01]"
>
<div
class="absolute rounded-lg bg-n-solid-active shadow-sm pointer-events-none h-8 outline-1 outline outline-n-container inset-y-0"
:class="{ 'transition-all duration-300 ease-out': enableTransition }"
:style="indicatorStyle"
/>
<template v-for="(tab, index) in tabs" :key="index">
<button
:ref="el => (tabRefs[index] = el)"
class="relative z-10 px-4 truncate py-1.5 text-sm border-0 outline-1 outline-transparent rounded-lg transition-all duration-200 ease-out hover:text-n-brand active:scale-[1.02]"
:class="[
activeTab === index
? 'text-n-blue-11 scale-100'
: 'text-n-slate-10 scale-[0.98]',
]"
@click="selectTab(index)"
>
{{ tab.label }} {{ tab.count ? `(${tab.count})` : '' }}
</button>
<div
v-if="index < tabs.length - 1"
class="w-px h-3.5 rounded my-auto transition-colors duration-300 ease-in-out"
:class="
showDivider(index)
? 'bg-n-strong'
: 'bg-transparent dark:bg-transparent'
"
/>
</template>
</div>
</template>