Dev/v1.2 (#4)
Build Docker Image / build-backend (push) Successful in 1m56s
Build Docker Image / build-frontend (push) Successful in 1m55s

Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-07-06 12:50:55 +08:00
parent e77712105c
commit d06c4701a4
190 changed files with 5365 additions and 26981 deletions
@@ -3,6 +3,7 @@ import { SheetRoot, SheetContent } from '@/components/ui/sheet'
import { Lucide } from '@/components/ui/lucide'
import { useColorSchemeStore, type ColorSchemes } from '@/stores/color-scheme'
import { useDarkModeStore } from '@/stores/dark-mode'
import { applyColorSchemeClass, applyDarkModeClass } from '@/utils/applyAppearance'
import { ref } from 'vue'
const themeSwitcherSheet = ref(false)
@@ -10,30 +11,19 @@ const setThemeSwitcherSheet = (value: boolean) => {
themeSwitcherSheet.value = value
}
const setColorSchemeClass = () => {
const el = document.querySelectorAll('html')[0]
el?.setAttribute('data-theme', useColorSchemeStore().colorSchemeValue)
if (useDarkModeStore().darkModeValue) el?.classList.add('dark')
}
const colorSchemeStore = useColorSchemeStore()
const switchColorScheme = (colorScheme: ColorSchemes) => {
useColorSchemeStore().setColorScheme(colorScheme)
setColorSchemeClass()
applyColorSchemeClass()
setThemeSwitcherSheet(false)
}
setColorSchemeClass()
const setDarkModeClass = () => {
const el = document.querySelectorAll('html')[0]
useDarkModeStore().darkModeValue ? el?.classList.add('dark') : el?.classList.remove('dark')
}
const darkModeStore = useDarkModeStore()
const switchDarkMode = (darkMode: boolean) => {
useDarkModeStore().setDarkMode(darkMode)
setDarkModeClass()
applyDarkModeClass()
setThemeSwitcherSheet(false)
}
setDarkModeClass()
const colorSchemes: Array<ColorSchemes> = ['default', '1', '2', '3', '4', '5']
@@ -7,30 +7,28 @@ import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { paginationRoot } from "@mykopkb/core/styles/pagination.styles";
const {
class: className,
asChild = false,
count,
pageSize,
siblingCount,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const props = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(pagination.machine, {
...props,
count,
pageSize,
siblingCount,
id: crypto.randomUUID(),
const paginationId = crypto.randomUUID();
const machineProps = computed(() => {
const { class: _class, asChild: _asChild, id: _id, ...rest } = props;
return {
...rest,
id: paginationId,
};
});
const service = useMachine(pagination.machine, machineProps);
const api = computed(() => pagination.connect(service, normalizeProps));
provide("paginationApi", api);
</script>
<template>
<Slot :class="cn(paginationRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot v-if="asChild" />
<Slot :class="cn(paginationRoot, props.class)" v-bind="{ ...$attrs, ...api.getRootProps() }">
<slot v-if="props.asChild" />
<div v-else>
<slot />
</div>
@@ -1,7 +1,7 @@
export const paginationRoot = 'flex gap-1'
export const paginationItem = [
'h-10 px-4 py-2 inline-flex items-center justify-center rounded-xl cursor-pointer hover:bg-foreground/5',
'data-[selected]:border data-[selected]:bg-background data-[selected]:border-foreground/10 data-[selected]:font-medium data-[selected]:shadow-md/5',
'h-10 min-w-10 px-3 py-2 inline-flex items-center justify-center rounded-xl cursor-pointer text-foreground hover:bg-foreground/5',
'data-[selected]:border data-[selected]:bg-primary/10 data-[selected]:border-primary/30 data-[selected]:text-primary data-[selected]:font-semibold data-[selected]:shadow-md/5',
'data-[disabled]:opacity-70',
]
export const paginationPrevTrigger = paginationItem
+35 -30
View File
@@ -94,25 +94,29 @@ const props = withDefaults(defineProps<{
items: TableItem[];
loading?: boolean;
itemsPerPage?: number;
page?: number;
itemKey?: string;
pagination?: PaginationData | null;
showPagination?: boolean;
currentSort?: SortConfig[];
exportable?: boolean;
exportFileName?: string;
/** When true, table body scrolls inside a max-height box instead of growing the page. */
scrollable?: boolean;
/** Max height of the scrollable table area (CSS value). */
maxHeight?: string;
}>(), {
items: () => [],
headers: () => [],
loading: false,
itemsPerPage: 10,
page: 1,
itemKey: 'id',
pagination: null,
showPagination: false,
currentSort: () => [],
exportable: false,
exportFileName: 'table-data'
exportFileName: 'table-data',
scrollable: true,
maxHeight: 'min(70dvh, 640px)',
});
// Define model props
@@ -122,7 +126,6 @@ const itemsPerPageModel = defineModel<number>('items-per-page');
// Emits
const emit = defineEmits<{
'update:sort-by': [value: SortConfig[]];
'update:page': [value: number];
'update:items-per-page': [value: number];
'update:column-widths': [value: Record<string, number>];
'export-error': [error: Error];
@@ -449,7 +452,6 @@ const setItemsPerPageValue = (details: { value: string[] }) => {
const handlePageChange = (details: { page: number }) => {
pageModel.value = details.page;
emit('update:page', details.page);
};
const getItemKey = (item: TableItem, index: number) => {
@@ -461,16 +463,18 @@ const currentPage = computed(
() => pageModel.value ?? props.pagination?.current_page ?? 1,
);
const isFirstPage = computed(() => currentPage.value <= 1);
const isLastPage = computed(
() => currentPage.value >= (props.pagination?.last_page ?? 1),
);
const slots = useSlots();
const showTableToolbar = computed(() => props.exportable || !!slots.toolbar);
const scrollContainerStyle = computed(() => {
if (!props.scrollable) {
return undefined;
}
return { maxHeight: props.maxHeight };
});
const recordSummary = computed(() => {
if (!props.pagination) return null;
@@ -743,12 +747,20 @@ watch(sortBy, (newSort) => {
</div>
</div>
<div class="relative px-4 pt-3" :class="loading && 'pointer-events-none opacity-50'">
<Table variant="boxed" class="custom-data-table border-separate border-spacing-y-2.5">
<div
class="relative px-4 pt-3"
:class="[
loading && 'pointer-events-none opacity-50',
scrollable && 'data-table-scroll-area overflow-auto',
]"
:style="scrollContainerStyle"
>
<Table variant="boxed" :class="cn('custom-data-table border-separate border-spacing-y-2.5', !scrollable && 'overflow-hidden')">
<TableHeader>
<TableRow class="border-0 hover:bg-transparent">
<TableHead v-for="header in sortedHeaders" :key="header.key" :class="cn(
'group/head relative bg-primary text-primary-foreground font-semibold border-y border-primary/20 first:rounded-tl-xl first:border-s last:rounded-tr-xl last:border-e',
scrollable && 'sticky top-0 z-10',
headerAlignClass(header.align),
)" :style="{ width: `${header.width}px`, minWidth: `${header.width}px` }">
<div v-if="header.sortable" class="flex items-center gap-2 cursor-pointer select-none" role="button"
@@ -851,28 +863,21 @@ watch(sortBy, (newSort) => {
</template>
</div>
<PaginationRoot class="flex items-center gap-2" :count="pagination.total" :pageSize="pagination.per_page"
:page="currentPage" :siblingCount="1" :onPageChange="handlePageChange">
<PaginationPrevTrigger asChild>
<Button size="sm" look="outline" variant="secondary" :disabled="isFirstPage">
<ArrowLeft class="size-4" />
</Button>
<PaginationRoot class="flex items-center gap-2" :count="pagination.total" :page-size="pagination.per_page"
:page="currentPage" :sibling-count="1" :on-page-change="handlePageChange">
<PaginationPrevTrigger>
<ArrowLeft class="size-4" />
</PaginationPrevTrigger>
<PaginationContext v-slot="{ pagination: paginationApi }">
<template v-for="(pageItem, index) in paginationApi?.pages" :key="index">
<PaginationItem v-if="pageItem.type === 'page'" v-bind="{ ...pageItem }" asChild>
<Button size="sm" :look="pageItem.value === currentPage ? 'filled' : 'outline'"
:variant="pageItem.value === currentPage ? 'primary' : 'secondary'">
{{ pageItem.value }}
</Button>
<PaginationItem v-if="pageItem.type === 'page'" v-bind="{ ...pageItem }">
{{ pageItem.value }}
</PaginationItem>
<PaginationEllipsis v-else :index="index" />
</template>
</PaginationContext>
<PaginationNextTrigger asChild>
<Button size="sm" look="outline" variant="secondary" :disabled="isLastPage">
<ArrowRight class="size-4" />
</Button>
<PaginationNextTrigger>
<ArrowRight class="size-4" />
</PaginationNextTrigger>
</PaginationRoot>
</div>
@@ -881,8 +886,8 @@ watch(sortBy, (newSort) => {
</template>
<style scoped>
.custom-data-table {
overflow: hidden;
.data-table-scroll-area > :deep(div) {
overflow: visible;
}
.sortable-indicator {