dev: hotfix refactor

+ merged pub/custom-ui and pub/base into my-ui
- droped pub/custom-ui
This commit is contained in:
2025-10-05 09:45:17 +07:00
parent 72627b8a37
commit 2da4e616ba
219 changed files with 474 additions and 474 deletions
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
defineProps<{
classValExt?: string
}>()
</script>
<template>
<div :class="`m-3 mb-5 flex-wrap md:flex ${classValExt || ''}`">
<slot />
</div>
</template>
+114
View File
@@ -0,0 +1,114 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
interface Item {
value: string
label: string
code?: string
}
const props = defineProps<{
id: string
modelValue?: string
items: Item[]
placeholder?: string
searchPlaceholder?: string
emptyMessage?: string
class?: string
disabled?: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const open = ref(false)
const selectedItem = computed(() =>
props.items.find(item => item.value === props.modelValue),
)
const displayText = computed(() =>
selectedItem.value?.label || props.placeholder || '---pilih item',
)
// Create searchable items with combined code and label for better search
// Sort by:
// 1. Selected item first (highest priority)
// 2. Then by label alphabetically
const searchableItems = computed(() => {
const itemsWithSearch = props.items.map(item => ({
...item,
searchValue: `${item.code || ''} ${item.label}`.trim(),
isSelected: item.value === props.modelValue,
}))
return itemsWithSearch.sort((a, b) => {
// Selected item always comes first
if (a.isSelected && !b.isSelected) return -1
if (!a.isSelected && b.isSelected) return 1
// If neither or both are selected, sort by label alphabetically
return a.label.localeCompare(b.label)
})
})
function onSelect(item: Item) {
emit('update:modelValue', item.value)
open.value = false
}
</script>
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
:id="props.id"
:disabled="props.disabled"
variant="outline"
role="combobox"
:aria-expanded="open"
:aria-controls="`${props.id}-list`"
:aria-describedby="`${props.id}-search`"
:class="cn(
'w-full justify-between border-1 border-gray-400 bg-white hover:bg-gray-50 text-sm font-normal focus:outline-none focus:ring-1 focus:ring-black',
!modelValue && 'text-muted-foreground',
props.class,
)"
>
{{ displayText }}
<Icon name="i-lucide-chevrons-up-down" class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-full p-0">
<Command>
<CommandInput
:id="`${props.id}-search`"
class="h-9"
:placeholder="searchPlaceholder || 'Cari...'"
:aria-label="`Cari ${displayText}`"
/>
<CommandEmpty>{{ emptyMessage || 'Item tidak ditemukan.' }}</CommandEmpty>
<CommandList :id="`${props.id}-list`" role="listbox">
<CommandGroup>
<CommandItem v-for="item in searchableItems" :key="item.value" :value="item.searchValue" @select="onSelect(item)">
<div class="flex items-center justify-between w-full">
<span>{{ item.label }}</span>
<div class="flex items-center gap-2">
<span v-if="item.code" class="text-xs text-muted-foreground">{{ item.code }}</span>
<Icon
name="i-lucide-check"
:class="cn(
'h-4 w-4',
modelValue === item.value ? 'opacity-100' : 'opacity-0',
)"
/>
</div>
</div>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</template>
@@ -0,0 +1,56 @@
<script setup lang="ts">
// helpers
import { format, parseISO } from 'date-fns'
// components
import { Button } from '~/components/pub/ui/button'
import { Calendar } from '~/components/pub/ui/calendar'
import { Popover, PopoverContent, PopoverTrigger } from '~/components/pub/ui/popover'
const props = defineProps<{
placeholder?: string
modelValue?: Date | string | undefined
}>()
const emit = defineEmits<{
'update:modelValue': [value: Date | string | undefined]
}>()
const date = ref<Date | any>(undefined)
watch(date, (value) => {
const newValue = format(value, 'yyyy-MM-dd')
emit('update:modelValue', newValue)
})
onMounted(() => {
if (props.modelValue) {
const value = props.modelValue
if (value instanceof Date) {
date.value = value
} else if (typeof value === 'string' && value) {
date.value = parseISO(value)
} else {
date.value = undefined
}
}
})
</script>
<template>
<div class="flex flex-col space-y-2">
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" class="bg-white border-gray-400 font-normal text-right h-[40px] w-full">
<div class="flex justify-between items-center w-full">
<p v-if="date">{{ format(date, 'PPP') }}</p>
<p v-else class="text-sm text-black text-opacity-50">{{ props.placeholder || 'Tanggal' }}</p>
<Icon name="i-lucide-calendar" class="h-5 w-5" />
</div>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar v-model="date" mode="single" />
</PopoverContent>
</Popover>
</div>
</template>
@@ -0,0 +1,43 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
column?: 1 | 2 | 3
density?: 'default' | 'dense'
side?: 'default' | 'break'
position?: 'default' | 'dynamic'
layout?: 'default' | 'stacked'
class?: string
}>(),
{
column: 1,
density: 'default',
side: 'default',
position: 'default',
layout: 'default',
class: '',
},
)
const widthClass = computed(() => {
if (props.column === 1) return 'md:w-full pe-4'
if (props.column === 2) return 'md:w-1/2 pe-4'
if (props.column === 3) return 'md:w-1/3 pe-4'
return 'w-full'
})
const wrapperClass = computed(() => [
'w-full flex-shrink-0 mb-3',
widthClass.value,
props.layout === 'stacked' ? 'flex flex-col p-2' : 'md:flex',
props.density !== 'dense' ? 'mb-2 md:mb-2.5 xl:mb-3' : '',
props.class,
])
</script>
<template>
<div :class="wrapperClass">
<slot />
</div>
</template>
+17
View File
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { XErrors } from '~/types/error'
defineProps<{
id?: string
errors?: XErrors
}>()
</script>
<template>
<div class="grow">
<slot />
<div v-if="id && errors?.[id]" class="field-error-info">
{{ errors[id]?.message }}
</div>
</div>
</template>
+55
View File
@@ -0,0 +1,55 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
labelFor?: string
size?: 'default' | 'narrow' | 'wide'
height?: 'default' | 'compact'
position?: 'default' | 'dynamic'
stacked?: boolean
}>(),
{
size: 'default',
height: 'default',
position: 'default',
stacked: false,
},
)
const sizeMap = {
default: 'w-28 2xl:w-36',
narrow: 'w-24 2xl:w-28',
wide: 'w-44 2xl:w-48',
} as const
const heightMap = {
default: 'pt-2 2xl:pt-2.5',
compact: 'leading-[14pt]',
} as const
const positionWrapMap = {
default: 'pe-2 text-start',
dynamic: 'md:text-end',
} as const
const positionChildMap = {
default: '',
dynamic: 'block pe-2.5',
} as const
const wrapperClass = computed(() => [
'block shrink-0',
props.stacked ? 'w-full mb-1 text-start' : sizeMap[props.size],
props.stacked ? '' : heightMap[props.height],
props.stacked ? '' : positionWrapMap[props.position],
])
const labelClass = computed(() => [props.stacked ? 'block mb-1 text-sm font-medium' : positionChildMap[props.position]])
</script>
<template>
<div :class="wrapperClass">
<label class="block" :class="labelClass" :for="labelFor">
<slot />
</label>
</div>
</template>
+82
View File
@@ -0,0 +1,82 @@
<script setup lang="ts">
import { SelectRoot } from 'radix-vue'
import {
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '~/components/pub/ui/select'
import { cn } from '~/lib/utils'
interface Item {
value: string
label: string
code?: string
}
const props = defineProps<{
modelValue?: string
items: Item[]
placeholder?: string
label?: string
separator?: boolean
class?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
// Sort items with selected item first, then alphabetically
const sortedItems = computed(() => {
const itemsWithSelection = props.items.map(item => ({
...item,
isSelected: item.value === props.modelValue,
}))
return itemsWithSelection.sort((a, b) => {
// Selected item always comes first
if (a.isSelected && !b.isSelected) return -1
if (!a.isSelected && b.isSelected) return 1
// If neither or both are selected, sort by label alphabetically
return a.label.localeCompare(b.label)
})
})
function onValueChange(value: string) {
emit('update:modelValue', value)
}
</script>
<template>
<SelectRoot :model-value="modelValue" @update:model-value="onValueChange">
<SelectTrigger :class="cn('w-full focus:outline-none focus:ring-1 focus:ring-black bg-white', props.class)">
<SelectValue :placeholder="placeholder || 'Pilih item'" :class="cn(
props.modelValue ? 'text-black' : 'text-muted-foreground',
)" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel v-if="label">
{{ label }}
</SelectLabel>
<SelectItem v-for="item in sortedItems" :key="item.value" :value="item.value" class="cursor-pointer">
<div class="flex items-center justify-between w-full">
<span>{{ item.label }}</span>
<span v-if="item.code" class="text-xs text-muted-foreground ml-2">
{{ item.code }}
</span>
</div>
</SelectItem>
<SelectSeparator v-if="separator" />
</SelectGroup>
</SelectContent>
</SelectRoot>
</template>