ketekny-ui-kit 1.0.59 → 1.0.61
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/README.md +1 -1
- package/index.js +1 -2
- package/package.json +1 -1
- package/src/ui/kDateSelector.vue +143 -317
- package/src/ui/kMenu.vue +15 -5
- package/src/ui/kSelect.vue +4 -1
- package/src/ui/kButton_v2.vue +0 -124
- package/src/ui/kDateSelector_v2.vue +0 -152
- package/src/ui/kDialog_v2.vue +0 -71
- package/src/ui/kDialog_v3.vue +0 -128
- package/src/ui/kDialog_v4.vue +0 -140
- package/src/ui/kDrawer_v2.vue +0 -77
- package/src/ui/kInput_v2.vue +0 -188
- package/src/ui/kMessage_v2.vue +0 -67
- package/src/ui/kMessage_v3.vue +0 -152
- package/src/ui/kMessage_v4.vue +0 -133
- package/src/ui/kProgress_v2.vue +0 -45
- package/src/ui/kSelect_v2.vue +0 -124
- package/src/ui/kSwitch_v2.vue +0 -49
- package/src/ui/kTag_v2.vue +0 -44
- package/src/ui/kToggle_v2.vue +0 -77
- package/src/ui/kToggle_v3.vue +0 -132
- package/src/ui/kToggle_v4.vue +0 -122
package/README.md
CHANGED
|
@@ -155,7 +155,7 @@ Components:
|
|
|
155
155
|
- `kMessage`, `kCode`, `kToolbar`, `kTable`, `kTabs`, `kChip`
|
|
156
156
|
- `kSpinner`, `kDatatable`, `kIcon`, `kMenu`, `kSkeleton`
|
|
157
157
|
- `kProgressBar`, `kTree`, `kButton`, `kSelect`, `kUploader`
|
|
158
|
-
- `kToggle`, `kInput`, `kDateSelector`, `
|
|
158
|
+
- `kToggle`, `kInput`, `kDateSelector`, `kEditor`
|
|
159
159
|
- `kSelectButton`, `kTags`, `kSearch`, `kArrayList`, `kList`, `kTextArea`
|
|
160
160
|
- `kDialog`, `kDrawer`
|
|
161
161
|
- `kAppHeader`, `kAppFooter`, `kAppMain`, `kHero`
|
package/index.js
CHANGED
|
@@ -7,7 +7,6 @@ import kDialog from './src/ui/kDialog.vue'
|
|
|
7
7
|
import kDrawer from './src/ui/kDrawer.vue'
|
|
8
8
|
import kInput from './src/ui/kInput.vue'
|
|
9
9
|
import kDateSelector from './src/ui/kDateSelector.vue'
|
|
10
|
-
import kDateSelectorV2 from './src/ui/kDateSelector_v2.vue'
|
|
11
10
|
import kToolbar from './src/ui/kToolbar.vue'
|
|
12
11
|
import kSelect from './src/ui/kSelect.vue'
|
|
13
12
|
import kTable from './src/ui/kTable.vue'
|
|
@@ -51,7 +50,7 @@ export {
|
|
|
51
50
|
kMessage, kCode, kToolbar, kTable, kTabs, kChip, kSpinner, kDatatable, kIcon, kMenu, kSkeleton, kProgressBar, kTree,
|
|
52
51
|
|
|
53
52
|
// Form Components
|
|
54
|
-
kButton, kSelect, kUploader, kToggle, kInput, kDateSelector,
|
|
53
|
+
kButton, kSelect, kUploader, kToggle, kInput, kDateSelector, kEditor, kSelectButton, kTags, kSearch, kArrayList, kList, kTextArea,
|
|
55
54
|
|
|
56
55
|
// Dialogs
|
|
57
56
|
kDialog, kDrawer,
|
package/package.json
CHANGED
package/src/ui/kDateSelector.vue
CHANGED
|
@@ -1,326 +1,152 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
<label
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@click="openCalendar"
|
|
28
|
-
aria-label="Open calendar"
|
|
29
|
-
>
|
|
30
|
-
<Calendar class="w-5 h-5" />
|
|
31
|
-
</button>
|
|
32
|
-
</div>
|
|
33
|
-
|
|
34
|
-
<!-- Hidden input; only used to render the popup -->
|
|
35
|
-
<Datepicker ref="dp" v-model="calendarDate" :format="dateFormat" :enable-time-picker="false" auto-apply :disabled="disabled" :hide-input-icon="true" teleport="body" class="sr-only" />
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
<!-- Year-Month Mode -->
|
|
39
|
-
<div v-else class="relative">
|
|
40
|
-
<div class="relative">
|
|
41
|
-
<div ref="trigger" :class="[defaultStyle, hasError ? errorStyle : '', disabled ? disabledStyle : '']" @click="togglePopup">
|
|
42
|
-
{{ displayValue || placeholder }}
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<!-- Right-side calendar icon (opens month grid) -->
|
|
46
|
-
<button
|
|
47
|
-
type="button"
|
|
48
|
-
class="absolute p-1 text-primary/70 -translate-y-1/2 right-2 top-1/2 hover:text-primary disabled:opacity-50"
|
|
49
|
-
:disabled="disabled"
|
|
50
|
-
@click.stop="togglePopup"
|
|
51
|
-
aria-label="Open month selector"
|
|
52
|
-
>
|
|
53
|
-
<Calendar class="w-5 h-5" />
|
|
54
|
-
</button>
|
|
55
|
-
|
|
56
|
-
<!-- Clear Button (sits just left of the calendar icon) -->
|
|
57
|
-
<button
|
|
58
|
-
v-if="displayValue && !disabled"
|
|
59
|
-
@click.stop="clearSelection"
|
|
60
|
-
type="button"
|
|
61
|
-
class="absolute text-sm font-bold text-gray-400 -translate-y-1/2 right-9 top-1/2 hover:text-red-500"
|
|
62
|
-
aria-label="Clear"
|
|
63
|
-
>
|
|
64
|
-
<X class="w-5 h-5" />
|
|
65
|
-
</button>
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<!-- Month Grid Popover (Teleported) -->
|
|
69
|
-
<teleport to="body">
|
|
70
|
-
<div v-if="showPopup" class="p-4 bg-white dark:bg-slate-800 border border-primary/20 dark:border-slate-600 rounded-lg shadow-lg" :style="popupStyles">
|
|
71
|
-
<!-- Year Nav -->
|
|
72
|
-
<div class="flex items-center justify-between pb-3 mb-4 border-b border-slate-200 dark:border-slate-600">
|
|
73
|
-
<button @click.stop="currentYear--" class="text-primary/80 dark:text-sky-300 hover:text-primary dark:hover:text-sky-200" aria-label="Previous year">
|
|
74
|
-
<ChevronLeft />
|
|
75
|
-
</button>
|
|
76
|
-
<span class="font-medium text-primary dark:text-sky-300">{{ currentYear }}</span>
|
|
77
|
-
<button @click.stop="currentYear++" class="text-primary/80 dark:text-sky-300 hover:text-primary dark:hover:text-sky-200" aria-label="Next year">
|
|
78
|
-
<ChevronRight />
|
|
79
|
-
</button>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<!-- Month Grid -->
|
|
83
|
-
<div class="grid grid-cols-3 gap-2">
|
|
84
|
-
<div
|
|
85
|
-
v-for="(month, index) in months"
|
|
86
|
-
:key="index"
|
|
87
|
-
@click="selectMonth(index + 1)"
|
|
88
|
-
class="px-3 py-2 text-center transition rounded-lg cursor-pointer"
|
|
89
|
-
:class="[isSelected(index + 1) ? 'bg-primary text-white' : 'text-gray-700 dark:text-slate-200 hover:bg-primary/15 dark:hover:bg-sky-500/30 dark:hover:text-sky-100']"
|
|
90
|
-
>
|
|
91
|
-
{{ month }}
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</teleport>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
<!-- API-provided error -->
|
|
99
|
-
<div class="text-sm text-red-500" v-if="hasError && error !== true && error !== ''">
|
|
100
|
-
{{ error }}
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<!-- Local parse error -->
|
|
104
|
-
<div class="text-sm text-red-500" v-if="inputInvalid && !hasError">Μη έγκυρη ημερομηνία. Χρησιμοποιήστε μορφή dd/mm/yyyy.</div>
|
|
105
|
-
|
|
106
|
-
<!-- Info message -->
|
|
107
|
-
<div class="text-sm text-primary/80" v-if="info != null">
|
|
108
|
-
{{ info }}
|
|
109
|
-
</div>
|
|
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="computedId"
|
|
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"
|
|
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>
|
|
110
27
|
</div>
|
|
111
28
|
</template>
|
|
112
29
|
|
|
113
|
-
<script>
|
|
114
|
-
import
|
|
115
|
-
import
|
|
116
|
-
import moment from "moment";
|
|
117
|
-
import { ChevronLeft, ChevronRight, X, Calendar } from "lucide-vue-next";
|
|
118
|
-
|
|
119
|
-
export default {
|
|
120
|
-
name: "kDatePicker",
|
|
121
|
-
components: { Datepicker, ChevronLeft, ChevronRight, X, Calendar },
|
|
122
|
-
props: {
|
|
123
|
-
modelValue: [String, Date],
|
|
124
|
-
type: {
|
|
125
|
-
type: String,
|
|
126
|
-
default: "date",
|
|
127
|
-
validator: (val) => ["date", "yearMonth"].includes(val),
|
|
128
|
-
},
|
|
129
|
-
info: { type: String, default: null },
|
|
130
|
-
error: { type: [String, Boolean], default: null },
|
|
131
|
-
label: { type: String, default: "Ημερομηνία" },
|
|
132
|
-
placeholder: { type: String, default: "Επιλέξτε ημερομηνία" },
|
|
133
|
-
disabled: { type: Boolean, default: false },
|
|
134
|
-
valueFormat: { type: String, default: null },
|
|
135
|
-
id: {
|
|
136
|
-
type: String,
|
|
137
|
-
default: () => `datepicker-${Math.random().toString(36).substr(2, 9)}`,
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
data() {
|
|
141
|
-
const parsed = moment(this.modelValue, "YYYY-MM", true);
|
|
142
|
-
const current = moment();
|
|
143
|
-
|
|
144
|
-
const initText = (() => {
|
|
145
|
-
if (!this.modelValue) return "";
|
|
146
|
-
const m = moment(this.modelValue);
|
|
147
|
-
return m.isValid() ? m.format("DD/MM/YYYY") : "";
|
|
148
|
-
})();
|
|
30
|
+
<script setup>
|
|
31
|
+
import { computed } from 'vue'
|
|
32
|
+
import { ElDatePicker } from 'element-plus'
|
|
149
33
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Year-Month UI state
|
|
156
|
-
showPopup: false,
|
|
157
|
-
currentYear: parsed.isValid() ? parsed.year() : current.year(),
|
|
158
|
-
selectedMonth: parsed.isValid() ? parsed.month() + 1 : null,
|
|
159
|
-
months: ["Ιαν", "Φεβ", "Μάρ", "Απρ", "Μάι", "Ιούν", "Ιούλ", "Αυγ", "Σεπ", "Οκτ", "Νοέ", "Δεκ"],
|
|
160
|
-
popupStyles: {
|
|
161
|
-
position: "absolute",
|
|
162
|
-
top: "0px",
|
|
163
|
-
left: "0px",
|
|
164
|
-
width: "100%",
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
textDate: initText,
|
|
168
|
-
inputInvalid: false,
|
|
169
|
-
};
|
|
170
|
-
},
|
|
171
|
-
computed: {
|
|
172
|
-
hasError() {
|
|
173
|
-
return this.error != null && this.error !== false;
|
|
174
|
-
},
|
|
175
|
-
displayValue() {
|
|
176
|
-
if (!this.selectedMonth || !this.currentYear) return "";
|
|
177
|
-
return moment(`${this.currentYear}-${String(this.selectedMonth).padStart(2, "0")}`).format("MM/YYYY");
|
|
178
|
-
},
|
|
179
|
-
calendarDate: {
|
|
180
|
-
get() {
|
|
181
|
-
if (!this.modelValue) return null;
|
|
182
|
-
const m = moment(this.modelValue);
|
|
183
|
-
return m.isValid() ? m.toDate() : null;
|
|
184
|
-
},
|
|
185
|
-
set(val) {
|
|
186
|
-
const m = moment(val);
|
|
187
|
-
if (!m.isValid()) {
|
|
188
|
-
this.$emit("update:modelValue", null);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const emitValue = this.valueFormat ? m.format(this.valueFormat) : m.toDate();
|
|
192
|
-
this.$emit("update:modelValue", emitValue);
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
dateFormat() {
|
|
196
|
-
return "dd/MM/yyyy";
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
watch: {
|
|
200
|
-
modelValue(newVal) {
|
|
201
|
-
if (!newVal) {
|
|
202
|
-
this.textDate = "";
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const m = moment(newVal);
|
|
206
|
-
this.textDate = m.isValid() ? m.format("DD/MM/YYYY") : "";
|
|
207
|
-
},
|
|
34
|
+
const props = defineProps({
|
|
35
|
+
modelValue: { type: [String, Date, Array], default: null },
|
|
36
|
+
id: {
|
|
37
|
+
type: [String, Array],
|
|
38
|
+
default: () => `datepicker-${Math.random().toString(36).slice(2, 11)}`,
|
|
208
39
|
},
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
};
|
|
215
|
-
document.addEventListener("click", this.clickOutsideHandler);
|
|
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),
|
|
216
45
|
},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (this.disabled) return;
|
|
230
|
-
|
|
231
|
-
let value = e.target.value || "";
|
|
232
|
-
|
|
233
|
-
// Allow digits and slashes only
|
|
234
|
-
value = value.replace(/[^0-9/]/g, "");
|
|
235
|
-
|
|
236
|
-
// Auto-add slashes if user omits them
|
|
237
|
-
const digits = value.replace(/\D/g, "");
|
|
238
|
-
if (digits.length > 2 && !value.includes("/")) {
|
|
239
|
-
// add first slash after 2 digits
|
|
240
|
-
value = `${digits.slice(0, 2)}/${digits.slice(2)}`;
|
|
241
|
-
}
|
|
242
|
-
if (digits.length > 4 && (value.match(/\//g) || []).length < 2) {
|
|
243
|
-
// ensure second slash exists after month
|
|
244
|
-
value = `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4)}`;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Prevent double slashes like "12//03"
|
|
248
|
-
value = value.replace(/\/{2,}/g, "/");
|
|
249
|
-
|
|
250
|
-
this.textDate = value;
|
|
251
|
-
|
|
252
|
-
// When 8 digits exist, try to parse automatically
|
|
253
|
-
if (digits.length === 8) {
|
|
254
|
-
const m = moment(value, "DD/MM/YYYY", true);
|
|
255
|
-
if (m.isValid()) {
|
|
256
|
-
this.inputInvalid = false;
|
|
257
|
-
const emitValue = this.valueFormat ? m.format(this.valueFormat) : m.toDate();
|
|
258
|
-
this.$emit("update:modelValue", emitValue);
|
|
259
|
-
} else {
|
|
260
|
-
this.inputInvalid = true;
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
this.inputInvalid = false;
|
|
264
|
-
}
|
|
265
|
-
},
|
|
266
|
-
|
|
267
|
-
commitTextDate() {
|
|
268
|
-
const raw = (this.textDate || "").trim();
|
|
269
|
-
if (!raw) {
|
|
270
|
-
this.inputInvalid = false;
|
|
271
|
-
this.$emit("update:modelValue", null);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
const formats = ["DD/MM/YYYY", "D/M/YYYY", "YYYY-MM-DD", moment.ISO_8601];
|
|
275
|
-
const m = moment(raw, formats, true);
|
|
276
|
-
if (!m.isValid()) {
|
|
277
|
-
this.inputInvalid = true;
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
this.inputInvalid = false;
|
|
281
|
-
const emitValue = this.valueFormat ? m.format(this.valueFormat) : m.toDate();
|
|
282
|
-
this.$emit("update:modelValue", emitValue);
|
|
283
|
-
},
|
|
284
|
-
|
|
285
|
-
openCalendar() {
|
|
286
|
-
if (this.disabled) return;
|
|
287
|
-
this.$refs.dp && this.$refs.dp.openMenu && this.$refs.dp.openMenu();
|
|
288
|
-
},
|
|
289
|
-
togglePopup() {
|
|
290
|
-
if (this.disabled) return;
|
|
291
|
-
this.showPopup = !this.showPopup;
|
|
292
|
-
if (this.showPopup) {
|
|
293
|
-
this.$nextTick(() => {
|
|
294
|
-
const trigger = this.$refs.trigger;
|
|
295
|
-
if (trigger) {
|
|
296
|
-
const rect = trigger.getBoundingClientRect();
|
|
297
|
-
this.popupStyles = {
|
|
298
|
-
position: "absolute",
|
|
299
|
-
top: `${rect.bottom + window.scrollY}px`,
|
|
300
|
-
left: `${rect.left + window.scrollX}px`,
|
|
301
|
-
width: `${rect.width}px`,
|
|
302
|
-
zIndex: 9999,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
clearSelection() {
|
|
309
|
-
this.selectedMonth = null;
|
|
310
|
-
this.$emit("update:modelValue", null);
|
|
311
|
-
this.showPopup = false;
|
|
312
|
-
},
|
|
313
|
-
selectMonth(month) {
|
|
314
|
-
this.selectedMonth = month;
|
|
315
|
-
const m = moment(`${this.currentYear}-${String(month).padStart(2, "0")}`, "YYYY-MM");
|
|
316
|
-
const emitValue = this.valueFormat ? m.format(this.valueFormat) : m.toDate();
|
|
317
|
-
this.$emit("update:modelValue", emitValue);
|
|
318
|
-
this.showPopup = false;
|
|
319
|
-
},
|
|
320
|
-
isSelected(month) {
|
|
321
|
-
const m = moment(this.modelValue, "YYYY-MM", true);
|
|
322
|
-
return this.selectedMonth === month && m.isValid() && m.year() === this.currentYear;
|
|
323
|
-
},
|
|
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),
|
|
324
58
|
},
|
|
325
|
-
}
|
|
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 isRange = computed(() => ['daterange', 'datetimerange', 'monthrange'].includes(pickerType.value))
|
|
70
|
+
const computedId = computed(() => {
|
|
71
|
+
if (Array.isArray(props.id)) return props.id
|
|
72
|
+
return isRange.value ? [`${props.id}-start`, `${props.id}-end`] : props.id
|
|
73
|
+
})
|
|
74
|
+
const hasError = computed(() => props.error != null && props.error !== false)
|
|
75
|
+
|
|
76
|
+
const defaultFormatByType = {
|
|
77
|
+
date: 'YYYY-MM-DD',
|
|
78
|
+
dates: 'YYYY-MM-DD',
|
|
79
|
+
week: 'gggg[w]ww',
|
|
80
|
+
year: 'YYYY',
|
|
81
|
+
month: 'YYYY-MM',
|
|
82
|
+
datetime: 'YYYY-MM-DD HH:mm:ss',
|
|
83
|
+
monthrange: 'YYYY-MM',
|
|
84
|
+
daterange: 'YYYY-MM-DD',
|
|
85
|
+
datetimerange: 'YYYY-MM-DD HH:mm:ss',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const resolvedFormat = computed(() => {
|
|
89
|
+
const custom = String(props.format || '').trim()
|
|
90
|
+
if (custom) return custom
|
|
91
|
+
return defaultFormatByType[pickerType.value] || 'YYYY-MM-DD'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const resolvedValueFormat = computed(() => {
|
|
95
|
+
const custom = String(props.valueFormat || '').trim()
|
|
96
|
+
if (custom) return custom
|
|
97
|
+
return defaultFormatByType[pickerType.value] || 'YYYY-MM-DD'
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
function normalizeIsoString(value, type) {
|
|
101
|
+
if (typeof value !== 'string' || !value.includes('T')) return value
|
|
102
|
+
|
|
103
|
+
if (type === 'year') return value.slice(0, 4)
|
|
104
|
+
if (type === 'month' || type === 'monthrange') return value.slice(0, 7)
|
|
105
|
+
|
|
106
|
+
if (type === 'datetime' || type === 'datetimerange') {
|
|
107
|
+
const normalized = value.replace('T', ' ').replace(/Z$/, '')
|
|
108
|
+
const fullMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})\s(\d{2}:\d{2}:\d{2})/)
|
|
109
|
+
if (fullMatch) return `${fullMatch[1]} ${fullMatch[2]}`
|
|
110
|
+
const shortMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})\s(\d{2}:\d{2})/)
|
|
111
|
+
if (shortMatch) return `${shortMatch[1]} ${shortMatch[2]}:00`
|
|
112
|
+
return normalized
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const dateMatch = value.match(/^(\d{4}-\d{2}-\d{2})/)
|
|
116
|
+
return dateMatch ? dateMatch[1] : value
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const normalizedModelValue = computed(() => {
|
|
120
|
+
if (Array.isArray(props.modelValue)) {
|
|
121
|
+
return props.modelValue.map((item) => normalizeIsoString(item, pickerType.value))
|
|
122
|
+
}
|
|
123
|
+
return normalizeIsoString(props.modelValue, pickerType.value)
|
|
124
|
+
})
|
|
326
125
|
</script>
|
|
126
|
+
|
|
127
|
+
<style>
|
|
128
|
+
/* Global because Element Plus picker panel is teleported to body. */
|
|
129
|
+
html.dark .el-date-picker,
|
|
130
|
+
html.dark .el-date-range-picker {
|
|
131
|
+
--el-datepicker-inrange-bg-color: rgba(3, 105, 161, 0.22);
|
|
132
|
+
--el-datepicker-inrange-hover-bg-color: rgba(3, 105, 161, 0.32);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
html.dark .el-date-table td.in-range .el-date-table-cell,
|
|
136
|
+
html.dark .el-month-table td.in-range .el-date-table-cell,
|
|
137
|
+
html.dark .el-year-table td.in-range .el-date-table-cell {
|
|
138
|
+
background-color: rgba(3, 105, 161, 0.22);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
html.dark .el-date-table td.in-range .el-date-table-cell:hover,
|
|
142
|
+
html.dark .el-month-table td.in-range .el-date-table-cell:hover,
|
|
143
|
+
html.dark .el-year-table td.in-range .el-date-table-cell:hover {
|
|
144
|
+
background-color: rgba(3, 105, 161, 0.32);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
html.dark .el-month-table td.available:hover .el-date-table-cell,
|
|
148
|
+
html.dark .el-year-table td.available:hover .el-date-table-cell {
|
|
149
|
+
background-color: rgba(148, 163, 184, 0.16);
|
|
150
|
+
border-radius: 24px;
|
|
151
|
+
}
|
|
152
|
+
</style>
|
package/src/ui/kMenu.vue
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
: 'top-[-7px] border-l border-t border-primary/20',
|
|
37
37
|
]"
|
|
38
38
|
:style="{ left: `${chevronOffset}px` }"></span>
|
|
39
|
-
<slot name="items">
|
|
39
|
+
<slot name="items" :close-menu="closeMenu" :close="closeMenu" :select="handleLinkClick">
|
|
40
40
|
<ul class="p-2">
|
|
41
41
|
<li v-for="(link, index) in normalizedLinks" :key="link.key || `${link.label}-${index}`">
|
|
42
42
|
<component
|
|
@@ -131,10 +131,20 @@ export default {
|
|
|
131
131
|
event.preventDefault();
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
|
|
135
|
+
const runSelectionHandlers = () => {
|
|
136
|
+
if (typeof link?.action === "function") link.action(link, event);
|
|
137
|
+
if (typeof link?.onClick === "function") link.onClick(link, event);
|
|
138
|
+
this.$emit("select", link);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (this.closeOnSelect) {
|
|
142
|
+
this.closeMenu();
|
|
143
|
+
window.setTimeout(runSelectionHandlers, 0);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
runSelectionHandlers();
|
|
138
148
|
},
|
|
139
149
|
handleClickOutside(event) {
|
|
140
150
|
const clickedTrigger = this.$refs.menuRef?.contains(event.target);
|
package/src/ui/kSelect.vue
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
:class="[defaultStyle, hasError ? errorStyle : '', disabled ? disabledStyle : '']"
|
|
13
13
|
:aria-label="label || 'Select option'"
|
|
14
14
|
>
|
|
15
|
-
<SelectValue
|
|
15
|
+
<SelectValue
|
|
16
|
+
:placeholder="placeholder"
|
|
17
|
+
class="block pr-10 truncate text-gray-700 data-[placeholder]:text-gray-400 dark:text-slate-100 dark:data-[placeholder]:text-slate-500"
|
|
18
|
+
/>
|
|
16
19
|
|
|
17
20
|
<button
|
|
18
21
|
v-if="clearable && isSelectionSet && !disabled"
|
package/src/ui/kButton_v2.vue
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
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>
|