ketekny-ui-kit 1.0.83 → 1.0.85
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/index.js +5 -3
- package/package.json +1 -1
- package/src/layout/kSingleColPage.vue +88 -0
- package/src/layout/kTwoColPage.vue +103 -0
- package/src/layout/twoColLayout.vue +36 -10
- package/src/layout/twoColSidebarMenuList.vue +112 -0
- package/src/layout/twoColSidebarPanel.vue +308 -102
- package/src/ui/kForm.vue +88 -0
- package/src/ui/kSearch.vue +7 -2
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import kSelect from './src/ui/kSelect.vue'
|
|
|
13
13
|
import kTable from './src/ui/kTable.vue'
|
|
14
14
|
import kTabs from './src/ui/kTabs.vue'
|
|
15
15
|
import kToggle from './src/ui/kToggle.vue'
|
|
16
|
+
import kForm from './src/ui/kForm.vue'
|
|
16
17
|
import kUploader from './src/ui/kUploader.vue'
|
|
17
18
|
import kEditor from './src/ui/kEditor.vue'
|
|
18
19
|
import kSpinner from './src/ui/kSpinner.vue'
|
|
@@ -35,6 +36,8 @@ import kAppHeader from './src/layout/kAppHeader.vue'
|
|
|
35
36
|
import kAppFooter from './src/layout/kAppFooter.vue'
|
|
36
37
|
import kAppMain from './src/layout/kAppMain.vue'
|
|
37
38
|
import kHero from './src/layout/kHero.vue'
|
|
39
|
+
import kSingleColPage from './src/layout/kSingleColPage.vue'
|
|
40
|
+
import kTwoColPage from './src/layout/kTwoColPage.vue'
|
|
38
41
|
import twoColLayout from './src/layout/twoColLayout.vue'
|
|
39
42
|
|
|
40
43
|
// Toast/Confirm/Alert Plugins
|
|
@@ -53,13 +56,13 @@ export {
|
|
|
53
56
|
kMessage, kCode, kToolbar, kTable, kTabs, kChip, kSpinner, kDatatable, kIcon, kMenu, kSkeleton, kProgressBar, kPagination, kTree,
|
|
54
57
|
|
|
55
58
|
// Form Components
|
|
56
|
-
kButton, kCheckbox, kSelect, kUploader, kToggle, kInput, kDateSelector, kEditor, kSelectButton, kTags, kSearch, kArrayList, kList, kTextArea,
|
|
59
|
+
kButton, kCheckbox, kSelect, kUploader, kToggle, kInput, kDateSelector, kEditor, kSelectButton, kTags, kSearch, kArrayList, kList, kTextArea, kForm,
|
|
57
60
|
|
|
58
61
|
// Dialogs
|
|
59
62
|
kDialog, kDrawer,
|
|
60
63
|
|
|
61
64
|
// Layout Components
|
|
62
|
-
kAppHeader, kAppFooter, kAppMain, kHero, twoColLayout,
|
|
65
|
+
kAppHeader, kAppFooter, kAppMain, kHero, kSingleColPage, kTwoColPage, twoColLayout,
|
|
63
66
|
|
|
64
67
|
// Plugins
|
|
65
68
|
toastPlugin, confirmPlugin, alertPlugin, tooltipPlugin, inputDialogPlugin,
|
|
@@ -67,4 +70,3 @@ export {
|
|
|
67
70
|
// Tailwind Config
|
|
68
71
|
tailwindPreset
|
|
69
72
|
}
|
|
70
|
-
|
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<main class="flex min-h-screen w-full max-w-none flex-col bg-slate-100">
|
|
3
|
+
<section class="bg-white px-4 pb-4 pb-0 pt-8 sm:px-6 lg:px-8">
|
|
4
|
+
<div class="mx-auto flex w-full max-w-none flex-col gap-5">
|
|
5
|
+
<div
|
|
6
|
+
v-if="pageTitle || pageDescription"
|
|
7
|
+
class="flex flex-wrap items-start justify-between gap-4"
|
|
8
|
+
>
|
|
9
|
+
<div class="min-w-0">
|
|
10
|
+
<p class="text-[10px] font-semibold uppercase tracking-[0.28em] text-slate-500">
|
|
11
|
+
{{ appTitle }}
|
|
12
|
+
</p>
|
|
13
|
+
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl">
|
|
14
|
+
{{ pageTitle }}
|
|
15
|
+
</h1>
|
|
16
|
+
<p v-if="pageDescription" class="mt-1 text-sm font-medium text-slate-500">
|
|
17
|
+
{{ pageDescription }}
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
21
|
+
<slot name="header-actions" />
|
|
22
|
+
<kButton
|
|
23
|
+
v-if="externalUrl"
|
|
24
|
+
:label="externalUrlLabel"
|
|
25
|
+
icon="ExternalLink"
|
|
26
|
+
variant="soft"
|
|
27
|
+
@click="openExternalUrl"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</section>
|
|
33
|
+
|
|
34
|
+
<div class="sticky top-0 z-30 border-b border-t border-slate-200 bg-slate-50 px-4 backdrop-blur sm:px-6 lg:px-8">
|
|
35
|
+
<div class="mx-auto flex w-full max-w-none items-center">
|
|
36
|
+
<slot name="header-extra" />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="flex-1 bg-slate-100 px-4 pb-8 pt-4 sm:px-6 lg:px-8">
|
|
41
|
+
<div class="mx-auto w-full max-w-none">
|
|
42
|
+
<slot />
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</main>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script>
|
|
49
|
+
import packageJson from '../../package.json'
|
|
50
|
+
import kButton from '../ui/kButton.vue'
|
|
51
|
+
|
|
52
|
+
export default {
|
|
53
|
+
name: 'PageShell',
|
|
54
|
+
components: { kButton },
|
|
55
|
+
props: {
|
|
56
|
+
appTitle: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: packageJson.description || packageJson.name || 'App',
|
|
59
|
+
},
|
|
60
|
+
pageTitle: {
|
|
61
|
+
type: String,
|
|
62
|
+
default: '',
|
|
63
|
+
},
|
|
64
|
+
pageDescription: {
|
|
65
|
+
type: String,
|
|
66
|
+
default: '',
|
|
67
|
+
},
|
|
68
|
+
externalUrl: {
|
|
69
|
+
type: String,
|
|
70
|
+
default: '',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
computed: {
|
|
74
|
+
externalUrlLabel() {
|
|
75
|
+
const target = String(this.externalUrl || '').trim()
|
|
76
|
+
if (!target) return ''
|
|
77
|
+
return target.replace(/^https?:\/\//i, '').replace(/\/$/, '')
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
methods: {
|
|
81
|
+
openExternalUrl() {
|
|
82
|
+
const target = String(this.externalUrl || '').trim()
|
|
83
|
+
if (!target) return
|
|
84
|
+
window.open(target, '_blank', 'noopener,noreferrer')
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<main class="flex h-screen w-full max-w-none flex-col overflow-hidden bg-slate-100">
|
|
3
|
+
<section class="shrink-0 bg-white px-4 pb-4 pb-0 pt-8 sm:px-6 lg:px-8">
|
|
4
|
+
<div class="mx-auto flex w-full max-w-none flex-col gap-5">
|
|
5
|
+
<div
|
|
6
|
+
v-if="pageTitle || pageDescription"
|
|
7
|
+
class="flex flex-wrap items-start justify-between gap-4"
|
|
8
|
+
>
|
|
9
|
+
<div class="min-w-0">
|
|
10
|
+
<p class="text-[10px] font-semibold uppercase tracking-[0.28em] text-slate-500">
|
|
11
|
+
{{ appTitle }}
|
|
12
|
+
</p>
|
|
13
|
+
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl">
|
|
14
|
+
{{ pageTitle }}
|
|
15
|
+
</h1>
|
|
16
|
+
<p v-if="pageDescription" class="mt-1 text-sm font-medium text-slate-500">
|
|
17
|
+
{{ pageDescription }}
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
21
|
+
<slot name="header-actions" />
|
|
22
|
+
<kButton
|
|
23
|
+
v-if="externalUrl"
|
|
24
|
+
:label="externalUrlLabel"
|
|
25
|
+
icon="ExternalLink"
|
|
26
|
+
variant="soft"
|
|
27
|
+
@click="openExternalUrl"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</section>
|
|
33
|
+
|
|
34
|
+
<div class="shrink-0 border-b border-t border-slate-200 bg-slate-50 px-4 backdrop-blur">
|
|
35
|
+
<div class="mx-auto flex w-full max-w-none items-center">
|
|
36
|
+
<slot name="header-extra" />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="min-h-0 flex-1 overflow-hidden bg-slate-100 py-4">
|
|
41
|
+
<div class="mx-auto flex h-full min-h-0 w-full max-w-none gap-2">
|
|
42
|
+
<aside class="ml-8 min-h-0 shrink-0 overflow-y-auto pr-4" :style="sidebarStyle">
|
|
43
|
+
<slot name="sidebar" />
|
|
44
|
+
</aside>
|
|
45
|
+
<section class="min-h-0 min-w-0 flex-1 overflow-y-auto px-2">
|
|
46
|
+
<slot name="content" />
|
|
47
|
+
</section>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</main>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script>
|
|
54
|
+
import packageJson from '../../package.json'
|
|
55
|
+
import kButton from '../ui/kButton.vue'
|
|
56
|
+
|
|
57
|
+
export default {
|
|
58
|
+
name: 'PageTwoColShell',
|
|
59
|
+
components: { kButton },
|
|
60
|
+
props: {
|
|
61
|
+
appTitle: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: packageJson.description || packageJson.name || 'App',
|
|
64
|
+
},
|
|
65
|
+
pageTitle: {
|
|
66
|
+
type: String,
|
|
67
|
+
default: '',
|
|
68
|
+
},
|
|
69
|
+
pageDescription: {
|
|
70
|
+
type: String,
|
|
71
|
+
default: '',
|
|
72
|
+
},
|
|
73
|
+
externalUrl: {
|
|
74
|
+
type: String,
|
|
75
|
+
default: '',
|
|
76
|
+
},
|
|
77
|
+
sidebarWidth: {
|
|
78
|
+
type: String,
|
|
79
|
+
default: '300px',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
computed: {
|
|
83
|
+
externalUrlLabel() {
|
|
84
|
+
const target = String(this.externalUrl || '').trim()
|
|
85
|
+
if (!target) return ''
|
|
86
|
+
return target.replace(/^https?:\/\//i, '').replace(/\/$/, '')
|
|
87
|
+
},
|
|
88
|
+
sidebarStyle() {
|
|
89
|
+
return {
|
|
90
|
+
minWidth: this.sidebarWidth,
|
|
91
|
+
maxWidth: this.sidebarWidth,
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
methods: {
|
|
96
|
+
openExternalUrl() {
|
|
97
|
+
const target = String(this.externalUrl || '').trim()
|
|
98
|
+
if (!target) return
|
|
99
|
+
window.open(target, '_blank', 'noopener,noreferrer')
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="relative flex min-h-screen flex-col bg-[#edf4fb] lg:h-screen lg:flex-row lg:overflow-
|
|
3
|
+
class="relative flex min-h-screen flex-col bg-[#edf4fb] lg:h-screen lg:flex-row lg:overflow-visible"
|
|
4
4
|
>
|
|
5
5
|
<button
|
|
6
6
|
v-show="isDesktopViewport && isSidebarCollapsed"
|
|
7
7
|
type="button"
|
|
8
8
|
:aria-label="expandLabel"
|
|
9
9
|
v-tooltip.right="expandLabel"
|
|
10
|
-
class="fixed top-0 z-[70] hidden h-10 w-10 items-center justify-center rounded-r-full border-white/15 bg-[#
|
|
10
|
+
class="fixed top-0 z-[70] hidden h-10 w-10 items-center justify-center rounded-r-full border-white/15 bg-[#0b0c0f] text-white shadow-2xl shadow-black/50 transition-all duration-300 hover:bg-[#14161b] lg:left-[42px] lg:translate-x-full lg:flex"
|
|
11
11
|
@click="toggleSidebar"
|
|
12
12
|
>
|
|
13
13
|
<slot name="collapsed-toggle-icon">
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
type="button"
|
|
21
21
|
:aria-label="collapseLabel"
|
|
22
22
|
v-tooltip.left="collapseLabel"
|
|
23
|
-
class="fixed top-0 z-[70] hidden h-10 w-10 items-center justify-center rounded-r-full border-white/15 bg-[#
|
|
23
|
+
class="fixed top-0 z-[70] hidden h-10 w-10 items-center justify-center rounded-r-full border-white/15 bg-[#0b0c0f] text-white shadow-2xl shadow-black/50 transition-all duration-300 hover:bg-[#14161b] lg:left-[326px] lg:translate-x-full lg:flex"
|
|
24
24
|
@click.stop="toggleSidebar"
|
|
25
25
|
>
|
|
26
26
|
<slot name="toggle-icon">
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
:contact-email="contactEmail"
|
|
46
46
|
:help-url="helpUrl"
|
|
47
47
|
:collapsed="isSidebarCollapsed"
|
|
48
|
+
:show-menu-search="showMenuSearch"
|
|
49
|
+
:menu-search-placeholder="menuSearchPlaceholder"
|
|
48
50
|
@logo-click="handleLogoClick"
|
|
49
51
|
@signin="handleSignin"
|
|
50
52
|
@signup="handleSignup"
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
<button
|
|
61
63
|
v-if="!isDesktopViewport && !mobileMenuOpen"
|
|
62
64
|
type="button"
|
|
63
|
-
class="fixed bottom-4 left-4 z-[80] flex h-14 w-14 items-center justify-center rounded-full bg-[#
|
|
65
|
+
class="fixed bottom-4 left-4 z-[80] flex h-14 w-14 items-center justify-center rounded-full bg-[#0b0c0f] text-white shadow-2xl shadow-black/40 transition-transform duration-200 hover:scale-105 hover:bg-[#14161b] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
|
64
66
|
aria-label="Open menu"
|
|
65
67
|
@click="mobileMenuOpen = true"
|
|
66
68
|
>
|
|
@@ -98,6 +100,8 @@
|
|
|
98
100
|
:contact-email="contactEmail"
|
|
99
101
|
:help-url="helpUrl"
|
|
100
102
|
:collapsed="false"
|
|
103
|
+
:show-menu-search="showMenuSearch"
|
|
104
|
+
:menu-search-placeholder="menuSearchPlaceholder"
|
|
101
105
|
@logo-click="handleLogoClick"
|
|
102
106
|
@item-click="closeMobileMenu"
|
|
103
107
|
@signin="handleSignin"
|
|
@@ -158,6 +162,11 @@ export default {
|
|
|
158
162
|
account: { type: Object, default: null },
|
|
159
163
|
contactEmail: { type: String, default: "grammateia@ketekny.gr" },
|
|
160
164
|
helpUrl: { type: String, default: "" },
|
|
165
|
+
showMenuSearch: { type: Boolean, default: false },
|
|
166
|
+
menuSearchPlaceholder: {
|
|
167
|
+
type: String,
|
|
168
|
+
default: "Αναζήτηση...",
|
|
169
|
+
},
|
|
161
170
|
},
|
|
162
171
|
data() {
|
|
163
172
|
return {
|
|
@@ -281,17 +290,34 @@ export default {
|
|
|
281
290
|
},
|
|
282
291
|
computed: {
|
|
283
292
|
normalizedMainMenu() {
|
|
284
|
-
|
|
293
|
+
const normalizeItem = (item, index, parentKey = "menu") => {
|
|
285
294
|
const isExternal = Boolean(item?.href);
|
|
286
295
|
const isLink = Boolean(item?.to);
|
|
296
|
+
const children = Array.isArray(item?.children)
|
|
297
|
+
? item.children.map((child, childIndex) =>
|
|
298
|
+
normalizeItem(
|
|
299
|
+
child,
|
|
300
|
+
childIndex,
|
|
301
|
+
item?.key || item?.label || `${parentKey}-${index}`
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
: [];
|
|
305
|
+
const hasActiveChild = children.some(
|
|
306
|
+
child => child.active || child.hasActiveChild
|
|
307
|
+
);
|
|
287
308
|
const active = Boolean(item?.active);
|
|
309
|
+
const key = item?.key || item?.label || `${parentKey}-${index}`;
|
|
288
310
|
|
|
289
311
|
return {
|
|
290
|
-
key
|
|
312
|
+
key,
|
|
291
313
|
label: item?.label || "",
|
|
292
314
|
description: item?.description || "",
|
|
293
315
|
icon: item?.icon || "Dot",
|
|
294
316
|
active,
|
|
317
|
+
hasActiveChild,
|
|
318
|
+
hasChildren: children.length > 0,
|
|
319
|
+
children,
|
|
320
|
+
defaultExpanded: Boolean(item?.defaultExpanded),
|
|
295
321
|
isExternal,
|
|
296
322
|
isLink,
|
|
297
323
|
bindings: isLink
|
|
@@ -311,11 +337,11 @@ export default {
|
|
|
311
337
|
type: "button",
|
|
312
338
|
title: item.title || item.label,
|
|
313
339
|
},
|
|
314
|
-
classes:
|
|
315
|
-
? "border-white/70 bg-white/90 text-slate-950 shadow-lg shadow-black/20"
|
|
316
|
-
: "border-white/10 bg-white/5 text-white hover:border-cyan-400/40 hover:bg-white/10",
|
|
340
|
+
classes: "",
|
|
317
341
|
};
|
|
318
|
-
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
return this.mainMenu.map((item, index) => normalizeItem(item, index));
|
|
319
345
|
},
|
|
320
346
|
},
|
|
321
347
|
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-1.5">
|
|
3
|
+
<div v-for="item in items" :key="item.key" class="space-y-1.5">
|
|
4
|
+
<div
|
|
5
|
+
class="flex items-stretch"
|
|
6
|
+
:class="item.hasChildren ? 'gap-2' : 'gap-0'"
|
|
7
|
+
>
|
|
8
|
+
<component
|
|
9
|
+
:is="menuItemComponent(item)"
|
|
10
|
+
v-bind="item.bindings"
|
|
11
|
+
class="flex min-w-0 flex-1 appearance-none items-center gap-3 border-0 bg-transparent px-2 py-1.5 text-left outline-none transition-all duration-150"
|
|
12
|
+
:class="itemRowClasses(item)"
|
|
13
|
+
@click="handleItemClick(item, $event)"
|
|
14
|
+
>
|
|
15
|
+
<span
|
|
16
|
+
class="flex h-5 w-5 shrink-0 items-center justify-center text-[#eef2f7] transition-colors duration-150"
|
|
17
|
+
>
|
|
18
|
+
<kIcon :name="item.icon" size="18" color="#eef2f7" />
|
|
19
|
+
</span>
|
|
20
|
+
<span class="min-w-0 flex-1">
|
|
21
|
+
<span class="block truncate text-[15px] font-medium leading-5">
|
|
22
|
+
{{ item.label }}
|
|
23
|
+
</span>
|
|
24
|
+
<span
|
|
25
|
+
v-if="item.description"
|
|
26
|
+
class="mt-1 block truncate text-[12px] leading-4"
|
|
27
|
+
:class="item.active ? 'text-[#9fa6b5]' : 'text-[#6f7786]'"
|
|
28
|
+
>
|
|
29
|
+
{{ item.description }}
|
|
30
|
+
</span>
|
|
31
|
+
</span>
|
|
32
|
+
</component>
|
|
33
|
+
|
|
34
|
+
<button
|
|
35
|
+
v-if="item.hasChildren"
|
|
36
|
+
type="button"
|
|
37
|
+
class="flex w-8 shrink-0 appearance-none items-center justify-center border-0 bg-transparent text-[#eef2f7] outline-none transition hover:bg-white/6 hover:text-white"
|
|
38
|
+
:aria-expanded="isExpanded(item)"
|
|
39
|
+
:aria-label="`${isExpanded(item) ? 'Collapse' : 'Expand'} ${item.label}`"
|
|
40
|
+
@click.stop="toggle(item.key)"
|
|
41
|
+
>
|
|
42
|
+
<kIcon
|
|
43
|
+
name="ChevronDown"
|
|
44
|
+
size="15"
|
|
45
|
+
color="#eef2f7"
|
|
46
|
+
class="transition-transform duration-200"
|
|
47
|
+
:class="isExpanded(item) ? 'rotate-180' : ''"
|
|
48
|
+
/>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
v-if="item.hasChildren && isExpanded(item)"
|
|
54
|
+
class="ml-2 border-l border-[#242834] pl-4"
|
|
55
|
+
>
|
|
56
|
+
<TwoColSidebarMenuList
|
|
57
|
+
:items="item.children"
|
|
58
|
+
:expanded-state="expandedState"
|
|
59
|
+
@item-click="$emit('item-click', $event)"
|
|
60
|
+
@toggle-branch="$emit('toggle-branch', $event)"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script>
|
|
68
|
+
import { RouterLink } from "vue-router";
|
|
69
|
+
import kIcon from "../ui/kIcon.vue";
|
|
70
|
+
|
|
71
|
+
export default {
|
|
72
|
+
name: "TwoColSidebarMenuList",
|
|
73
|
+
components: {
|
|
74
|
+
RouterLink,
|
|
75
|
+
kIcon,
|
|
76
|
+
},
|
|
77
|
+
props: {
|
|
78
|
+
items: { type: Array, default: () => [] },
|
|
79
|
+
expandedState: { type: Object, required: true },
|
|
80
|
+
},
|
|
81
|
+
emits: ["item-click", "toggle-branch"],
|
|
82
|
+
methods: {
|
|
83
|
+
menuItemComponent(item) {
|
|
84
|
+
if (item?.isLink) return RouterLink;
|
|
85
|
+
if (item?.isExternal) return "a";
|
|
86
|
+
return "button";
|
|
87
|
+
},
|
|
88
|
+
handleItemClick(item, event) {
|
|
89
|
+
event?.stopPropagation?.();
|
|
90
|
+
|
|
91
|
+
if (item?.hasChildren) {
|
|
92
|
+
event?.preventDefault?.();
|
|
93
|
+
this.toggle(item.key);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.$emit("item-click", item);
|
|
98
|
+
},
|
|
99
|
+
isExpanded(item) {
|
|
100
|
+
return Boolean(this.expandedState[item.key]);
|
|
101
|
+
},
|
|
102
|
+
itemRowClasses(item) {
|
|
103
|
+
return item.active
|
|
104
|
+
? "relative rounded-xl bg-[#242424] text-white before:absolute before:bottom-2 before:left-0 before:top-2 before:w-[3px] before:rounded-r-full before:bg-[#2491ff]"
|
|
105
|
+
: "rounded-xl text-[#e5e7eb] hover:bg-[#242424] hover:text-white";
|
|
106
|
+
},
|
|
107
|
+
toggle(key) {
|
|
108
|
+
this.$emit("toggle-branch", key);
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
</script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="flex h-full w-full flex-col overflow-
|
|
3
|
+
class="flex h-full w-full flex-col overflow-visible border-r border-[#171a21] bg-[#0b0c0f] text-white shadow-[20px_0_50px_rgba(0,0,0,0.35)] transition-all duration-300 ease-out lg:relative lg:z-30 lg:will-change-transform"
|
|
4
4
|
>
|
|
5
5
|
<div class="relative">
|
|
6
6
|
<button
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
class="px-4 py-4 transition-all duration-300 cursor-pointer"
|
|
17
17
|
/>
|
|
18
18
|
</button>
|
|
19
|
+
|
|
19
20
|
<div
|
|
20
21
|
v-if="title && !collapsed"
|
|
21
22
|
class="py-3 text-xl font-bold text-center"
|
|
@@ -23,81 +24,94 @@
|
|
|
23
24
|
{{ title }}
|
|
24
25
|
</div>
|
|
25
26
|
|
|
26
|
-
<div v-if="helpUrl && !collapsed" class="px-4 pb-2">
|
|
27
|
-
<a
|
|
28
|
-
:href="helpUrl"
|
|
29
|
-
target="_blank"
|
|
30
|
-
rel="noreferrer"
|
|
31
|
-
class="inline-flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-semibold text-white/85 transition hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
|
32
|
-
>
|
|
33
|
-
<kIcon name="HelpCircle" size="16" color="#ffffff" />
|
|
34
|
-
<span>Χρειάζεστε βοήθεια;</span>
|
|
35
|
-
</a>
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
27
|
<slot name="sidebar-header" />
|
|
39
28
|
</div>
|
|
40
29
|
|
|
41
|
-
<div class="flex-1 pt-
|
|
42
|
-
<div v-if="collapsed" class="
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
class="
|
|
49
|
-
|
|
50
|
-
v-tooltip.right="item.label"
|
|
51
|
-
@click="$emit('item-click', item)"
|
|
52
|
-
>
|
|
53
|
-
<kIcon
|
|
54
|
-
:name="item.icon"
|
|
55
|
-
size="20"
|
|
56
|
-
:color="item.active ? '#0f172a' : '#ffffff'"
|
|
57
|
-
/>
|
|
58
|
-
</component>
|
|
30
|
+
<div class="min-h-0 flex-1 overflow-y-auto px-3 pb-3 pt-2">
|
|
31
|
+
<div v-if="showMenuSearch && !collapsed" class="mb-3 px-1">
|
|
32
|
+
<kSearch
|
|
33
|
+
v-model="menuSearchTerm"
|
|
34
|
+
:debounce="0"
|
|
35
|
+
:placeholder="menuSearchPlaceholder"
|
|
36
|
+
input-class="border-[#242834] bg-[#1f1f1f] text-[#eef2f7] placeholder:text-[#8a92a1] shadow-none focus:border-[#3b82f6] focus:ring-[#3b82f6]/20"
|
|
37
|
+
icon-class="text-[#8a92a1]"
|
|
38
|
+
/>
|
|
59
39
|
</div>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
40
|
+
|
|
41
|
+
<div
|
|
42
|
+
v-if="collapsed"
|
|
43
|
+
class="mt-2 flex flex-col items-center gap-1.5"
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
63
46
|
v-for="item in menuItems"
|
|
64
47
|
:key="item.key"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@click="$emit('item-click', item)"
|
|
48
|
+
class="relative"
|
|
49
|
+
@mouseenter="handleCollapsedItemEnter(item, $event)"
|
|
50
|
+
@mouseleave="handleCollapsedItemLeave(item)"
|
|
69
51
|
>
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"
|
|
52
|
+
<component
|
|
53
|
+
:is="menuItemComponent(item)"
|
|
54
|
+
v-bind="item.bindings"
|
|
55
|
+
class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6"
|
|
56
|
+
:class="item.classes"
|
|
57
|
+
:title="collapsedItemTooltip(item)"
|
|
58
|
+
@mouseenter="handleCollapsedItemEnter(item, $event)"
|
|
59
|
+
@click="handleCollapsedItemClick(item, $event)"
|
|
79
60
|
>
|
|
80
|
-
<kIcon
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
61
|
+
<kIcon
|
|
62
|
+
:name="item.icon"
|
|
63
|
+
size="18"
|
|
64
|
+
:color="item.active ? '#ffffff' : '#b3b9c6'"
|
|
65
|
+
/>
|
|
66
|
+
</component>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div v-else-if="menuItems.length" class="mt-2">
|
|
70
|
+
<TwoColSidebarMenuList
|
|
71
|
+
:items="filteredMenuItems"
|
|
72
|
+
:expanded-state="expandedBranches"
|
|
73
|
+
@item-click="$emit('item-click', $event)"
|
|
74
|
+
@toggle-branch="toggleBranch"
|
|
75
|
+
/>
|
|
95
76
|
</div>
|
|
96
77
|
</div>
|
|
97
78
|
|
|
79
|
+
<Teleport to="body">
|
|
80
|
+
<div
|
|
81
|
+
v-if="collapsedFlyoutItem"
|
|
82
|
+
class="fixed z-[200]"
|
|
83
|
+
:style="collapsedFlyoutStyle"
|
|
84
|
+
@mouseenter="handleCollapsedItemEnter(collapsedFlyoutItem)"
|
|
85
|
+
@mouseleave="handleCollapsedItemLeave(collapsedFlyoutItem)"
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
class="w-[320px] rounded-2xl border border-[#242834] bg-[#0f1116] p-3 shadow-[0_24px_60px_rgba(0,0,0,0.45)]"
|
|
89
|
+
>
|
|
90
|
+
<div class="mb-2 px-2">
|
|
91
|
+
<div class="text-sm font-semibold text-white">
|
|
92
|
+
{{ collapsedFlyoutItem.label }}
|
|
93
|
+
</div>
|
|
94
|
+
<div
|
|
95
|
+
v-if="collapsedFlyoutItem.description"
|
|
96
|
+
class="mt-0.5 text-xs text-[#7f8794]"
|
|
97
|
+
>
|
|
98
|
+
{{ collapsedFlyoutItem.description }}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<TwoColSidebarMenuList
|
|
103
|
+
:items="collapsedFlyoutItem.children"
|
|
104
|
+
:expanded-state="expandedBranches"
|
|
105
|
+
@item-click="$emit('item-click', $event)"
|
|
106
|
+
@toggle-branch="toggleBranch"
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</Teleport>
|
|
111
|
+
|
|
98
112
|
<div
|
|
99
113
|
v-if="account"
|
|
100
|
-
class="
|
|
114
|
+
class="shrink-0 border-t border-[#171a21] px-3 pb-4 pt-3 text-white/90"
|
|
101
115
|
>
|
|
102
116
|
<div v-if="collapsed" class="flex flex-col gap-3">
|
|
103
117
|
<div v-if="helpUrl" class="flex flex-col items-center gap-2">
|
|
@@ -105,66 +119,98 @@
|
|
|
105
119
|
:href="helpUrl"
|
|
106
120
|
target="_blank"
|
|
107
121
|
rel="noreferrer"
|
|
108
|
-
aria-label="
|
|
109
|
-
title="
|
|
110
|
-
class="relative flex items-center justify-center
|
|
111
|
-
v-tooltip.right="'
|
|
122
|
+
aria-label="Βοήθεια"
|
|
123
|
+
title="Βοήθεια"
|
|
124
|
+
class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6 hover:text-white"
|
|
125
|
+
v-tooltip.right="'Βοήθεια'"
|
|
112
126
|
>
|
|
113
|
-
<kIcon name="HelpCircle" size="
|
|
127
|
+
<kIcon name="HelpCircle" size="18" color="#b3b9c6" />
|
|
114
128
|
</a>
|
|
115
129
|
</div>
|
|
116
130
|
<div v-if="account.loggedIn" class="flex flex-col items-center gap-2">
|
|
117
131
|
<button
|
|
118
132
|
type="button"
|
|
119
|
-
class="relative flex items-center justify-center
|
|
133
|
+
class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6 hover:text-white"
|
|
120
134
|
:title="editProfileLabel"
|
|
121
135
|
v-tooltip.right="editProfileLabel"
|
|
122
136
|
@click="$emit('edit-profile')"
|
|
123
137
|
>
|
|
124
|
-
<kIcon name="UserRoundCog" size="
|
|
138
|
+
<kIcon name="UserRoundCog" size="18" color="#b3b9c6" />
|
|
125
139
|
</button>
|
|
126
140
|
<button
|
|
127
141
|
type="button"
|
|
128
|
-
class="group relative flex h-
|
|
142
|
+
class="group relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6 hover:text-white"
|
|
129
143
|
:title="signOutLabel"
|
|
130
144
|
v-tooltip.right="signOutLabel"
|
|
131
145
|
@click="$emit('signout')"
|
|
132
146
|
>
|
|
133
|
-
<kIcon name="LogOut" size="
|
|
147
|
+
<kIcon name="LogOut" size="18" color="#b3b9c6" />
|
|
134
148
|
</button>
|
|
135
149
|
</div>
|
|
136
150
|
<div v-else class="flex flex-col items-center gap-2">
|
|
137
151
|
<button
|
|
138
152
|
type="button"
|
|
139
|
-
class="relative flex items-center justify-center
|
|
153
|
+
class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6 hover:text-white"
|
|
140
154
|
:title="signInLabel"
|
|
141
155
|
v-tooltip.right="signInLabel"
|
|
142
156
|
@click="$emit('signin')"
|
|
143
157
|
>
|
|
144
|
-
<kIcon name="LogIn" size="
|
|
158
|
+
<kIcon name="LogIn" size="18" color="#b3b9c6" />
|
|
145
159
|
</button>
|
|
146
160
|
<button
|
|
147
161
|
type="button"
|
|
148
|
-
class="relative flex items-center justify-center
|
|
162
|
+
class="relative flex h-11 w-11 items-center justify-center rounded-lg border border-transparent text-[#b3b9c6] transition hover:bg-white/6 hover:text-white"
|
|
149
163
|
:title="signUpLabel"
|
|
150
164
|
v-tooltip.right="signUpLabel"
|
|
151
165
|
@click="$emit('signup')"
|
|
152
166
|
>
|
|
153
|
-
<kIcon name="UserPlus" size="
|
|
167
|
+
<kIcon name="UserPlus" size="18" color="#b3b9c6" />
|
|
154
168
|
</button>
|
|
155
169
|
</div>
|
|
156
170
|
</div>
|
|
157
|
-
<div v-else class="flex flex-col
|
|
158
|
-
<div
|
|
171
|
+
<div v-else class="flex flex-col px-1">
|
|
172
|
+
<div
|
|
173
|
+
v-if="helpUrl || account.loggedIn"
|
|
174
|
+
>
|
|
175
|
+
<a
|
|
176
|
+
v-if="helpUrl"
|
|
177
|
+
:href="helpUrl"
|
|
178
|
+
target="_blank"
|
|
179
|
+
rel="noreferrer"
|
|
180
|
+
class="flex w-full items-center gap-3 rounded-xl px-2 py-2 text-lg font-medium text-[#d7dbe3] transition hover:bg-[#242424] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
|
181
|
+
>
|
|
182
|
+
<kIcon name="HelpCircle" size="16" color="#d7dbe3" />
|
|
183
|
+
<span>Βοήθεια και υποστήριξη</span>
|
|
184
|
+
</a>
|
|
185
|
+
<button
|
|
186
|
+
v-if="account.loggedIn"
|
|
187
|
+
type="button"
|
|
188
|
+
class="flex w-full items-center gap-3 rounded-xl px-2 py-2 text-left text-lg font-medium text-[#d7dbe3] transition hover:bg-[#242424] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
|
|
189
|
+
@click="$emit('edit-profile')"
|
|
190
|
+
>
|
|
191
|
+
<kIcon name="UserRoundCog" size="16" color="#d7dbe3" />
|
|
192
|
+
<span>Το προφίλ μου</span>
|
|
193
|
+
</button>
|
|
194
|
+
<button
|
|
195
|
+
v-if="account.loggedIn"
|
|
196
|
+
type="button"
|
|
197
|
+
class="flex w-full items-center gap-3 rounded-xl px-2 py-2 text-left text-lg font-medium text-[#f59e0b] transition hover:bg-[#3a1f12] hover:text-[#fbbf24] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#f59e0b]/40"
|
|
198
|
+
@click="$emit('signout')"
|
|
199
|
+
>
|
|
200
|
+
<kIcon name="LogOut" size="16" color="#f59e0b" />
|
|
201
|
+
<span>Αποσύνδεση</span>
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
<div v-if="!account.loggedIn" class="min-w-0">
|
|
159
205
|
<p
|
|
160
206
|
v-if="account.loggedIn"
|
|
161
|
-
class="mt-
|
|
207
|
+
class="mt-1 truncate text-sm font-semibold text-white"
|
|
162
208
|
>
|
|
163
209
|
{{ accountDisplayName }}
|
|
164
210
|
</p>
|
|
165
211
|
<p
|
|
166
212
|
v-if="account.loggedIn && displayEmail"
|
|
167
|
-
class="text-
|
|
213
|
+
class="truncate text-xs text-[#7c8392]"
|
|
168
214
|
>
|
|
169
215
|
{{ displayEmail }}
|
|
170
216
|
</p>
|
|
@@ -184,33 +230,19 @@
|
|
|
184
230
|
:label="signUpLabel"
|
|
185
231
|
@click="$emit('signup')"
|
|
186
232
|
/>
|
|
187
|
-
<kButton
|
|
188
|
-
v-if="account.loggedIn"
|
|
189
|
-
variant="soft"
|
|
190
|
-
icon="UserRoundCog"
|
|
191
|
-
:label="editProfileLabel"
|
|
192
|
-
@click="$emit('edit-profile')"
|
|
193
|
-
/>
|
|
194
|
-
<kButton
|
|
195
|
-
v-if="account.loggedIn"
|
|
196
|
-
icon="LogOut"
|
|
197
|
-
warning
|
|
198
|
-
:label="signOutLabel"
|
|
199
|
-
@click="$emit('signout')"
|
|
200
|
-
/>
|
|
201
233
|
</div>
|
|
202
234
|
</div>
|
|
203
235
|
</div>
|
|
204
236
|
|
|
205
237
|
<div
|
|
206
238
|
v-if="!collapsed"
|
|
207
|
-
class="border-t
|
|
239
|
+
class="shrink-0 border-t border-[#171a21] p-3 text-[#8c93a1]"
|
|
208
240
|
>
|
|
209
|
-
<div v-if="showContactInfo" class="flex flex-col gap-2 px-
|
|
210
|
-
<p class="text-
|
|
241
|
+
<div v-if="showContactInfo" class="flex flex-col gap-2 px-1">
|
|
242
|
+
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#666d79]">
|
|
211
243
|
KETEKNY AE
|
|
212
244
|
</p>
|
|
213
|
-
<div class="flex flex-wrap items-center
|
|
245
|
+
<div class="flex flex-wrap items-center gap-x-3 text-xs">
|
|
214
246
|
<span class="whitespace-nowrap">{{ contactAddressLabel }}</span>
|
|
215
247
|
<span class="whitespace-nowrap">210 3648 337</span>
|
|
216
248
|
<span class="whitespace-nowrap">
|
|
@@ -232,10 +264,7 @@
|
|
|
232
264
|
</div>
|
|
233
265
|
</div>
|
|
234
266
|
|
|
235
|
-
<div
|
|
236
|
-
v-if="showSponsors"
|
|
237
|
-
class="mt-2 flex flex-col gap-3 bg-[#0d4b87] px-4 py-1"
|
|
238
|
-
>
|
|
267
|
+
<div v-if="showSponsors" class="mt-2 -mx-3 flex flex-col gap-3 bg-[#0d4b87] px-4 py-1">
|
|
239
268
|
<div>
|
|
240
269
|
<img
|
|
241
270
|
src="https://s3.ketekny.gr/public/web-apps/sponsors/Greece 2.0_NextGeneration_White_gr.png"
|
|
@@ -259,6 +288,8 @@
|
|
|
259
288
|
import { RouterLink } from "vue-router";
|
|
260
289
|
import kButton from "../ui/kButton.vue";
|
|
261
290
|
import kIcon from "../ui/kIcon.vue";
|
|
291
|
+
import kSearch from "../ui/kSearch.vue";
|
|
292
|
+
import TwoColSidebarMenuList from "./twoColSidebarMenuList.vue";
|
|
262
293
|
|
|
263
294
|
const LABELS = {
|
|
264
295
|
editProfile: "Το προφίλ μου",
|
|
@@ -274,7 +305,9 @@ export default {
|
|
|
274
305
|
components: {
|
|
275
306
|
kButton,
|
|
276
307
|
kIcon,
|
|
308
|
+
kSearch,
|
|
277
309
|
RouterLink,
|
|
310
|
+
TwoColSidebarMenuList,
|
|
278
311
|
},
|
|
279
312
|
emits: [
|
|
280
313
|
"logo-click",
|
|
@@ -299,8 +332,64 @@ export default {
|
|
|
299
332
|
contactEmail: { type: String, default: "grammateia@ketekny.gr" },
|
|
300
333
|
helpUrl: { type: String, default: "" },
|
|
301
334
|
collapsed: { type: Boolean, default: false },
|
|
335
|
+
showMenuSearch: { type: Boolean, default: false },
|
|
336
|
+
menuSearchPlaceholder: {
|
|
337
|
+
type: String,
|
|
338
|
+
default: "Αναζήτηση...",
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
data() {
|
|
342
|
+
return {
|
|
343
|
+
expandedBranches: {},
|
|
344
|
+
collapsedFlyoutKey: null,
|
|
345
|
+
collapsedFlyoutPosition: null,
|
|
346
|
+
collapsedFlyoutCloseTimer: null,
|
|
347
|
+
menuSearchTerm: "",
|
|
348
|
+
};
|
|
302
349
|
},
|
|
303
350
|
computed: {
|
|
351
|
+
filteredMenuItems() {
|
|
352
|
+
if (!this.showMenuSearch) return this.menuItems;
|
|
353
|
+
|
|
354
|
+
const query = this.menuSearchTerm.trim().toLowerCase();
|
|
355
|
+
if (!query) return this.menuItems;
|
|
356
|
+
|
|
357
|
+
const filterItems = items =>
|
|
358
|
+
items.reduce((acc, item) => {
|
|
359
|
+
const filteredChildren = item.children?.length
|
|
360
|
+
? filterItems(item.children)
|
|
361
|
+
: [];
|
|
362
|
+
const matchesSelf =
|
|
363
|
+
item.label?.toLowerCase().includes(query) ||
|
|
364
|
+
item.description?.toLowerCase().includes(query);
|
|
365
|
+
|
|
366
|
+
if (!matchesSelf && filteredChildren.length === 0) {
|
|
367
|
+
return acc;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
acc.push({
|
|
371
|
+
...item,
|
|
372
|
+
children: filteredChildren,
|
|
373
|
+
hasChildren: filteredChildren.length > 0,
|
|
374
|
+
});
|
|
375
|
+
return acc;
|
|
376
|
+
}, []);
|
|
377
|
+
|
|
378
|
+
return filterItems(this.menuItems);
|
|
379
|
+
},
|
|
380
|
+
collapsedFlyoutItem() {
|
|
381
|
+
if (!this.collapsedFlyoutKey) return null;
|
|
382
|
+
|
|
383
|
+
return this.findMenuItemByKey(this.filteredMenuItems, this.collapsedFlyoutKey);
|
|
384
|
+
},
|
|
385
|
+
collapsedFlyoutStyle() {
|
|
386
|
+
if (!this.collapsedFlyoutPosition) return {};
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
left: `${this.collapsedFlyoutPosition.left}px`,
|
|
390
|
+
top: `${this.collapsedFlyoutPosition.top}px`,
|
|
391
|
+
};
|
|
392
|
+
},
|
|
304
393
|
accountDisplayName() {
|
|
305
394
|
if (!this.account) return "";
|
|
306
395
|
|
|
@@ -345,12 +434,129 @@ export default {
|
|
|
345
434
|
`;
|
|
346
435
|
},
|
|
347
436
|
},
|
|
437
|
+
watch: {
|
|
438
|
+
menuItems: {
|
|
439
|
+
immediate: true,
|
|
440
|
+
handler(items) {
|
|
441
|
+
this.expandedBranches = this.buildExpandedState(items, {
|
|
442
|
+
...this.expandedBranches,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (
|
|
446
|
+
this.collapsedFlyoutKey &&
|
|
447
|
+
!this.findMenuItemByKey(items, this.collapsedFlyoutKey)
|
|
448
|
+
) {
|
|
449
|
+
this.collapsedFlyoutKey = null;
|
|
450
|
+
this.collapsedFlyoutPosition = null;
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
menuSearchTerm() {
|
|
455
|
+
this.expandedBranches = this.buildExpandedState(this.filteredMenuItems, {
|
|
456
|
+
...this.expandedBranches,
|
|
457
|
+
});
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
beforeUnmount() {
|
|
461
|
+
this.clearCollapsedFlyoutCloseTimer();
|
|
462
|
+
},
|
|
348
463
|
methods: {
|
|
349
464
|
menuItemComponent(item) {
|
|
350
465
|
if (item?.isLink) return RouterLink;
|
|
351
466
|
if (item?.isExternal) return "a";
|
|
352
467
|
return "button";
|
|
353
468
|
},
|
|
469
|
+
collapsedItemTooltip(item) {
|
|
470
|
+
return item?.hasChildren ? null : item.label;
|
|
471
|
+
},
|
|
472
|
+
buildExpandedState(items, state = {}) {
|
|
473
|
+
items.forEach(item => {
|
|
474
|
+
if (!item?.hasChildren) return;
|
|
475
|
+
|
|
476
|
+
const hasActiveDescendant = this.branchHasActiveChild(item.children);
|
|
477
|
+
if (!(item.key in state)) {
|
|
478
|
+
state[item.key] = Boolean(hasActiveDescendant);
|
|
479
|
+
} else if (hasActiveDescendant) {
|
|
480
|
+
state[item.key] = true;
|
|
481
|
+
}
|
|
482
|
+
this.buildExpandedState(item.children, state);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
return state;
|
|
486
|
+
},
|
|
487
|
+
branchHasActiveChild(children = []) {
|
|
488
|
+
return children.some(
|
|
489
|
+
child => child.active || this.branchHasActiveChild(child.children || [])
|
|
490
|
+
);
|
|
491
|
+
},
|
|
492
|
+
toggleBranch(key) {
|
|
493
|
+
this.expandedBranches = {
|
|
494
|
+
...this.expandedBranches,
|
|
495
|
+
[key]: !this.expandedBranches[key],
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
findMenuItemByKey(items, key) {
|
|
499
|
+
for (const item of items) {
|
|
500
|
+
if (item.key === key) return item;
|
|
501
|
+
if (item.children?.length) {
|
|
502
|
+
const found = this.findMenuItemByKey(item.children, key);
|
|
503
|
+
if (found) return found;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return null;
|
|
508
|
+
},
|
|
509
|
+
updateCollapsedFlyoutPosition(event) {
|
|
510
|
+
const trigger = event?.currentTarget;
|
|
511
|
+
if (!trigger || typeof window === "undefined") return;
|
|
512
|
+
|
|
513
|
+
const rect = trigger.getBoundingClientRect();
|
|
514
|
+
this.collapsedFlyoutPosition = {
|
|
515
|
+
left: rect.right + 12,
|
|
516
|
+
top: Math.max(12, rect.top),
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
clearCollapsedFlyoutCloseTimer() {
|
|
520
|
+
if (this.collapsedFlyoutCloseTimer) {
|
|
521
|
+
window.clearTimeout(this.collapsedFlyoutCloseTimer);
|
|
522
|
+
this.collapsedFlyoutCloseTimer = null;
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
handleCollapsedItemEnter(item, event) {
|
|
526
|
+
if (!item?.hasChildren) return;
|
|
527
|
+
|
|
528
|
+
this.clearCollapsedFlyoutCloseTimer();
|
|
529
|
+
this.collapsedFlyoutKey = item.key;
|
|
530
|
+
if (event) {
|
|
531
|
+
this.updateCollapsedFlyoutPosition(event);
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
handleCollapsedItemLeave(item) {
|
|
535
|
+
if (this.collapsedFlyoutKey !== item?.key) return;
|
|
536
|
+
|
|
537
|
+
this.clearCollapsedFlyoutCloseTimer();
|
|
538
|
+
this.collapsedFlyoutCloseTimer = window.setTimeout(() => {
|
|
539
|
+
if (this.collapsedFlyoutKey !== item?.key) return;
|
|
540
|
+
|
|
541
|
+
this.collapsedFlyoutKey = null;
|
|
542
|
+
this.collapsedFlyoutPosition = null;
|
|
543
|
+
this.collapsedFlyoutCloseTimer = null;
|
|
544
|
+
}, 180);
|
|
545
|
+
},
|
|
546
|
+
showCollapsedFlyout(item) {
|
|
547
|
+
return this.collapsedFlyoutKey === item?.key && item?.hasChildren;
|
|
548
|
+
},
|
|
549
|
+
handleCollapsedItemClick(item, event) {
|
|
550
|
+
if (item?.hasChildren) {
|
|
551
|
+
event?.preventDefault?.();
|
|
552
|
+
this.clearCollapsedFlyoutCloseTimer();
|
|
553
|
+
this.collapsedFlyoutKey = item.key;
|
|
554
|
+
this.updateCollapsedFlyoutPosition(event);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
this.$emit("item-click", item);
|
|
559
|
+
},
|
|
354
560
|
},
|
|
355
561
|
};
|
|
356
562
|
</script>
|
package/src/ui/kForm.vue
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col gap-2">
|
|
3
|
+
<template v-for="(item, idx) in formInputs" :key="item.id ?? idx">
|
|
4
|
+
<component
|
|
5
|
+
:is="components[item.type]"
|
|
6
|
+
v-bind="item.props"
|
|
7
|
+
:label="item.label + (item.required ? '*' : '')"
|
|
8
|
+
:required="item.required"
|
|
9
|
+
:modelValue="formModel[item.id]"
|
|
10
|
+
:error="errors[item.id]?.message"
|
|
11
|
+
@update:modelValue="(val) => updateField(item.id, val)"
|
|
12
|
+
/>
|
|
13
|
+
</template>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
import kButton from './kButton.vue'
|
|
19
|
+
import kEditor from './kEditor.vue'
|
|
20
|
+
import kInput from './kInput.vue'
|
|
21
|
+
import kSelect from './kSelect.vue'
|
|
22
|
+
import kToggle from './kToggle.vue'
|
|
23
|
+
|
|
24
|
+
const componentsMap = { kInput, kToggle, kButton, kSelect, kEditor }
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
name: 'DynamicForm',
|
|
28
|
+
props: {
|
|
29
|
+
formInputs: { type: Array, default: () => [] },
|
|
30
|
+
formModel: { type: Object, default: () => ({}) },
|
|
31
|
+
},
|
|
32
|
+
emits: ['update:formModel', 'validated'],
|
|
33
|
+
data() {
|
|
34
|
+
return {
|
|
35
|
+
errors: {},
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
computed: {
|
|
39
|
+
components() {
|
|
40
|
+
return componentsMap
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
methods: {
|
|
44
|
+
validate() {
|
|
45
|
+
const errors = {}
|
|
46
|
+
|
|
47
|
+
for (const item of this.formInputs) {
|
|
48
|
+
const id = item.id
|
|
49
|
+
const val = this.formModel[id]
|
|
50
|
+
|
|
51
|
+
if (item.required) {
|
|
52
|
+
const empty =
|
|
53
|
+
val === null ||
|
|
54
|
+
val === undefined ||
|
|
55
|
+
(typeof val === 'string' && val.trim() === '') ||
|
|
56
|
+
(Array.isArray(val) && val.length === 0) ||
|
|
57
|
+
(typeof val === 'boolean' && val === false && item.type !== 'kToggle')
|
|
58
|
+
if (empty) {
|
|
59
|
+
errors[id] = { message: item.requiredMessage || 'Απαιτείται πεδίο.' }
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (item.pattern instanceof RegExp && typeof val === 'string' && !item.pattern.test(val)) {
|
|
65
|
+
errors[id] = { message: item.patternMessage || 'Μη έγκυρη τιμή.' }
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof item.validator === 'function') {
|
|
70
|
+
const res = item.validator(val, this.formModel, item)
|
|
71
|
+
if (res !== true) {
|
|
72
|
+
errors[id] = { message: res || 'Μη έγκυρη τιμή.' }
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.errors = errors
|
|
79
|
+
const valid = Object.keys(errors).length === 0
|
|
80
|
+
this.$emit('validated', { valid, errors })
|
|
81
|
+
return { valid, errors }
|
|
82
|
+
},
|
|
83
|
+
updateField(key, value) {
|
|
84
|
+
this.$emit('update:formModel', { ...this.formModel, [key]: value })
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
</script>
|
package/src/ui/kSearch.vue
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
type="text"
|
|
9
9
|
v-model="localValue"
|
|
10
10
|
@input="onInput"
|
|
11
|
-
:class="[defaultStyle, disabled ? disabledStyle : '']"
|
|
11
|
+
:class="[defaultStyle, inputClass, disabled ? disabledStyle : '']"
|
|
12
12
|
:placeholder="placeholder"
|
|
13
13
|
:aria-label="!label ? placeholder : null"
|
|
14
14
|
:disabled="disabled"
|
|
15
15
|
/>
|
|
16
|
-
<Search
|
|
16
|
+
<Search
|
|
17
|
+
class="absolute w-4 h-4 -translate-y-1/2 pointer-events-none right-3 top-1/2"
|
|
18
|
+
:class="iconClass"
|
|
19
|
+
/>
|
|
17
20
|
</div>
|
|
18
21
|
</template>
|
|
19
22
|
|
|
@@ -30,6 +33,8 @@ export default {
|
|
|
30
33
|
placeholder: { type: String, default: "Αναζήτηση..." },
|
|
31
34
|
label: { type: String, default: "" },
|
|
32
35
|
disabled: { type: Boolean, default: false },
|
|
36
|
+
inputClass: { type: String, default: "" },
|
|
37
|
+
iconClass: { type: String, default: "text-primary/70" },
|
|
33
38
|
},
|
|
34
39
|
data() {
|
|
35
40
|
return {
|