ketekny-ui-kit 1.0.38 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ketekny-ui-kit",
3
3
  "type": "module",
4
- "version": "1.0.38",
4
+ "version": "1.0.40",
5
5
  "description": "A Vue 3 UI component library with Tailwind CSS styling",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -40,6 +40,9 @@
40
40
  "dependencies": {
41
41
  "@primeuix/themes": "^1.1.1",
42
42
  "@vuepic/vue-datepicker": "^11.0.2",
43
+ "class-variance-authority": "^0.7.1",
44
+ "clsx": "^2.1.1",
45
+ "element-plus": "^2.13.5",
43
46
  "he-tree-vue": "^3.1.2",
44
47
  "json-editor-vue": "^0.18.1",
45
48
  "lucide-vue-next": "^0.511.0",
@@ -49,6 +52,7 @@
49
52
  "quill": "^2.0.3",
50
53
  "reka-ui": "^2.8.2",
51
54
  "simple-code-editor": "^2.0.9",
55
+ "tailwind-merge": "^3.5.0",
52
56
  "vue-router": "^4.6.4",
53
57
  "vue3-easy-data-table": "^1.5.47"
54
58
  }
@@ -0,0 +1,6 @@
1
+ import { clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,124 @@
1
+ <template>
2
+ <el-button
3
+ :type="resolvedVisualType"
4
+ :native-type="resolvedNativeType"
5
+ :size="resolvedSize"
6
+ :plain="resolvedPlain"
7
+ :round="round"
8
+ :circle="resolvedCircle"
9
+ :loading="loading"
10
+ :disabled="disabled"
11
+ :icon="iconComponent"
12
+ :autofocus="autofocus"
13
+ :title="tooltip"
14
+ :aria-label="resolvedAriaLabel"
15
+ :class="buttonClass"
16
+ @click="$emit('click', $event)"
17
+ >
18
+ <slot v-if="!resolvedIconOnly">{{ label }}</slot>
19
+ </el-button>
20
+ </template>
21
+
22
+ <script setup>
23
+ import { computed, getCurrentInstance } from 'vue'
24
+ import { ElButton } from 'element-plus'
25
+ import * as lucideIcons from 'lucide-vue-next'
26
+
27
+ const STYLE_TYPES = ['default', 'primary', 'success', 'warning', 'danger', 'info']
28
+ const NATIVE_TYPES = ['button', 'submit', 'reset']
29
+
30
+ const props = defineProps({
31
+ label: { type: String, default: '' },
32
+ type: {
33
+ type: String,
34
+ default: 'default',
35
+ validator: (val) => ['default', 'primary', 'success', 'warning', 'danger', 'info', 'button', 'submit', 'reset'].includes(val),
36
+ },
37
+ size: {
38
+ type: String,
39
+ default: 'default',
40
+ validator: (val) => ['large', 'default', 'small', 'normal'].includes(val),
41
+ },
42
+ plain: { type: Boolean, default: false },
43
+ round: { type: Boolean, default: false },
44
+ circle: { type: Boolean, default: false },
45
+ loading: { type: Boolean, default: false },
46
+ disabled: { type: Boolean, default: false },
47
+ icon: { type: [String, Object, Function], default: null },
48
+ autofocus: { type: Boolean, default: false },
49
+ primary: { type: Boolean, default: false },
50
+ secondary: { type: Boolean, default: false },
51
+ outlined: { type: Boolean, default: false },
52
+ danger: { type: Boolean, default: false },
53
+ warning: { type: Boolean, default: false },
54
+ success: { type: Boolean, default: false },
55
+ ariaLabel: { type: String, default: null },
56
+ tooltip: { type: String, default: null },
57
+ variant: {
58
+ type: String,
59
+ default: null,
60
+ validator: (val) => val == null || ['solid', 'soft', 'outlined', 'plain'].includes(val),
61
+ },
62
+ small: { type: Boolean, default: false },
63
+ fullWidth: { type: Boolean, default: false },
64
+ iconOnly: { type: Boolean, default: false },
65
+ truncate: { type: Boolean, default: false },
66
+ })
67
+
68
+ defineEmits(['click'])
69
+
70
+ const instance = getCurrentInstance()
71
+ const passedProps = new Set(Object.keys(instance?.vnode.props || {}))
72
+ const hasProp = (...names) => names.some((name) => passedProps.has(name))
73
+
74
+ const resolvedVisualType = computed(() => {
75
+ if (props.danger) return 'danger'
76
+ if (props.success) return 'success'
77
+ if (props.warning) return 'warning'
78
+ if (props.secondary) return 'info'
79
+ if (props.primary) return 'primary'
80
+ return STYLE_TYPES.includes(props.type) ? props.type : 'default'
81
+ })
82
+
83
+ const resolvedNativeType = computed(() => {
84
+ return NATIVE_TYPES.includes(props.type) ? props.type : 'button'
85
+ })
86
+
87
+ const resolvedSize = computed(() => {
88
+ if (props.small) return 'small'
89
+ if (props.size === 'normal') return 'default'
90
+ return ['large', 'default', 'small'].includes(props.size) ? props.size : 'default'
91
+ })
92
+
93
+ const resolvedPlain = computed(() => {
94
+ if (props.outlined) return true
95
+ if (hasProp('variant')) return props.variant === 'outlined' || props.variant === 'plain'
96
+ return props.plain
97
+ })
98
+
99
+ const resolvedCircle = computed(() => {
100
+ if (hasProp('iconOnly', 'icon-only')) return props.iconOnly
101
+ return props.circle
102
+ })
103
+
104
+ const resolvedIconOnly = computed(() => props.iconOnly)
105
+
106
+ const iconComponent = computed(() => {
107
+ if (typeof props.icon === 'string') return lucideIcons[props.icon] || null
108
+ return props.icon
109
+ })
110
+
111
+ const resolvedAriaLabel = computed(() => {
112
+ if (props.ariaLabel) return props.ariaLabel
113
+ if (props.label && !resolvedIconOnly.value) return props.label
114
+ if (typeof props.icon === 'string') return props.icon.replace(/([a-z])([A-Z])/g, '$1 $2')
115
+ return null
116
+ })
117
+
118
+ const buttonClass = computed(() => {
119
+ return [
120
+ props.fullWidth ? '!w-full justify-center' : '',
121
+ props.truncate ? 'max-w-full [&>span]:max-w-full [&>span]:truncate [&>span]:inline-block' : '',
122
+ ]
123
+ })
124
+ </script>
@@ -2,9 +2,9 @@
2
2
  <Teleport to="body">
3
3
  <Transition name="fade">
4
4
  <div v-if="visible" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-40" style="z-index: 1500">
5
- <div class="w-full max-w-md p-6 bg-white border rounded-lg shadow-xl border-primary/20 dark:bg-slate-800 dark:border-slate-700">
5
+ <div class="w-full max-w-md p-6 bg-white border rounded-lg shadow-xl border-primary/20 dark:bg-zinc-900 dark:border-zinc-800">
6
6
  <h2 class="mb-2 text-lg font-semibold text-primary">{{ title }}</h2>
7
- <div class="mb-6 text-gray-700 dark:text-slate-300"><span v-html="message" /></div>
7
+ <div class="mb-6 text-gray-700 dark:text-zinc-300"><span v-html="message" /></div>
8
8
  <div class="flex justify-end gap-3">
9
9
  <kButton :disabled="loading" secondary label="Άκυρο" @click="cancel" />
10
10
  <kButton :loading="loading" danger label="Συνέχεια" @click="confirm" />
@@ -0,0 +1,147 @@
1
+ <template>
2
+ <div>
3
+ <label v-if="label" class="block mb-1 text-sm font-medium text-slate-700 dark:text-slate-300">{{ label }}</label>
4
+ <el-date-picker
5
+ :id="id"
6
+ :model-value="normalizedModelValue"
7
+ :type="pickerType"
8
+ :placeholder="placeholder"
9
+ :disabled="disabled"
10
+ :readonly="readonly"
11
+ :clearable="clearable"
12
+ :editable="editable"
13
+ :size="size"
14
+ :format="resolvedFormat"
15
+ :value-format="resolvedValueFormat"
16
+ :disabled-date="disabledDate"
17
+ :start-placeholder="startPlaceholder"
18
+ :end-placeholder="endPlaceholder"
19
+ :range-separator="rangeSeparator"
20
+ class="k-date-v2"
21
+ style="width: 100%"
22
+ @update:model-value="$emit('update:modelValue', $event)"
23
+ @change="$emit('change', $event)"
24
+ />
25
+ <p v-if="hasError" class="mt-1 text-xs text-red-600 dark:text-red-400">{{ typeof error === 'string' ? error : '' }}</p>
26
+ <p v-else-if="info" class="mt-1 text-xs text-slate-500 dark:text-slate-400">{{ info }}</p>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup>
31
+ import { computed } from 'vue'
32
+ import { ElDatePicker } from 'element-plus'
33
+
34
+ const props = defineProps({
35
+ modelValue: { type: [String, Date, Array], default: null },
36
+ id: {
37
+ type: String,
38
+ default: () => `datepicker-${Math.random().toString(36).slice(2, 11)}`,
39
+ },
40
+ label: { type: String, default: '' },
41
+ type: {
42
+ type: String,
43
+ default: 'date',
44
+ validator: (val) => ['year', 'month', 'date', 'dates', 'datetime', 'week', 'datetimerange', 'daterange', 'monthrange', 'yearMonth'].includes(val),
45
+ },
46
+ placeholder: { type: String, default: 'Select date' },
47
+ startPlaceholder: { type: String, default: 'Start date' },
48
+ endPlaceholder: { type: String, default: 'End date' },
49
+ rangeSeparator: { type: String, default: 'To' },
50
+ disabled: { type: Boolean, default: false },
51
+ readonly: { type: Boolean, default: false },
52
+ clearable: { type: Boolean, default: true },
53
+ editable: { type: Boolean, default: true },
54
+ size: {
55
+ type: String,
56
+ default: 'default',
57
+ validator: (val) => ['large', 'default', 'small'].includes(val),
58
+ },
59
+ format: { type: String, default: '' },
60
+ valueFormat: { type: String, default: '' },
61
+ disabledDate: { type: Function, default: undefined },
62
+ error: { type: [String, Boolean], default: null },
63
+ info: { type: String, default: '' },
64
+ })
65
+
66
+ defineEmits(['update:modelValue', 'change'])
67
+
68
+ const pickerType = computed(() => props.type === 'yearMonth' ? 'month' : props.type)
69
+ const hasError = computed(() => props.error != null && props.error !== false)
70
+
71
+ const defaultFormatByType = {
72
+ date: 'YYYY-MM-DD',
73
+ dates: 'YYYY-MM-DD',
74
+ week: 'gggg[w]ww',
75
+ year: 'YYYY',
76
+ month: 'YYYY-MM',
77
+ datetime: 'YYYY-MM-DD HH:mm:ss',
78
+ monthrange: 'YYYY-MM',
79
+ daterange: 'YYYY-MM-DD',
80
+ datetimerange: 'YYYY-MM-DD HH:mm:ss',
81
+ }
82
+
83
+ const resolvedFormat = computed(() => {
84
+ const custom = String(props.format || '').trim()
85
+ if (custom) return custom
86
+ return defaultFormatByType[pickerType.value] || 'YYYY-MM-DD'
87
+ })
88
+
89
+ const resolvedValueFormat = computed(() => {
90
+ const custom = String(props.valueFormat || '').trim()
91
+ if (custom) return custom
92
+ return defaultFormatByType[pickerType.value] || 'YYYY-MM-DD'
93
+ })
94
+
95
+ function normalizeIsoString(value, type) {
96
+ if (typeof value !== 'string' || !value.includes('T')) return value
97
+
98
+ if (type === 'year') return value.slice(0, 4)
99
+ if (type === 'month' || type === 'monthrange') return value.slice(0, 7)
100
+
101
+ if (type === 'datetime' || type === 'datetimerange') {
102
+ const normalized = value.replace('T', ' ').replace(/Z$/, '')
103
+ const fullMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})\s(\d{2}:\d{2}:\d{2})/)
104
+ if (fullMatch) return `${fullMatch[1]} ${fullMatch[2]}`
105
+ const shortMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})\s(\d{2}:\d{2})/)
106
+ if (shortMatch) return `${shortMatch[1]} ${shortMatch[2]}:00`
107
+ return normalized
108
+ }
109
+
110
+ const dateMatch = value.match(/^(\d{4}-\d{2}-\d{2})/)
111
+ return dateMatch ? dateMatch[1] : value
112
+ }
113
+
114
+ const normalizedModelValue = computed(() => {
115
+ if (Array.isArray(props.modelValue)) {
116
+ return props.modelValue.map((item) => normalizeIsoString(item, pickerType.value))
117
+ }
118
+ return normalizeIsoString(props.modelValue, pickerType.value)
119
+ })
120
+ </script>
121
+
122
+ <style>
123
+ /* Global because Element Plus picker panel is teleported to body. */
124
+ html.dark .el-date-picker,
125
+ html.dark .el-date-range-picker {
126
+ --el-datepicker-inrange-bg-color: rgba(3, 105, 161, 0.22);
127
+ --el-datepicker-inrange-hover-bg-color: rgba(3, 105, 161, 0.32);
128
+ }
129
+
130
+ html.dark .el-date-table td.in-range .el-date-table-cell,
131
+ html.dark .el-month-table td.in-range .el-date-table-cell,
132
+ html.dark .el-year-table td.in-range .el-date-table-cell {
133
+ background-color: rgba(3, 105, 161, 0.22);
134
+ }
135
+
136
+ html.dark .el-date-table td.in-range .el-date-table-cell:hover,
137
+ html.dark .el-month-table td.in-range .el-date-table-cell:hover,
138
+ html.dark .el-year-table td.in-range .el-date-table-cell:hover {
139
+ background-color: rgba(3, 105, 161, 0.32);
140
+ }
141
+
142
+ html.dark .el-month-table td.available:hover .el-date-table-cell,
143
+ html.dark .el-year-table td.available:hover .el-date-table-cell {
144
+ background-color: rgba(148, 163, 184, 0.16);
145
+ border-radius: 24px;
146
+ }
147
+ </style>
@@ -8,7 +8,7 @@
8
8
  <transition name="dialog">
