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 +5 -1
- package/src/lib/utils.js +6 -0
- package/src/ui/kButton_v2.vue +124 -0
- package/src/ui/kConfirmDialog.vue +2 -2
- package/src/ui/kDateSelector_v2.vue +147 -0
- package/src/ui/kDialog.vue +7 -7
- package/src/ui/kDialog_v2.vue +71 -0
- package/src/ui/kDialog_v3.vue +128 -0
- package/src/ui/kDialog_v4.vue +140 -0
- package/src/ui/kDrawer_v2.vue +77 -0
- package/src/ui/kInput_v2.vue +188 -0
- package/src/ui/kMessage_v2.vue +67 -0
- package/src/ui/kMessage_v3.vue +152 -0
- package/src/ui/kMessage_v4.vue +133 -0
- package/src/ui/kProgress_v2.vue +45 -0
- package/src/ui/kSelect_v2.vue +124 -0
- package/src/ui/kSwitch_v2.vue +49 -0
- package/src/ui/kTag_v2.vue +44 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ketekny-ui-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
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
|
}
|
package/src/lib/utils.js
ADDED
|
@@ -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-
|
|
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-
|
|
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>
|
package/src/ui/kDialog.vue
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<transition name="dialog">
|
|
9
9
|
<div
|
|
10
10
|
ref="dialogPanel"
|
|
11
|
-
class="relative bg-white dark:bg-
|
|
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
|
|
21
|
-
<div :id="titleId" class="text-
|
|
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
|
|
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
|
|
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-
|
|
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>
|