Merge branch 'dev' into feat/page-cleaning

This commit is contained in:
Andrian Roshandy
2025-11-26 08:24:02 +07:00
2 changed files with 189 additions and 0 deletions
@@ -0,0 +1,132 @@
<script setup lang="ts">
import type { ContentHeader } from './index'
import { refDebounced } from '@vueuse/core'
const props = defineProps<ContentHeader>()
const emit = defineEmits<{
'update:searchModelValue': [value: string]
'search': [value: string]
}>()
// Quick search
const search = ref(props.quickSearchNav?.modelValue || props.refSearchNav?.modelValue || '')
// const qsModelValue = ref(props.quickSearchNav?.modelValue || '')
// const rsModelValue = ref(props.refSearchNav?.modelValue || '')
const debouncedSearch = refDebounced(search, props.quickSearchNav?.debounceDuration || 500)
// Computed search model for v-model
const searchModel = computed({
get: () => search.value,
set: (value: string) => {
search.value = value
emit('update:searchModelValue', value)
},
})
// Watch for external changes to modelValue
watch(() => props.quickSearchNav?.modelValue, (newValue) => {
if (newValue !== props.quickSearchNav?.modelValue) {
search.value = newValue || ''
}
})
// Watch debounced search and emit search event
watch(debouncedSearch, (newValue) => {
const minLength = props.quickSearchNav?.minLength || 3
// Only search if meets minimum length or empty (to clear search)
if (newValue.length === 0 || newValue.length >= minLength) {
emit('search', newValue)
props.refSearchNav?.onInput(newValue)
}
})
// Handle clear search
function clearSearch() {
searchModel.value = ''
props.quickSearchNav?.onClear()
props.refSearchNav?.onClear()
}
</script>
<template>
<div class="flex items-center justify-between pb-4 2xl:pb-5 ">
<div class="flex items-center">
<div class="ml-3 text-lg font-semibold text-gray-900">
<Icon v-if="icon" :name="icon" class="mr-2 size-4 md:size-6 align-middle" />
{{ title }}
</div>
</div>
<div class="flex items-center [&>*]:ms-2">
<!-- Slot -->
<slot />
<!-- Search Section -->
<div v-if="quickSearchNav || refSearchNav" class="relative">
<Input
v-model="searchModel"
name="search"
type="text"
:class="quickSearchNav?.inputClass || refSearchNav?.inputClass"
:placeholder="quickSearchNav?.placeholder || refSearchNav?.placeholder || 'Cari (min. 3 karakter)...'"
/>
<button
v-if="search.length > 0"
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
type="button"
@click="clearSearch"
>
<Icon name="i-lucide-x" class="h-4 w-4" />
</button>
<div
v-if="quickSearchNav && quickSearchNav.showValidationFeedback !== false && searchModel.length > 0 && searchModel.length < (quickSearchNav.minLength || 3)"
class="absolute -bottom-6 left-0 text-xs text-amber-600 whitespace-nowrap"
>
Minimal {{ quickSearchNav.minLength || 3 }} karakter untuk mencari
</div>
</div>
<!-- Add Button -->
<div v-if="addNav" class="flex items-center">
<Button
class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm"
:class="addNav.classVal"
:variant="(addNav.variant as any) || 'default'"
@click="addNav?.onClick"
>
<Icon :name="addNav.icon || 'i-lucide-plus'" class="mr-2 h-4 w-4 align-middle" />
{{ addNav.label }}
</Button>
</div>
<!-- Filter Button -->
<div v-if="filterNav" class="flex items-center">
<Button
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
:class="filterNav.classVal"
:variant="(filterNav.variant as any) || 'default'"
@click="filterNav?.onClick"
>
<Icon :name="filterNav.icon || 'i-lucide-filter'" class="mr-2 h-4 w-4 align-middle" />
{{ filterNav.label }}
</Button>
</div>
<!-- Print Button -->
<div v-if="printNav" class="flex items-center">
<Button
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
:class="printNav.classVal"
:variant="(printNav.variant as any) || 'default'"
@click="printNav?.onClick"
>
<Icon :name="printNav.icon || 'i-lucide-printer'" class="mr-2 h-4 w-4 align-middle" />
{{ printNav.label }}
</Button>
</div>
</div>
</div>
</template>
@@ -0,0 +1,57 @@
export type ComponentWithProps = { component: Component, props: Record<string, any> }
export interface ButtonNav {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
classVal?: string
classValExt?: string
icon?: string
label: string
onClick?: () => void
}
// can type directly
export interface QuickSearchNav {
modelValue?: string
placeholder?: string
inputClass?: string
inputPlaceHolder?: string
minLength?: number
btnClass?: string
btnIcon?: string
btnLabel?: string
showValidationFeedback?: boolean
debounceDuration?: number
searchParams: object
onSubmit?: (searchParams: object) => void
onClear: () => void
}
// callback on event
export interface RefSearchNav {
modelValue?: string
placeholder?: string
inputClass?: string
inputPlaceHolder?: string
btnClass?: string
btnIcon?: string
onInput: (val: string) => void
onClick: () => void
onClear: () => void
}
export interface RefExportNav {
onExportPdf?: () => void
onExportCsv?: () => void
onExportExcel?: () => void
}
export interface ContentHeader {
title?: string
icon?: string
components?: ComponentWithProps[]
quickSearchNav?: QuickSearchNav
refSearchNav?: RefSearchNav // either ref or quick
filterNav?: ButtonNav
addNav?: ButtonNav
printNav?: ButtonNav
}