cherry-pick: pub components from feat/laporan-tindakan-185
This commit is contained in:
@@ -4,7 +4,7 @@ import { type Item } from './index'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
id?: string
|
id?: string
|
||||||
modelValue?: string
|
modelValue?: string | number
|
||||||
items: Item[]
|
items: Item[]
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
searchPlaceholder?: string
|
searchPlaceholder?: string
|
||||||
@@ -16,8 +16,8 @@ const props = defineProps<{
|
|||||||
const model = defineModel()
|
const model = defineModel()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: string]
|
'update:modelValue': [value: string | number]
|
||||||
'update:searchText': [value: string]
|
'update:searchText': [value: string | number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface Item {
|
export interface Item {
|
||||||
value: string
|
value: string | number
|
||||||
label: string
|
label: string
|
||||||
code?: string
|
code?: string
|
||||||
priority?: number
|
priority?: number
|
||||||
@@ -7,12 +7,12 @@ export interface Item {
|
|||||||
|
|
||||||
export function recStrToItem(input: Record<string, string>): Item[] {
|
export function recStrToItem(input: Record<string, string>): Item[] {
|
||||||
const items: Item[] = []
|
const items: Item[] = []
|
||||||
let idx = 0;
|
let idx = 0
|
||||||
for (const key in input) {
|
for (const key in input) {
|
||||||
if (input.hasOwnProperty(key)) {
|
if (input.hasOwnProperty(key)) {
|
||||||
items.push({
|
items.push({
|
||||||
value: key || ('unknown-' + idx),
|
value: key || 'unknown-' + idx,
|
||||||
label: input[key] || ('unknown-' + idx),
|
label: input[key] || 'unknown-' + idx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
idx++
|
idx++
|
||||||
|
|||||||
@@ -1,33 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
height?: number
|
height?: number
|
||||||
class?: string
|
|
||||||
activeTab?: 1 | 2
|
activeTab?: 1 | 2
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const classVal = computed(() => {
|
|
||||||
return props.class ? props.class : ''
|
|
||||||
})
|
|
||||||
const activeTab = ref(props.activeTab || 1)
|
const activeTab = ref(props.activeTab || 1)
|
||||||
|
|
||||||
function switchActiveTab() {
|
function handleClick(value: 1 | 2) {
|
||||||
activeTab.value = activeTab.value === 1 ? 2 : 1
|
activeTab.value = value
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="`content-switcher ${classVal}`" :style="height ? `height:${200}px` : ''">
|
<div class="content-switcher" :style="`height: ${height || 200}px`">
|
||||||
<div class="wrapper">
|
<div :class="`${activeTab === 1 ? 'active' : 'inactive'}`">
|
||||||
<div :class="`item item-1 ${activeTab === 1 ? 'active' : 'inactive'}`">
|
<div class="content-wrapper">
|
||||||
<slot name="content1" />
|
<div>
|
||||||
|
<slot name="content1" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`nav border-slate-300 ${ activeTab == 1 ? 'border-l' : 'border-r'}`">
|
<div class="content-nav">
|
||||||
<button @click="switchActiveTab()" class="!p-0 w-full h-full">
|
<button @click="handleClick(1)">
|
||||||
<Icon :name="activeTab == 1 ? 'i-lucide-chevron-left' : 'i-lucide-chevron-right'" class="text-3xl" />
|
<Icon name="i-lucide-chevron-right" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`item item-2 ${activeTab === 2 ? 'active' : 'inactive'}`">
|
</div>
|
||||||
<slot name="content2" />
|
<div :class="`${activeTab === 2 ? 'active' : 'inactive'}`">
|
||||||
|
<div class="content-nav">
|
||||||
|
<button @click="handleClick(2)">
|
||||||
|
<Icon name="i-lucide-chevron-left" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<div>
|
||||||
|
<slot name="content2" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,24 +42,45 @@ function switchActiveTab() {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.content-switcher {
|
.content-switcher {
|
||||||
@apply overflow-hidden
|
@apply flex overflow-hidden gap-3
|
||||||
}
|
}
|
||||||
.wrapper {
|
.content-switcher > * {
|
||||||
@apply flex w-[200%] h-full
|
@apply border border-slate-300 rounded-md flex overflow-hidden
|
||||||
}
|
|
||||||
.item {
|
|
||||||
@apply w-[calc(50%-60px)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-1.active {
|
.content-wrapper {
|
||||||
@apply ms-0 transition-all duration-500 ease-in-out
|
@apply p-4 2xl:p-5 overflow-hidden grow
|
||||||
}
|
}
|
||||||
.item-1.inactive {
|
.inactive .content-wrapper {
|
||||||
@apply -ms-[calc(50%-60px)] transition-all duration-500 ease-in-out
|
@apply p-0 w-0
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.content-nav {
|
||||||
@apply h-full w-[60px] flex flex-row items-center justify-center content-center !text-2xl overflow-hidden
|
@apply h-full flex flex-row items-center justify-center content-center !text-2xl overflow-hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-nav button {
|
||||||
|
@apply pt-2 px-2 h-full w-full
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .content-switcher .inactive > .content-wrapper {
|
||||||
|
@apply w-0 p-0 opacity-0 transition-all duration-500 ease-in-out
|
||||||
|
} */
|
||||||
|
.content-switcher .inactive {
|
||||||
|
@apply w-16 transition-all duration-500 ease-in-out
|
||||||
|
}
|
||||||
|
.content-switcher .inactive > .content-nav {
|
||||||
|
@apply w-full transition-all duration-100 ease-in-out
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-switcher .active {
|
||||||
|
@apply grow transition-all duration-500 ease-in-out
|
||||||
|
}
|
||||||
|
.content-switcher .active > .content-nav {
|
||||||
|
@apply w-0 transition-all duration-100 ease-in-out
|
||||||
|
}
|
||||||
|
/* .content-switcher .active > .content-wrapper {
|
||||||
|
@apply w-full delay-1000 transition-all duration-1000 ease-in-out
|
||||||
|
} */
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { LinkItem, ListItemDto } from './types'
|
||||||
|
import { ActionEvents } from './types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rec: ListItemDto
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const recId = inject<Ref<number>>('rec_id')!
|
||||||
|
const recAction = inject<Ref<string>>('rec_action')!
|
||||||
|
const recItem = inject<Ref<any>>('rec_item')!
|
||||||
|
const timestamp = inject<Ref<number>>('timestamp')!
|
||||||
|
const activeKey = ref<string | null>(null)
|
||||||
|
const linkItems: LinkItem[] = [
|
||||||
|
{
|
||||||
|
label: 'Detail',
|
||||||
|
onClick: () => {
|
||||||
|
detail()
|
||||||
|
},
|
||||||
|
icon: 'i-lucide-eye',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function detail() {
|
||||||
|
recId.value = props.rec.id || 0
|
||||||
|
recAction.value = ActionEvents.showDetail
|
||||||
|
recItem.value = props.rec
|
||||||
|
timestamp.value = Date.now()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SidebarMenuButton
|
||||||
|
size="lg"
|
||||||
|
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="i-lucide-chevrons-up-down"
|
||||||
|
class="ml-auto size-4"
|
||||||
|
/>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-for="item in linkItems"
|
||||||
|
:key="item.label"
|
||||||
|
class="hover:bg-gray-100 dark:hover:bg-slate-700"
|
||||||
|
@click="item.onClick"
|
||||||
|
@mouseenter="activeKey = item.label"
|
||||||
|
@mouseleave="activeKey = null"
|
||||||
|
>
|
||||||
|
<Icon :name="item.icon ?? ''" />
|
||||||
|
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -53,8 +53,8 @@ const settingClass = computed(() => {
|
|||||||
'[&_.cell]:2xl:flex',
|
'[&_.cell]:2xl:flex',
|
||||||
][getBreakpointIdx(props.cellFlexPoint)]
|
][getBreakpointIdx(props.cellFlexPoint)]
|
||||||
cls += ' [&_.label]:flex-shrink-0 ' + [
|
cls += ' [&_.label]:flex-shrink-0 ' + [
|
||||||
'[&_.label]:md:w-16 [&_.label]:xl:w-20',
|
'[&_.label]:md:w-12 [&_.label]:xl:w-20',
|
||||||
'[&_.label]:md:w-20 [&_.label]:xl:w-24',
|
'[&_.label]:md:w-16 [&_.label]:xl:w-24',
|
||||||
'[&_.label]:md:w-24 [&_.label]:xl:w-32',
|
'[&_.label]:md:w-24 [&_.label]:xl:w-32',
|
||||||
'[&_.label]:md:w-32 [&_.label]:xl:w-40',
|
'[&_.label]:md:w-32 [&_.label]:xl:w-40',
|
||||||
'[&_.label]:md:w-44 [&_.label]:xl:w-52',
|
'[&_.label]:md:w-44 [&_.label]:xl:w-52',
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button } from '~/components/pub/ui/button'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button Action Component untuk form
|
||||||
|
* Support preset: add, delete, save, cancel
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
/**
|
||||||
|
* Preset button type dengan styling bawaan
|
||||||
|
* - add: Button tambah (outline primary)
|
||||||
|
* - delete: Button hapus (ghost)
|
||||||
|
* - save: Button simpan (primary)
|
||||||
|
* - cancel: Button batal (secondary)
|
||||||
|
* - custom: Custom styling
|
||||||
|
*/
|
||||||
|
preset?: 'add' | 'delete' | 'save' | 'cancel' | 'custom'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon name (UnoCSS/Lucide)
|
||||||
|
* Default akan diset berdasarkan preset
|
||||||
|
*/
|
||||||
|
icon?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button text
|
||||||
|
* Set ke empty string ('') untuk icon-only mode
|
||||||
|
*/
|
||||||
|
label?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button title (tooltip)
|
||||||
|
* Wajib untuk icon-only buttons (accessibility)
|
||||||
|
*/
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon only mode (no label)
|
||||||
|
* Otomatis true jika label kosong
|
||||||
|
*/
|
||||||
|
iconOnly?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button type
|
||||||
|
*/
|
||||||
|
type?: 'button' | 'submit' | 'reset'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disabled state
|
||||||
|
*/
|
||||||
|
disabled?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom class untuk override styling
|
||||||
|
*/
|
||||||
|
class?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsive width (full width on mobile)
|
||||||
|
*/
|
||||||
|
fullWidthMobile?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button size
|
||||||
|
*/
|
||||||
|
size?: 'default' | 'sm' | 'lg' | 'icon'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button variant (override preset variant)
|
||||||
|
*/
|
||||||
|
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
preset: 'custom',
|
||||||
|
type: 'button',
|
||||||
|
disabled: false,
|
||||||
|
fullWidthMobile: false,
|
||||||
|
iconOnly: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Preset configurations
|
||||||
|
const presetConfig = {
|
||||||
|
add: {
|
||||||
|
variant: 'outline' as const,
|
||||||
|
icon: 'i-lucide-plus',
|
||||||
|
label: 'Tambah',
|
||||||
|
classes:
|
||||||
|
'border-primary bg-white text-primary hover:bg-primary hover:text-white dark:bg-slate-800 dark:border-primary dark:text-primary dark:hover:bg-primary dark:hover:text-white',
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
variant: 'ghost' as const,
|
||||||
|
icon: 'i-lucide-trash-2',
|
||||||
|
label: '', // Default kosong untuk icon-only
|
||||||
|
classes:
|
||||||
|
'hover:bg-destructive hover:text-white hover:border-destructive dark:hover:bg-destructive dark:hover:text-white',
|
||||||
|
},
|
||||||
|
save: {
|
||||||
|
variant: 'default' as const,
|
||||||
|
icon: 'i-lucide-save',
|
||||||
|
label: 'Simpan',
|
||||||
|
classes: 'bg-primary text-primary-foreground hover:bg-primary/90 dark:bg-primary dark:hover:bg-primary/90',
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
variant: 'secondary' as const,
|
||||||
|
icon: 'i-lucide-x',
|
||||||
|
label: 'Batal',
|
||||||
|
classes: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 dark:bg-slate-700 dark:hover:bg-slate-600',
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
variant: 'default' as const,
|
||||||
|
icon: '',
|
||||||
|
label: '',
|
||||||
|
classes: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPreset = computed(() => presetConfig[props.preset])
|
||||||
|
const buttonVariant = computed(() => props.variant || currentPreset.value.variant)
|
||||||
|
const buttonIcon = computed(() => props.icon || currentPreset.value.icon)
|
||||||
|
|
||||||
|
// Label handling: gunakan prop label jika ada, fallback ke preset, atau undefined jika iconOnly
|
||||||
|
const buttonLabel = computed(() => {
|
||||||
|
if (props.label !== undefined) return props.label
|
||||||
|
return currentPreset.value.label
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonTitle = computed(() => props.title || buttonLabel.value)
|
||||||
|
|
||||||
|
// Deteksi icon-only mode
|
||||||
|
const isIconOnly = computed(() => {
|
||||||
|
return props.iconOnly || buttonLabel.value === '' || !buttonLabel.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonClasses = computed(() => {
|
||||||
|
// Base classes berbeda untuk icon-only vs with-label
|
||||||
|
const baseClasses = isIconOnly.value
|
||||||
|
? 'rounded-md p-2 w-9 h-9 transition-colors flex items-center justify-center' // Icon only: square button
|
||||||
|
: 'rounded-md px-4 py-2 transition-colors sm:text-sm' // With label: padding horizontal lebih besar
|
||||||
|
|
||||||
|
const widthClasses = props.fullWidthMobile && !isIconOnly.value ? 'w-full sm:w-auto' : ''
|
||||||
|
const presetClasses = currentPreset.value.classes
|
||||||
|
|
||||||
|
return cn(baseClasses, widthClasses, presetClasses, props.class)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
:title="buttonTitle"
|
||||||
|
:type="type"
|
||||||
|
:variant="buttonVariant"
|
||||||
|
:size="size"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="buttonClasses"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-if="buttonIcon"
|
||||||
|
:name="buttonIcon"
|
||||||
|
:class="cn('h-4 w-4 align-middle transition-colors', !isIconOnly ? 'mr-2' : '')"
|
||||||
|
/>
|
||||||
|
<slot v-if="!isIconOnly">{{ buttonLabel }}</slot>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
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 { Input } from '~/components/pub/ui/input'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* A "Ghost" wrapper component designed to group related form fields
|
||||||
|
* without adding unnecessary depth to the DOM tree.
|
||||||
|
* * Unlike a standard <div> wrapper, this component leverages Vue 3's
|
||||||
|
* multi-root feature to render the title and slot content as direct siblings.
|
||||||
|
* This ensures the parent Form's grid or flex layout remains intact.
|
||||||
|
* * @property {string} [title] for compiler marker.
|
||||||
|
*
|
||||||
|
* Example Usage:
|
||||||
|
*
|
||||||
|
<Fragment
|
||||||
|
v-slot="{ section }"
|
||||||
|
title="Tim Pelaksana Tindakan"
|
||||||
|
>
|
||||||
|
<p class="text-lg font-semibold">{{ section }}</p>
|
||||||
|
</Fragment>
|
||||||
|
*/
|
||||||
|
defineProps<{
|
||||||
|
title?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<slot :section="title" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export { default as Block } from './block.vue'
|
||||||
|
export { default as ButtonAction } from './button-action.vue'
|
||||||
|
export { default as FieldGroup } from './field-group.vue'
|
||||||
|
export { default as Field } from './field.vue'
|
||||||
|
export { default as FileField } from './file-field.vue'
|
||||||
|
export { default as Fragment } from './fragment.vue'
|
||||||
|
export { default as InputBase } from './input-base.vue'
|
||||||
|
export { default as Label } from './label.vue'
|
||||||
|
export { default as Select } from './select.vue'
|
||||||
|
export { default as TextAreaInput } from './text-area-input.vue'
|
||||||
|
export { default as TextCaptcha } from './text-captcha.vue'
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
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 { Input } from '~/components/pub/ui/input'
|
||||||
import { cn } from '~/lib/utils'
|
import { cn } from '~/lib/utils'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useFieldError } from 'vee-validate'
|
||||||
|
|
||||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
|
|
||||||
|
type InputType = 'text' | 'number' | 'password' | 'email' | 'date' | 'time' | 'datetime-local' | 'search' | 'tel'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldName: string
|
fieldName: string
|
||||||
placeholder: string
|
placeholder: string
|
||||||
label: string
|
label?: string
|
||||||
errors?: FormErrors
|
errors?: FormErrors
|
||||||
class?: string
|
class?: string
|
||||||
colSpan?: number
|
colSpan?: number
|
||||||
@@ -21,8 +22,13 @@ const props = defineProps<{
|
|||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
rightLabel?: string
|
rightLabel?: string
|
||||||
bottomLabel?: string
|
bottomLabel?: string
|
||||||
|
suffixMsg?: string
|
||||||
|
iconName?: string
|
||||||
|
inputType?: InputType
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { class: containerClass } = props
|
||||||
|
|
||||||
function handleInput(event: Event) {
|
function handleInput(event: Event) {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
let value = target.value
|
let value = target.value
|
||||||
@@ -44,12 +50,16 @@ function handleInput(event: Event) {
|
|||||||
target.dispatchEvent(new Event('input', { bubbles: true }))
|
target.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get error state from vee-validate
|
||||||
|
const fieldError = useFieldError(() => props.fieldName)
|
||||||
|
const hasError = computed(() => !!fieldError.value)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DE.Cell :col-span="colSpan || 1">
|
<DE.Cell :col-span="colSpan || 1">
|
||||||
<DE.Label
|
<DE.Label
|
||||||
v-if="label !== ''"
|
v-if="label"
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired && !isDisabled"
|
:is-required="isRequired && !isDisabled"
|
||||||
>
|
>
|
||||||
@@ -63,27 +73,51 @@ function handleInput(event: Event) {
|
|||||||
v-slot="{ componentField }"
|
v-slot="{ componentField }"
|
||||||
:name="fieldName"
|
:name="fieldName"
|
||||||
>
|
>
|
||||||
<FormItem :class="cn(`relative`,)">
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl :class="cn('relative', containerClass)">
|
||||||
<Input
|
<div class="relative w-full max-w-sm items-center">
|
||||||
:disabled="isDisabled"
|
<Input
|
||||||
v-bind="componentField"
|
:disabled="isDisabled"
|
||||||
:placeholder="placeholder"
|
v-bind="componentField"
|
||||||
:maxlength="maxLength"
|
:placeholder="placeholder"
|
||||||
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0', props.class)"
|
:maxlength="maxLength"
|
||||||
autocomplete="off"
|
:class="cn(hasError && 'border-red-500 focus-visible:border-red-500 focus-visible:ring-red-500')"
|
||||||
aria-autocomplete="none"
|
autocomplete="off"
|
||||||
autocorrect="off"
|
aria-autocomplete="none"
|
||||||
autocapitalize="off"
|
autocorrect="off"
|
||||||
spellcheck="false"
|
autocapitalize="off"
|
||||||
@input="handleInput"
|
spellcheck="false"
|
||||||
/>
|
@input="handleInput"
|
||||||
<p v-show="rightLabel" class="text-gray-400 absolute top-0 right-3">{{ rightLabel }}</p>
|
:type="inputType"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="suffixMsg"
|
||||||
|
class="absolute inset-y-0 end-0 flex items-center justify-center px-2 text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ suffixMsg }}
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
v-if="iconName"
|
||||||
|
:name="iconName"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-show="rightLabel"
|
||||||
|
class="absolute right-3 top-0 text-gray-400"
|
||||||
|
>
|
||||||
|
{{ rightLabel }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
</DE.Field>
|
</DE.Field>
|
||||||
<p v-show="bottomLabel" class="text-gray-400 mt-1">{{ bottomLabel }}</p>
|
<p
|
||||||
|
v-show="bottomLabel"
|
||||||
|
class="text-gray-400"
|
||||||
|
>
|
||||||
|
{{ bottomLabel }}
|
||||||
|
</p>
|
||||||
</DE.Cell>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SelectRoot } from 'radix-vue'
|
import { SelectRoot } from 'radix-vue'
|
||||||
import { watch } from 'vue'
|
import { watch, computed } from 'vue'
|
||||||
|
import { useFieldError } from 'vee-validate'
|
||||||
import SelectContent from '~/components/pub/ui/select/SelectContent.vue'
|
import SelectContent from '~/components/pub/ui/select/SelectContent.vue'
|
||||||
import SelectGroup from '~/components/pub/ui/select/SelectGroup.vue'
|
import SelectGroup from '~/components/pub/ui/select/SelectGroup.vue'
|
||||||
import SelectItem from '~/components/pub/ui/select/SelectItem.vue'
|
import SelectItem from '~/components/pub/ui/select/SelectItem.vue'
|
||||||
@@ -29,9 +30,14 @@ const props = defineProps<{
|
|||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
autoWidth?: boolean
|
autoWidth?: boolean
|
||||||
autoFill?: boolean
|
autoFill?: boolean
|
||||||
|
id?: string
|
||||||
// otherPlacement sudah tidak digunakan, diganti dengan priority system di Item interface
|
// otherPlacement sudah tidak digunakan, diganti dengan priority system di Item interface
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Get error state from vee-validate if id is provided
|
||||||
|
const fieldError = props.id ? useFieldError(() => props.id!) : ref(null)
|
||||||
|
const hasError = computed(() => !!fieldError.value)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: string]
|
'update:modelValue': [value: string]
|
||||||
}>()
|
}>()
|
||||||
@@ -121,6 +127,7 @@ watch(
|
|||||||
'cursor-not-allowed bg-gray-100 opacity-50': isDisabled,
|
'cursor-not-allowed bg-gray-100 opacity-50': isDisabled,
|
||||||
'bg-white text-black dark:bg-gray-800 dark:text-white': !isDisabled,
|
'bg-white text-black dark:bg-gray-800 dark:text-white': !isDisabled,
|
||||||
'w-full': !autoWidth,
|
'w-full': !autoWidth,
|
||||||
|
'border-red-500 focus:ring-red-500': hasError,
|
||||||
},
|
},
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormErrors } from '~/types/error'
|
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'
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
import * as DE from '~/components/pub/my-ui/doc-entry'
|
import * as DE from '~/components/pub/my-ui/doc-entry'
|
||||||
@@ -19,8 +15,11 @@ const props = defineProps<{
|
|||||||
maxLength?: number
|
maxLength?: number
|
||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
|
resize?: 'none' | 'y' | 'x'
|
||||||
|
rows?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { resize = 'none', class: className } = props
|
||||||
function handleInput(event: Event) {
|
function handleInput(event: Event) {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
let value = target.value
|
let value = target.value
|
||||||
@@ -47,7 +46,7 @@ function handleInput(event: Event) {
|
|||||||
<template>
|
<template>
|
||||||
<DE.Cell :col-span="colSpan || 1">
|
<DE.Cell :col-span="colSpan || 1">
|
||||||
<DE.Label
|
<DE.Label
|
||||||
class="font-medium mb-1"
|
:class="className"
|
||||||
v-if="label !== ''"
|
v-if="label !== ''"
|
||||||
:label-for="fieldName"
|
:label-for="fieldName"
|
||||||
:is-required="isRequired && !isDisabled"
|
:is-required="isRequired && !isDisabled"
|
||||||
@@ -65,11 +64,12 @@ function handleInput(event: Event) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
:rows="rows"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:maxlength="maxLength"
|
:maxlength="maxLength"
|
||||||
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0', props.class)"
|
:class="cn('focus:border-primary focus:ring-2 focus:ring-primary focus:ring-offset-0')"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
aria-autocomplete="none"
|
aria-autocomplete="none"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
@@ -83,4 +83,4 @@ function handleInput(event: Event) {
|
|||||||
</FormField>
|
</FormField>
|
||||||
</DE.Field>
|
</DE.Field>
|
||||||
</DE.Cell>
|
</DE.Cell>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from '~/lib/utils';
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
label: string
|
label: string
|
||||||
@@ -9,15 +9,17 @@ const props = defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="cn(`flex flex-col gap-1 lg:grid lg:grid-cols-[180px_minmax(0,1fr)] lg:gap-x-3`, props.class)">
|
<div :class="cn(`flex flex-col gap-0.5 lg:grid lg:grid-cols-[180px_auto_minmax(0,1fr)] lg:gap-x-3 lg:gap-y-1`, props.class)">
|
||||||
<!-- Label -->
|
<!-- Label -->
|
||||||
<span :class="cn(`text-md font-normal text-muted-foreground`, props.labelClass)">
|
<span :class="cn(`text-md font-normal text-muted-foreground`, props.labelClass)">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- Colon (hidden on mobile) -->
|
||||||
|
<span class="hidden text-md tracking-wide text-muted-foreground lg:block">:</span>
|
||||||
|
|
||||||
<!-- Value -->
|
<!-- Value -->
|
||||||
<span class="truncate lg:whitespace-normal">
|
<span class="text-md font-sans tracking-wide">
|
||||||
<span class="me-3 hidden lg:inline-block">:</span>
|
|
||||||
<slot />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ import { type EncounterItem } from "~/handlers/encounter-init.handler";
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialActiveMenu: string
|
initialActiveMenu: string
|
||||||
data: EncounterItem[]
|
data: EncounterItem[]
|
||||||
canCreate?: boolean
|
|
||||||
canRead?: boolean
|
|
||||||
canUpdate?: boolean
|
|
||||||
canDelete?: boolean
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const activeMenu = ref(props.initialActiveMenu)
|
const activeMenu = ref(props.initialActiveMenu)
|
||||||
@@ -42,12 +38,7 @@ function changeMenu(value: string) {
|
|||||||
class="flex-1 rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
|
class="flex-1 rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
|
||||||
<component
|
<component
|
||||||
:is="data.find((m) => m.id === activeMenu)?.component"
|
:is="data.find((m) => m.id === activeMenu)?.component"
|
||||||
v-bind="data.find((m) => m.id === activeMenu)?.props"
|
v-bind="data.find((m) => m.id === activeMenu)?.props" />
|
||||||
:can-create="canCreate"
|
|
||||||
:can-read="canRead"
|
|
||||||
:can-update="canUpdate"
|
|
||||||
:can-delete="canDelete"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export type ClickType = 'back' | 'draft' | 'submit'
|
||||||
@@ -93,7 +93,7 @@ function getButtonClass(pageNumber: number) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex w-full min-w-0 items-center justify-between px-2 py-2">
|
<div class="flex w-full min-w-0 items-center justify-between overflow-x-scroll px-2 py-2 md:overflow-hidden">
|
||||||
<!-- Info text -->
|
<!-- Info text -->
|
||||||
<div
|
<div
|
||||||
v-if="showInfo && endRecord > 0"
|
v-if="showInfo && endRecord > 0"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ErrorMessage } from 'vee-validate'
|
||||||
|
import { cn } from '~/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
name: string
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ErrorMessage
|
||||||
|
:name="props.name"
|
||||||
|
v-slot="{ message }"
|
||||||
|
>
|
||||||
|
<p :class="cn('font-sans text-[0.8rem] text-destructive', props.class)">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
</ErrorMessage>
|
||||||
|
</template>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { default as ArrayMessage } from './array-message.vue'
|
||||||
export { default as FormControl } from './FormControl.vue'
|
export { default as FormControl } from './FormControl.vue'
|
||||||
export { default as FormDescription } from './FormDescription.vue'
|
export { default as FormDescription } from './FormDescription.vue'
|
||||||
export { default as FormItem } from './FormItem.vue'
|
export { default as FormItem } from './FormItem.vue'
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
|
|||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'border-input dark:bg-slate-950 ring-offset-background placeholder:text-muted-foreground flex h-9 md:h-8 2xl:h-9 w-full rounded-md border border-gray-400 px-3 py-2 md:text-xs 2xl:text-sm file:border-0 file:bg-transparent md:file:!text-xs xl:file:!text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50',
|
'border-input dark:bg-slate-950 ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary focus-visible:ring-offset-0 focus-visible:border-primary flex h-9 md:h-8 2xl:h-9 w-full rounded-md border border-gray-400 px-3 py-2 md:text-xs 2xl:text-sm file:border-0 file:bg-transparent md:file:!text-xs xl:file:!text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
Reference in New Issue
Block a user