9
9
  <div
10
10
  ref="dialogPanel"
11
- class="relative bg-white dark:bg-slate-900 shadow-lg overflow-hidden rounded-2xl flex flex-col max-h-[calc(100dvh-1rem)] sm:max-h-[90vh] m-2 sm:m-4"
11
+ class="relative bg-white dark:bg-zinc-900 shadow-lg overflow-hidden rounded-2xl flex flex-col max-h-[calc(100dvh-1rem)] sm:max-h-[90vh] m-2 sm:m-4"
12
12
  :class="dialogClasses"
13
13
  :role="'dialog'"
14
14
  :aria-modal="'true'"
@@ -17,16 +17,16 @@
17
17
  v-show="visible"
18
18
  >
19
19
  <!-- Header -->
20
- <div class="flex flex-row items-center p-3 text-white bg-sky-800 shrink-0 sm:p-4">
21
- <div :id="titleId" class="text-base font-semibold !text-white sm:text-xl">{{ title }}</div>
20
+ <div class="flex flex-row items-center px-4 py-3 border-b border-primary/20 bg-primary/5 dark:bg-primary/10 shrink-0">
21
+ <div :id="titleId" class="text-sm font-semibold text-primary sm:text-base">{{ title }}</div>
22
22
  <div class="flex-1" />
