Merge branch 'feat/consultation-82' into fe-prescription-56
This commit is contained in:
@@ -5,7 +5,7 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`m-3 mb-5 flex-wrap md:flex ${classValExt || ''}`">
|
||||
<div :class="` mb-5 flex-wrap md:flex ${classValExt || ''}`">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,7 @@ interface Item {
|
||||
value: string
|
||||
label: string
|
||||
code?: string
|
||||
priority?: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -15,7 +16,7 @@ const props = defineProps<{
|
||||
searchPlaceholder?: string
|
||||
emptyMessage?: string
|
||||
class?: string
|
||||
disabled?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -24,31 +25,32 @@ const emit = defineEmits<{
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
const selectedItem = computed(() =>
|
||||
props.items.find(item => item.value === props.modelValue),
|
||||
)
|
||||
const selectedItem = computed(() => props.items.find((item) => item.value === props.modelValue))
|
||||
|
||||
const displayText = computed(() =>
|
||||
selectedItem.value?.label || props.placeholder || '---pilih item',
|
||||
)
|
||||
const displayText = computed(() => {
|
||||
if (selectedItem.value?.label) {
|
||||
return selectedItem.value.label
|
||||
}
|
||||
return 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 => ({
|
||||
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
|
||||
const aPriority = a.priority ?? 0
|
||||
const bPriority = b.priority ?? 0
|
||||
if (aPriority !== bPriority) {
|
||||
return bPriority - aPriority
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -63,45 +65,78 @@ function onSelect(item: Item) {
|
||||
<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,
|
||||
)"
|
||||
:id="props.id"
|
||||
:disabled="props.isDisabled"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
:aria-expanded="open"
|
||||
:aria-controls="`${props.id}-list`"
|
||||
:aria-describedby="`${props.id}-search`"
|
||||
:class="
|
||||
cn(
|
||||
'w-full justify-between border text-sm font-normal rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white',
|
||||
{
|
||||
'cursor-not-allowed bg-gray-100 opacity-50 border-gray-300 text-gray-500': props.isDisabled,
|
||||
'bg-white text-black dark:bg-gray-800 dark:text-white dark:border-gray-600 border-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700': !props.isDisabled,
|
||||
'text-gray-400 dark:text-gray-500': !modelValue && !props.isDisabled,
|
||||
},
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ displayText }}
|
||||
<Icon name="i-lucide-chevrons-up-down" class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<Icon
|
||||
name="i-lucide-chevrons-up-down"
|
||||
:class="cn('ml-2 h-4 w-4 shrink-0', {
|
||||
'opacity-30': props.isDisabled,
|
||||
'opacity-50 text-gray-500 dark:text-gray-300': !props.isDisabled
|
||||
})"
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-full p-0">
|
||||
<Command>
|
||||
<PopoverContent class="w-[var(--radix-popover-trigger-width)] p-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
|
||||
<Command class="bg-white dark:bg-gray-800">
|
||||
<CommandInput
|
||||
:id="`${props.id}-search`"
|
||||
class="h-9"
|
||||
class="h-9 bg-white dark:bg-gray-800 text-black dark:text-white border-0 border-b border-gray-200 dark:border-gray-700 focus:ring-0"
|
||||
:placeholder="searchPlaceholder || 'Cari...'"
|
||||
:aria-label="`Cari ${displayText}`"
|
||||
/>
|
||||
<CommandEmpty>{{ emptyMessage || 'Item tidak ditemukan.' }}</CommandEmpty>
|
||||
<CommandList :id="`${props.id}-list`" role="listbox">
|
||||
<CommandEmpty class="text-gray-500 dark:text-gray-400 py-6 text-center text-sm">
|
||||
{{ emptyMessage || 'Item tidak ditemukan.' }}
|
||||
</CommandEmpty>
|
||||
<CommandList
|
||||
:id="`${props.id}-list`"
|
||||
role="listbox"
|
||||
class="max-h-60 overflow-auto"
|
||||
>
|
||||
<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">
|
||||
<CommandItem
|
||||
v-for="item in searchableItems"
|
||||
:key="item.value"
|
||||
:value="item.searchValue"
|
||||
:class="
|
||||
cn(
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 text-sm',
|
||||
'focus:outline-none text-black dark:text-white',
|
||||
'hover:bg-primary hover:text-white focus:bg-primary focus:text-white',
|
||||
'data-[highlighted]:bg-primary data-[highlighted]:text-white',
|
||||
)
|
||||
"
|
||||
@select="onSelect(item)"
|
||||
>
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<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>
|
||||
<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',
|
||||
)"
|
||||
:class="cn('h-4 w-4', modelValue === item.value ? 'opacity-100' : 'opacity-0')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// helpers
|
||||
import { format, parseISO } from 'date-fns'
|
||||
import { id as localeID } from 'date-fns/locale'
|
||||
// components
|
||||
import { Button } from '~/components/pub/ui/button'
|
||||
import { Calendar } from '~/components/pub/ui/calendar'
|
||||
import Calendar from '~/components/pub/ui/calendar/Calendar.vue'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '~/components/pub/ui/popover'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -18,7 +19,7 @@ const emit = defineEmits<{
|
||||
const date = ref<Date | any>(undefined)
|
||||
|
||||
watch(date, (value) => {
|
||||
const newValue = format(value, 'yyyy-MM-dd')
|
||||
const newValue = format(value, 'yyyy-MM-dd', { locale: localeID })
|
||||
emit('update:modelValue', newValue)
|
||||
})
|
||||
|
||||
@@ -42,14 +43,14 @@ onMounted(() => {
|
||||
<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-if="date">{{ format(date, 'PPP', { locale: localeID }) }}</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" />
|
||||
<Calendar v-model="date" mode="single" locale="id" />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ const props = withDefaults(
|
||||
density: 'default',
|
||||
side: 'default',
|
||||
position: 'default',
|
||||
layout: 'default',
|
||||
layout: 'stacked',
|
||||
class: '',
|
||||
},
|
||||
)
|
||||
@@ -26,12 +26,13 @@ const widthClass = computed(() => {
|
||||
})
|
||||
|
||||
const wrapperClass = computed(() => [
|
||||
'w-full flex-shrink-0 mb-3',
|
||||
'w-full flex-shrink-0',
|
||||
widthClass.value,
|
||||
|
||||
props.layout === 'stacked' ? 'flex flex-col p-2' : 'md:flex',
|
||||
props.layout === 'stacked' ? 'flex flex-col' : 'md:flex',
|
||||
|
||||
props.density !== 'dense' ? 'mb-2 md:mb-2.5 xl:mb-3' : '',
|
||||
// Only add margin bottom if no custom class overrides it
|
||||
props.class?.includes('mb-') ? '' : (props.density !== 'dense' ? 'mb-2 md:mb-2.5 xl:mb-3' : 'mb-3'),
|
||||
props.class,
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -10,8 +10,9 @@ defineProps<{
|
||||
<template>
|
||||
<div class="grow">
|
||||
<slot />
|
||||
<div v-if="id && errors?.[id]" class="field-error-info">
|
||||
{{ errors[id]?.message }}
|
||||
<!-- Always reserve space for error message to prevent CLS -->
|
||||
<div class="field-error-info">
|
||||
{{ (id && errors?.[id]) ? errors[id]?.message : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormErrors } from '~/types/error'
|
||||
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
|
||||
import Field from '~/components/pub/my-ui/form/field.vue'
|
||||
import Label from '~/components/pub/my-ui/form/label.vue'
|
||||
import { Input } from '~/components/pub/ui/input'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
fieldName: string
|
||||
placeholder: string
|
||||
label: string
|
||||
errors?: FormErrors
|
||||
class?: string
|
||||
numericOnly?: boolean
|
||||
maxLength?: number
|
||||
isRequired?: boolean
|
||||
isDisabled?: boolean
|
||||
}>()
|
||||
|
||||
function handleInput(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
let value = target.value
|
||||
|
||||
// Filter numeric only jika diperlukan
|
||||
if (props.numericOnly) {
|
||||
value = value.replace(/\D/g, '')
|
||||
}
|
||||
|
||||
// Batasi panjang maksimal jika diperlukan
|
||||
if (props.maxLength && value.length > props.maxLength) {
|
||||
value = value.slice(0, props.maxLength)
|
||||
}
|
||||
|
||||
// Update value jika ada perubahan
|
||||
if (target.value !== value) {
|
||||
target.value = value
|
||||
// Trigger input event untuk update form
|
||||
target.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FieldGroup>
|
||||
<Label
|
||||
v-if="label !== ''"
|
||||
:label-for="fieldName"
|
||||
:is-required="isRequired && !isDisabled"
|
||||
>
|
||||
{{ label }}
|
||||
</Label>
|
||||
<Field
|
||||
:id="fieldName"
|
||||
:errors="errors"
|
||||
>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
:name="fieldName"
|
||||
>
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
:disabled="isDisabled"
|
||||
v-bind="componentField"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxLength"
|
||||
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
||||
autocomplete="off"
|
||||
aria-autocomplete="none"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</template>
|
||||
@@ -2,16 +2,18 @@
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
labelFor?: string
|
||||
size?: 'default' | 'narrow' | 'wide'
|
||||
size?: 'default' | 'narrow' | 'wide' | 'fit'
|
||||
height?: 'default' | 'compact'
|
||||
position?: 'default' | 'dynamic'
|
||||
stacked?: boolean
|
||||
isRequired?: boolean
|
||||
}>(),
|
||||
{
|
||||
size: 'default',
|
||||
height: 'default',
|
||||
position: 'default',
|
||||
stacked: false,
|
||||
stacked: true,
|
||||
isRequired: false,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -19,6 +21,7 @@ const sizeMap = {
|
||||
default: 'w-28 2xl:w-36',
|
||||
narrow: 'w-24 2xl:w-28',
|
||||
wide: 'w-44 2xl:w-48',
|
||||
fit: 'w-fit',
|
||||
} as const
|
||||
|
||||
const heightMap = {
|
||||
@@ -43,13 +46,23 @@ const wrapperClass = computed(() => [
|
||||
props.stacked ? '' : positionWrapMap[props.position],
|
||||
])
|
||||
|
||||
const labelClass = computed(() => [props.stacked ? 'block mb-1 text-sm font-medium' : positionChildMap[props.position]])
|
||||
const labelClass = computed(() => [props.stacked ? 'block mb-1 text-sm font-normal' : positionChildMap[props.position]])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<label class="block" :class="labelClass" :for="labelFor">
|
||||
<label
|
||||
class="block"
|
||||
:class="labelClass"
|
||||
:for="labelFor"
|
||||
>
|
||||
<slot />
|
||||
<span
|
||||
v-if="isRequired"
|
||||
class="text-red-600"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { SelectRoot } from 'radix-vue'
|
||||
import {
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '~/components/pub/ui/select'
|
||||
import { watch } from 'vue'
|
||||
import SelectContent from '~/components/pub/ui/select/SelectContent.vue'
|
||||
import SelectGroup from '~/components/pub/ui/select/SelectGroup.vue'
|
||||
import SelectItem from '~/components/pub/ui/select/SelectItem.vue'
|
||||
import SelectLabel from '~/components/pub/ui/select/SelectLabel.vue'
|
||||
import SelectSeparator from '~/components/pub/ui/select/SelectSeparator.vue'
|
||||
import SelectTrigger from '~/components/pub/ui/select/SelectTrigger.vue'
|
||||
import SelectValue from '~/components/pub/ui/select/SelectValue.vue'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
interface Item {
|
||||
value: string
|
||||
label: string
|
||||
code?: string
|
||||
priority?: number // Priority untuk sorting: negatif = bawah, positif = atas, 0/undefined = normal sorting
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -24,40 +24,121 @@ const props = defineProps<{
|
||||
label?: string
|
||||
separator?: boolean
|
||||
class?: string
|
||||
isSelectedFirst?: boolean
|
||||
preserveOrder?: boolean
|
||||
isDisabled?: boolean
|
||||
autoWidth?: boolean
|
||||
autoFill?: boolean
|
||||
// otherPlacement sudah tidak digunakan, diganti dengan priority system di Item interface
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
// Sort items with selected item first, then alphabetically
|
||||
const sortedItems = computed(() => {
|
||||
const itemsWithSelection = props.items.map(item => ({
|
||||
const processedItems = 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
|
||||
// Jika preserveOrder true, kembalikan urutan array asli
|
||||
if (props.preserveOrder) {
|
||||
return itemsWithSelection
|
||||
}
|
||||
|
||||
// Sorting dengan priority system
|
||||
return itemsWithSelection.sort((a, b) => {
|
||||
const aPriority = a.priority ?? 0
|
||||
const bPriority = b.priority ?? 0
|
||||
|
||||
// Jika ada priority, sort berdasarkan priority (descending: tinggi ke rendah)
|
||||
if (aPriority !== bPriority) {
|
||||
return bPriority - aPriority
|
||||
}
|
||||
|
||||
// Jika priority sama (termasuk 0/undefined), lakukan sorting normal
|
||||
if (props.isSelectedFirst) {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
// Computed property untuk menghitung width optimal berdasarkan konten terpanjang
|
||||
const optimalWidth = computed(() => {
|
||||
// Jika autoWidth false atau undefined, gunakan full width
|
||||
if (!props.autoWidth) return '100%'
|
||||
|
||||
if (!props.items.length) return 'auto'
|
||||
|
||||
// Mencari label terpanjang
|
||||
const longestLabel = props.items.reduce((longest, item) => {
|
||||
const itemText = item.code ? `${item.label} ${item.code}` : item.label
|
||||
return itemText.length > longest.length ? itemText : longest
|
||||
}, '')
|
||||
|
||||
// Menghitung width berdasarkan panjang karakter (estimasi)
|
||||
// Setiap karakter ~0.6em, ditambah padding dan space untuk icon
|
||||
const estimatedWidth = Math.max(longestLabel.length * 0.6 + 3, 8) // minimum 8em
|
||||
|
||||
return `${Math.min(estimatedWidth, 25)}em` // maksimum 25em
|
||||
})
|
||||
|
||||
function onValueChange(value: string) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
// Auto fill logic - automatically select first item if autoFill is enabled
|
||||
watch(
|
||||
() => props.items,
|
||||
(newItems) => {
|
||||
if (props.autoFill && newItems.length > 0 && !props.modelValue) {
|
||||
// Auto select first item only if no value is currently selected
|
||||
const firstItem = newItems[0]
|
||||
if (firstItem?.value) {
|
||||
emit('update:modelValue', firstItem.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</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',
|
||||
)" />
|
||||
<SelectRoot
|
||||
:model-value="modelValue"
|
||||
:disabled="isDisabled"
|
||||
@update:model-value="onValueChange"
|
||||
>
|
||||
<SelectTrigger
|
||||
:class="
|
||||
cn(
|
||||
'rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white',
|
||||
{
|
||||
'cursor-not-allowed bg-gray-100 opacity-50': isDisabled,
|
||||
'bg-white text-black dark:bg-gray-800 dark:text-white': !isDisabled,
|
||||
'w-full': !autoWidth,
|
||||
},
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
:style="autoWidth ? { width: optimalWidth, minWidth: '8em' } : {}"
|
||||
icon-name="i-radix-icons-chevron-down"
|
||||
icon-class="text-gray-500 dark:text-gray-300"
|
||||
>
|
||||
<SelectValue
|
||||
:placeholder="placeholder || 'Pilih item'"
|
||||
:class="
|
||||
cn('text-sm', {
|
||||
'text-gray-400': !props.modelValue,
|
||||
'text-black dark:text-white': props.modelValue,
|
||||
'text-gray-500': isDisabled,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
@@ -66,10 +147,18 @@ function onValueChange(value: string) {
|
||||
{{ 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">
|
||||
<SelectItem
|
||||
v-for="item in processedItems"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
class="cursor-pointer hover:bg-primary hover:text-white focus:bg-primary focus:text-white"
|
||||
>
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.code" class="text-xs text-muted-foreground ml-2">
|
||||
<span
|
||||
v-if="item.code"
|
||||
class="ml-2 text-xs text-muted-foreground"
|
||||
>
|
||||
{{ item.code }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-1 lg:grid lg:grid-cols-[180px_minmax(0,1fr)] lg:gap-x-3">
|
||||
<!-- Label -->
|
||||
<span class="text-md font-normal text-muted-foreground">
|
||||
{{ label }}
|
||||
</span>
|
||||
|
||||
<!-- Value -->
|
||||
<span class="truncate lg:whitespace-normal">
|
||||
<span class="me-3 hidden lg:inline-block">:</span>
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
headerClass?: string
|
||||
contentClass?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mb-6">
|
||||
<h3 :class="cn('mb-3 flex items-center gap-1 pb-1 text-base font-semibold', headerClass)">
|
||||
<slot name="icon" />
|
||||
{{ title }}
|
||||
</h3>
|
||||
|
||||
<div :class="cn('space-y-2', contentClass)">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -41,11 +41,12 @@ const isOpen = computed({
|
||||
<template>
|
||||
<Dialog v-model:open="isOpen">
|
||||
<DialogContent
|
||||
:class="sizeClass" @interact-outside="(e: any) => preventOutside && e.preventDefault()"
|
||||
:class="sizeClass"
|
||||
@interact-outside="(e: any) => preventOutside && e.preventDefault()"
|
||||
@pointer-down-outside="(e: any) => preventOutside && e.preventDefault()"
|
||||
>
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ props.title }}</DialogTitle>
|
||||
<DialogTitle class="text-[20px]">{{ props.title }}</DialogTitle>
|
||||
<DialogDescription v-if="props.description">{{ props.description }}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<slot />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
type ClickType = 'cancel' | 'draft' | 'submit'
|
||||
type ClickType = 'cancel' | 'edit'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', type: ClickType): void
|
||||
@@ -12,12 +12,27 @@ function onClick(type: ClickType) {
|
||||
|
||||
<template>
|
||||
<div class="m-2 flex gap-2 px-2">
|
||||
<Button class="bg-gray-400" type="button" @click="onClick('cancel')">
|
||||
<Icon name="i-lucide-arrow-left" class="me-2 align-middle" />
|
||||
<Button
|
||||
class="bg-gray-400"
|
||||
type="button"
|
||||
@click="onClick('cancel')"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-arrow-left"
|
||||
class="me-2 align-middle"
|
||||
/>
|
||||
Back
|
||||
</Button>
|
||||
<Button class="bg-orange-500" variant="outline" type="button" @click="onClick('draft')">
|
||||
<Icon name="i-lucide-file" class="me-2 align-middle" />
|
||||
<Button
|
||||
class="bg-orange-500"
|
||||
variant="outline"
|
||||
type="button"
|
||||
@click="onClick('edit')"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-file"
|
||||
class="me-2 align-middle"
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -12,12 +12,24 @@ function onClick(type: ClickType) {
|
||||
|
||||
<template>
|
||||
<div class="m-2 flex gap-2 px-2">
|
||||
<Button class="bg-gray-400" @click="onClick('cancel')">
|
||||
<Icon name="i-lucide-arrow-left" class="me-2 align-middle" />
|
||||
<Button
|
||||
class="bg-gray-400"
|
||||
@click="onClick('cancel')"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-arrow-left"
|
||||
class="me-2 align-middle"
|
||||
/>
|
||||
Back
|
||||
</Button>
|
||||
<Button class="bg-primary" @click="onClick('submit')">
|
||||
<Icon name="i-lucide-check" class="me-2 align-middle" />
|
||||
<Button
|
||||
class="bg-primary"
|
||||
@click="onClick('submit')"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-check"
|
||||
class="me-2 align-middle"
|
||||
/>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
prep: HeaderPrep
|
||||
refSearchNav?: RefSearchNav
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
function emitSearchNavClick() {
|
||||
@@ -21,15 +23,24 @@ function btnClick() {
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="flex items-center justify-between"
|
||||
:class="cn('', props.class)"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="md:text-base xl:text-lg font-semibold text-gray-900">
|
||||
<Icon :name="props.prep.icon!" class="mr-2 size-4 md:size-6 align-middle" />
|
||||
<div class="font-semibold text-gray-900 md:text-base xl:text-lg">
|
||||
<Icon
|
||||
:name="props.prep.icon!"
|
||||
class="mr-2 size-4 align-middle md:size-6"
|
||||
/>
|
||||
{{ props.prep.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div v-if="props.refSearchNav" class="text-lg text-gray-900">
|
||||
<div
|
||||
v-if="props.refSearchNav"
|
||||
class="text-lg text-gray-900"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
@@ -38,9 +49,18 @@ function btnClick() {
|
||||
@input="onInput"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="prep.addNav" class="flex items-center ms-2">
|
||||
<Button class="rounded-md border border-gray-300 text-white" @click="btnClick">
|
||||
<Icon name="i-lucide-plus" class="align-middle" />
|
||||
<div
|
||||
v-if="prep.addNav"
|
||||
class="ms-2 flex items-center"
|
||||
>
|
||||
<Button
|
||||
class="rounded-md border border-gray-300 text-white"
|
||||
@click="btnClick"
|
||||
>
|
||||
<Icon
|
||||
name="i-lucide-plus"
|
||||
class="mr-2 h-4 w-4 align-middle"
|
||||
/>
|
||||
{{ prep.addNav.label }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,17 @@ import { CalendarRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from '.'
|
||||
import CalendarCell from './CalendarCell.vue'
|
||||
import CalendarCellTrigger from './CalendarCellTrigger.vue'
|
||||
import CalendarGrid from './CalendarGrid.vue'
|
||||
import CalendarGridBody from './CalendarGridBody.vue'
|
||||
import CalendarGridHead from './CalendarGridHead.vue'
|
||||
import CalendarGridRow from './CalendarGridRow.vue'
|
||||
import CalendarHeadCell from './CalendarHeadCell.vue'
|
||||
import CalendarHeader from './CalendarHeader.vue'
|
||||
import CalendarHeading from './CalendarHeading.vue'
|
||||
import CalendarNextButton from './CalendarNextButton.vue'
|
||||
import CalendarPrevButton from './CalendarPrevButton.vue'
|
||||
|
||||
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
|
||||
@@ -11,6 +11,6 @@ const { name, formMessageId } = useFormField()
|
||||
:id="formMessageId"
|
||||
as="p"
|
||||
:name="toValue(name)"
|
||||
class="text-[0.8rem] text-destructive font-medium"
|
||||
class="text-[0.8rem] text-destructive font-sans"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectRootEmits, SelectRootProps } from 'radix-vue'
|
||||
import { SelectRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import {
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '~/components/pub/ui/select'
|
||||
import SelectContent from './SelectContent.vue'
|
||||
import SelectGroup from './SelectGroup.vue'
|
||||
import SelectItem from './SelectItem.vue'
|
||||
import SelectSeparator from './SelectSeparator.vue'
|
||||
import SelectTrigger from './SelectTrigger.vue'
|
||||
import SelectValue from './SelectValue.vue'
|
||||
|
||||
interface Item {
|
||||
value: string
|
||||
|
||||
@@ -5,7 +5,8 @@ import { SelectContent, SelectPortal, SelectViewport, useForwardPropsEmits } fro
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
import { SelectScrollDownButton, SelectScrollUpButton } from '.'
|
||||
import SelectScrollDownButton from './SelectScrollDownButton.vue'
|
||||
import SelectScrollUpButton from './SelectScrollUpButton.vue'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
|
||||
Reference in New Issue
Block a user