ketekny-ui-kit 1.0.37 → 1.0.39
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/kArrayList.vue +3 -3
- package/src/ui/kButton_v2.vue +124 -0
- package/src/ui/kConfirmDialog.vue +2 -2
- package/src/ui/kDatatable.vue +37 -0
- package/src/ui/kDateSelector_v2.vue +147 -0
- 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/kEditor.vue +7 -7
- package/src/ui/kInputDialog.vue +4 -4
- package/src/ui/kInput_v2.vue +188 -0
- package/src/ui/kList.vue +7 -7
- package/src/ui/kMenu.vue +4 -4
- 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/kSearch.vue +2 -2
- package/src/ui/kSelect.vue +33 -17
- package/src/ui/kSelectButton.vue +2 -2
- package/src/ui/kSelect_v2.vue +124 -0
- package/src/ui/kSkeleton.vue +1 -1
- package/src/ui/kSwitch_v2.vue +49 -0
- package/src/ui/kTag_v2.vue +44 -0
- package/src/ui/kTags.vue +9 -6
- package/src/ui/kTextArea.vue +6 -6
- package/src/ui/kToolbar.vue +7 -7
- package/src/ui/kTree.vue +22 -0
- package/src/ui/kUploader.vue +8 -8
- package/src/ui/themes/kInput.theme.js +10 -10
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.39",
|
|
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
package/src/ui/kArrayList.vue
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
:rows="rows"
|
|
13
13
|
:disabled="disabled"
|
|
14
14
|
:class="[
|
|
15
|
-
'block w-full p-3 transition border border-gray-300 shadow-sm outline-none rounded-xl',
|
|
16
|
-
disabled ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white focus:border-primary focus:ring-2 focus:ring-primary/20',
|
|
15
|
+
'block w-full p-3 transition border border-gray-300 shadow-sm outline-none rounded-xl dark:border-slate-600 dark:text-slate-100 dark:placeholder-slate-500',
|
|
16
|
+
disabled ? 'bg-gray-100 text-gray-400 cursor-not-allowed dark:bg-slate-900 dark:text-slate-500' : 'bg-white focus:border-primary focus:ring-2 focus:ring-primary/20 dark:bg-slate-800',
|
|
17
17
|
]"
|
|
18
18
|
@focus="isFocused = true"
|
|
19
19
|
@blur="isFocused = false"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
@compositionend="onCompositionEnd"
|
|
22
22
|
></textarea>
|
|
23
23
|
|
|
24
|
-
<div class="flex items-center justify-between mt-2 text-xs text-gray-500">
|
|
24
|
+
<div class="flex items-center justify-between mt-2 text-xs text-gray-500 dark:text-slate-400">
|
|
25
25
|
<span>Items: <span class="font-semibold text-primary">{{ count }}</span></span>
|
|
26
26
|
<span v-if="hint">{{ hint }}</span>
|
|
27
27
|
</div>
|
|
@@ -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">
|
|
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">
|
|
6
6
|
<h2 class="mb-2 text-lg font-semibold text-primary">{{ title }}</h2>
|
|
7
|
-
<div class="mb-6 text-gray-700"><span v-html="message" /></div>
|
|
7
|
+
<div class="mb-6 text-gray-700 dark:text-slate-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" />
|
package/src/ui/kDatatable.vue
CHANGED
|
@@ -694,6 +694,43 @@ export default {
|
|
|
694
694
|
color: #1f2937;
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
+
.dark .customize-table {
|
|
698
|
+
--easy-table-row-border: 1px solid #374151;
|
|
699
|
+
|
|
700
|
+
--easy-table-header-font-color: #e5e7eb;
|
|
701
|
+
|
|
702
|
+
--easy-table-body-even-row-font-color: #e5e7eb;
|
|
703
|
+
--easy-table-body-row-font-color: #e5e7eb;
|
|
704
|
+
--easy-table-body-row-hover-font-color: #e5e7eb;
|
|
705
|
+
--easy-table-body-row-hover-background-color: #1e3a5f;
|
|
706
|
+
|
|
707
|
+
--easy-table-footer-font-color: #e5e7eb;
|
|
708
|
+
--easy-table-rows-per-page-selector-font-color: #e5e7eb;
|
|
709
|
+
|
|
710
|
+
--easy-table-loading-mask-background-color: rgba(15, 23, 42, 0.6);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.dark .k-datatable-checkbox {
|
|
714
|
+
background-color: #1e293b;
|
|
715
|
+
border-color: #64748b;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.dark .k-datatable-checkbox:disabled {
|
|
719
|
+
background-color: #334155;
|
|
720
|
+
border-color: #475569;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.dark .customize-table .easy-data-table__footer select {
|
|
724
|
+
background-color: #1e293b;
|
|
725
|
+
color: #e5e7eb;
|
|
726
|
+
border-color: #3b82f6;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.dark .customize-table .easy-data-table__footer select option {
|
|
730
|
+
background-color: #1e293b;
|
|
731
|
+
color: #e5e7eb;
|
|
732
|
+
}
|
|
733
|
+
|
|
697
734
|
@media (max-width: 640px) {
|
|
698
735
|
.k-datatable-wrapper .customize-table {
|
|
699
736
|
--easy-table-header-font-size: 12px;
|
|
@@ -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>
|
|
@@ -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>
|