23
23
  <button
24
24
  type="button"
25
- class="p-1 text-black transition duration-100 ease-in-out rounded-full cursor-pointer text-secondary hover:bg-white hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
25
+ class="p-1.5 rounded-md text-zinc-400 hover:text-primary dark:text-zinc-500 dark:hover:text-primary hover:bg-primary/10 transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
26
26
  aria-label="Close dialog"
27
27
  @click="close"
28
28
  >
29
- <X />
29
+ <X class="w-5 h-5" />
30
30
  </button>
31
31
  </div>
32
32
 
@@ -36,14 +36,14 @@
36
36
  </div>
37
37
 
38
38
  <!-- Scrollable slot content -->
39
- <div class="flex-1 p-3 overflow-auto bg-secondary dark:bg-slate-800/60 sm:p-4">
39
+ <div class="flex-1 p-3 overflow-auto sm:p-4">
40
40
  <slot></slot>
41
41
  </div>
42
42
 
43
43
  <!-- Footer -->
44
44
  <div
45
45
  v-if="$slots.footer"
46
- class="flex flex-col-reverse gap-2 p-3 border-t shrink-0 border-slate-100 dark:border-slate-700 sm:flex-row sm:justify-end sm:p-4"
46
+ class="flex flex-col-reverse gap-2 p-3 border-t shrink-0 border-zinc-200 dark:border-zinc-800 sm:flex-row sm:justify-end sm:p-4"
47
47
  >
48
48
  <slot name="footer"></slot>
49
49
  </div>
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <el-dialog
3
+ :model-value="resolvedOpen"
4
+ :title="title"
5
+ :width="width"
6
+ :fullscreen="resolvedFullscreen"
7
+ :modal="modal"
8
+ :append-to-body="appendToBody"
9
+ :close-on-click-modal="resolvedCloseOnBackdrop"
10
+ :close-on-press-escape="resolvedCloseOnEsc"
11
+ :show-close="showClose"
12
+ :draggable="draggable"
13
+ :center="center"
14
+ :align-center="alignCenter"
15
+ :destroy-on-close="destroyOnClose"
16
+ @update:model-value="emitModelUpdate"
17
+ @open="$emit('open')"
18
+ @opened="$emit('opened')"
19
+ @close="$emit('close')"
20
+ @closed="$emit('closed')"
21
+ >
22
+ <template v-if="$slots.header" #header>
23
+ <slot name="header"></slot>
24
+ </template>
25
+ <slot></slot>
26
+ <template v-if="$slots.footer" #footer>
27
+ <slot name="footer"></slot>
28
+ </template>
29
+ </el-dialog>
30
+ </template>
31
+
32
+ <script setup>
33
+ import { computed, getCurrentInstance } from 'vue'
34
+ import { ElDialog } from 'element-plus'
35
+
36
+ const props = defineProps({
37
+ modelValue: { type: Boolean, default: false },
38
+ visible: { type: Boolean, default: undefined },
39
+ title: { type: String, default: '' },
40
+ width: { type: [String, Number], default: '50%' },
41
+ fullscreen: { type: Boolean, default: false },
42
+ maximized: { type: Boolean, default: false },
43
+ modal: { type: Boolean, default: true },
44
+ appendToBody: { type: Boolean, default: false },
45
+ closeOnClickModal: { type: Boolean, default: true },
46
+ closeOnBackdrop: { type: Boolean, default: true },
47
+ closeOnPressEscape: { type: Boolean, default: true },
48
+ closeOnEsc: { type: Boolean, default: true },
49
+ showClose: { type: Boolean, default: true },
50
+ draggable: { type: Boolean, default: false },
51
+ center: { type: Boolean, default: false },
52
+ alignCenter: { type: Boolean, default: false },
53
+ destroyOnClose: { type: Boolean, default: false },
54
+ })
55
+
56
+ const emit = defineEmits(['update:modelValue', 'update:visible', 'open', 'opened', 'close', 'closed'])
57
+
58
+ const instance = getCurrentInstance()
59
+ const passedProps = new Set(Object.keys(instance?.vnode.props || {}))
60
+ const hasProp = (...names) => names.some((name) => passedProps.has(name))
61
+
62
+ const resolvedOpen = computed(() => (hasProp('visible') ? props.visible : props.modelValue))
63
+ const resolvedCloseOnBackdrop = computed(() => (hasProp('closeOnBackdrop', 'close-on-backdrop') ? props.closeOnBackdrop : props.closeOnClickModal))
64
+ const resolvedCloseOnEsc = computed(() => (hasProp('closeOnEsc', 'close-on-esc') ? props.closeOnEsc : props.closeOnPressEscape))
65
+ const resolvedFullscreen = computed(() => (hasProp('maximized') ? props.maximized : props.fullscreen))
66
+
67
+ function emitModelUpdate(value) {
68
+ emit('update:modelValue', value)
69
+ emit('update:visible', value)
70
+ }
71
+ </script>
@@ -0,0 +1,128 @@
1
+ <template>
2
+ <DialogRoot :open="resolvedOpen" @update:open="emitModelUpdate">
3
+ <DialogTrigger v-if="$slots.trigger" as-child>
4
+ <slot name="trigger"></slot>
5
+ </DialogTrigger>
6
+
7
+ <DialogPortal>
8
+ <DialogOverlay class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-overlayIn data-[state=closed]:animate-overlayOut" />
9
+
10
+ <DialogContent
11
+ class="fixed z-50 bg-white dark:bg-slate-900 shadow-lg rounded-xl overflow-hidden flex flex-col
12
+ top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
13
+ w-[calc(100vw-2rem)] max-h-[calc(100dvh-2rem)]
14
+ data-[state=open]:animate-dialogIn data-[state=closed]:animate-dialogOut"
15
+ :class="resolvedWidthClass"
16
+ @escape-key-down="resolvedCloseOnEscape ? undefined : $event.preventDefault()"
17
+ @interact-outside="closeOnBackdrop ? undefined : $event.preventDefault()"
18
+ >
19
+ <!-- Header -->
20
+ <div class="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-700 shrink-0">
21
+ <DialogTitle class="text-lg font-semibold text-slate-900 dark:text-slate-100">
22
+ {{ title }}
23
+ </DialogTitle>
24
+ <DialogClose
25
+ class="p-1.5 rounded-lg text-slate-400 hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
26
+ aria-label="Close"
27
+ >
28
+ <X class="w-4 h-4" />
29
+ </DialogClose>
30
+ </div>
31
+
32
+ <!-- Description (optional) -->
33
+ <DialogDescription v-if="description" class="px-4 pt-3 text-sm text-slate-500 dark:text-slate-400">
34
+ {{ description }}
35
+ </DialogDescription>
36
+
37
+ <!-- Body -->
38
+ <div class="flex-1 p-4 overflow-auto">
39
+ <slot></slot>
40
+ </div>
41
+
42
+ <!-- Footer -->
43
+ <div v-if="$slots.footer" class="flex justify-end gap-2 p-4 border-t border-slate-200 dark:border-slate-700 shrink-0">
44
+ <slot name="footer"></slot>
45
+ </div>
46
+ </DialogContent>
47
+ </DialogPortal>
48
+ </DialogRoot>
49
+ </template>
50
+
51
+ <script setup>
52
+ import { computed, getCurrentInstance } from 'vue'
53
+ import {
54
+ DialogRoot,
55
+ DialogTrigger,
56
+ DialogPortal,
57
+ DialogOverlay,
58
+ DialogContent,
59
+ DialogTitle,
60
+ DialogDescription,
61
+ DialogClose,
62
+ } from 'reka-ui'
63
+ import { X } from 'lucide-vue-next'
64
+
65
+ const props = defineProps({
66
+ modelValue: { type: Boolean, default: false },
67
+ visible: { type: Boolean, default: undefined },
68
+ title: { type: String, default: '' },
69
+ description: { type: String, default: '' },
70
+ width: { type: String, default: 'md' },
71
+ maximized: { type: Boolean, default: false },
72
+ closeOnBackdrop: { type: Boolean, default: true },
73
+ closeOnEscape: { type: Boolean, default: true },
74
+ closeOnEsc: { type: Boolean, default: true },
75
+ })
76
+
77
+ const emit = defineEmits(['update:modelValue', 'update:visible'])
78
+
79
+ const WIDTH_CLASS_MAP = {
80
+ sm: 'sm:max-w-sm',
81
+ md: 'sm:max-w-lg',
82
+ lg: 'sm:max-w-2xl',
83
+ xl: 'sm:max-w-4xl',
84
+ full: 'sm:max-w-[calc(100vw-4rem)]',
85
+ }
86
+
87
+ const instance = getCurrentInstance()
88
+ const passedProps = new Set(Object.keys(instance?.vnode.props || {}))
89
+ const hasProp = (...names) => names.some((name) => passedProps.has(name))
90
+
91
+ const resolvedOpen = computed(() => (hasProp('visible') ? props.visible : props.modelValue))
92
+ const resolvedCloseOnEscape = computed(() => (hasProp('closeOnEsc', 'close-on-esc') ? props.closeOnEsc : props.closeOnEscape))
93
+
94
+ const resolvedWidthClass = computed(() => {
95
+ if (hasProp('maximized') && props.maximized) return WIDTH_CLASS_MAP.full
96
+ const value = String(props.width || 'md')
97
+ return WIDTH_CLASS_MAP[value] || value
98
+ })
99
+
100
+ function emitModelUpdate(value) {
101
+ emit('update:modelValue', value)
102
+ emit('update:visible', value)
103
+ }
104
+ </script>
105
+
106
+ <style scoped>
107
+ @keyframes overlayIn {
108
+ from { opacity: 0; }
109
+ to { opacity: 1; }
110
+ }
111
+ @keyframes overlayOut {
112
+ from { opacity: 1; }
113
+ to { opacity: 0; }
114
+ }
115
+ @keyframes dialogIn {
116
+ from { opacity: 0; transform: translate(-50%, -50%) scale(0.96); }
117
+ to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
118
+ }
119
+ @keyframes dialogOut {
120
+ from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
121
+ to { opacity: 0; transform: translate(-50%, -50%) scale(0.96); }
122
+ }
123
+
124
+ .animate-overlayIn { animation: overlayIn 200ms ease-out; }
125
+ .animate-overlayOut { animation: overlayOut 150ms ease-in; }
126
+ .animate-dialogIn { animation: dialogIn 200ms ease-out; }
127
+ .animate-dialogOut { animation: dialogOut 150ms ease-in; }
128
+ </style>