mgv-backoffice 1.0.13 → 1.2.0
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 +735 -70
- package/dist/index.d.ts +1 -27
- package/dist/{components → src/components}/BaseAlert.vue.d.ts +2 -2
- package/dist/src/components/BaseAppLayout.vue.d.ts +31 -0
- package/dist/{components → src/components}/BaseButton.vue.d.ts +5 -2
- package/dist/src/components/BaseCollapsibleSection.vue.d.ts +35 -0
- package/dist/src/components/BaseConfirmModal.vue.d.ts +23 -0
- package/dist/src/components/BaseEntityPickerModal.vue.d.ts +58 -0
- package/dist/{components → src/components}/BaseLogo.vue.d.ts +3 -3
- package/dist/{components → src/components}/BaseModal.vue.d.ts +4 -3
- package/dist/src/components/BaseModalShell.vue.d.ts +43 -0
- package/dist/src/components/BaseNotFoundPage.vue.d.ts +17 -0
- package/dist/src/components/BaseSidebar.vue.d.ts +46 -0
- package/dist/src/components/BaseTextInputModal.vue.d.ts +46 -0
- package/dist/{components → src/components}/BaseToast.vue.d.ts +14 -2
- package/dist/{components → src/components}/Pagination.vue.d.ts +0 -3
- package/dist/src/composables/useDebounce.d.ts +10 -0
- package/dist/src/composables/useEscapeKey.d.ts +6 -0
- package/dist/src/composables/useMobileSidebar.d.ts +6 -0
- package/dist/src/composables/useTheme.d.ts +19 -0
- package/dist/src/composables/useThemeClasses.d.ts +62 -0
- package/dist/src/composables/useToast.d.ts +19 -0
- package/dist/{enums → src/enums}/AlertEnum.d.ts +1 -1
- package/dist/{enums → src/enums}/BaseButtonSizeEnum.d.ts +1 -1
- package/dist/src/enums/BaseLogoEnum.d.ts +7 -0
- package/dist/src/index.d.ts +46 -0
- package/dist/src/types/entityPicker.d.ts +11 -0
- package/dist/src/types/sidebar.d.ts +22 -0
- package/dist/src/utils/httpColors.d.ts +19 -0
- package/dist/ui-lib.css +3 -0
- package/dist/ui-lib.js +1624 -1011
- package/dist/ui-lib.umd.cjs +1 -1
- package/package.json +14 -14
- package/src/components/BaseAlert.vue +2 -2
- package/src/components/BaseAppLayout.vue +65 -0
- package/src/components/BaseBreadcrumb.vue +2 -1
- package/src/components/BaseButton.vue +152 -132
- package/src/components/BaseCollapsibleSection.vue +107 -0
- package/src/components/BaseConfirmModal.vue +110 -0
- package/src/components/BaseEntityPickerModal.vue +330 -0
- package/src/components/BaseLogo.vue +5 -5
- package/src/components/BaseModal.vue +95 -75
- package/src/components/BaseModalShell.vue +115 -0
- package/src/components/BaseNotFoundPage.vue +51 -0
- package/src/components/BaseSidebar.vue +223 -0
- package/src/components/BaseTextInputModal.vue +144 -0
- package/src/components/BaseToast.vue +41 -20
- package/src/components/ColoredSquares.vue +2 -4
- package/src/components/Pagination.vue +137 -136
- package/src/components/TrendArrow.vue +1 -1
- package/src/composables/useDebounce.ts +32 -0
- package/src/composables/useEscapeKey.ts +15 -0
- package/src/composables/useMobileSidebar.ts +23 -0
- package/src/composables/useTheme.ts +69 -0
- package/src/composables/useThemeClasses.ts +133 -0
- package/src/composables/useToast.ts +56 -0
- package/src/enums/AlertEnum.ts +1 -1
- package/src/enums/BaseButtonSizeEnum.ts +1 -1
- package/src/enums/BaseLogoEnum.ts +4 -1
- package/src/index.ts +64 -36
- package/src/types/entityPicker.ts +11 -0
- package/src/types/sidebar.ts +24 -0
- package/src/utils/httpColors.ts +68 -0
- package/src/utils/util.ts +61 -53
- package/dist/enums/BaseLogoEnum.d.ts +0 -5
- package/dist/style.css +0 -1
- /package/dist/{components → src/components}/BaseBadge.vue.d.ts +0 -0
- /package/dist/{components → src/components}/BaseBreadcrumb.vue.d.ts +0 -0
- /package/dist/{components → src/components}/BaseLine.vue.d.ts +0 -0
- /package/dist/{components → src/components}/BaseRow.vue.d.ts +0 -0
- /package/dist/{components → src/components}/BaseSpinner.vue.d.ts +0 -0
- /package/dist/{components → src/components}/ColoredSquares.vue.d.ts +0 -0
- /package/dist/{components → src/components}/EarningsCard.vue.d.ts +0 -0
- /package/dist/{components → src/components}/EuroAmount.vue.d.ts +0 -0
- /package/dist/{components → src/components}/TrendArrow.vue.d.ts +0 -0
- /package/dist/{enums → src/enums}/BaseBadgeEnum.d.ts +0 -0
- /package/dist/{enums → src/enums}/BaseButtonEnum.d.ts +0 -0
- /package/dist/{enums → src/enums}/BaseModalEnum.d.ts +0 -0
- /package/dist/{enums → src/enums}/BaseToastEnum.d.ts +0 -0
- /package/dist/{enums → src/enums}/ColorsEnums.d.ts +0 -0
- /package/dist/{enums → src/enums}/LineEnum.d.ts +0 -0
- /package/dist/{enums → src/enums}/PositioningEnum.d.ts +0 -0
- /package/dist/{utils → src/utils}/util.d.ts +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center justify-center min-h-[60vh]">
|
|
3
|
+
<div class="text-center px-6">
|
|
4
|
+
<ExclamationTriangleIcon class="w-16 h-16 mx-auto mb-4" :class="t.illustration" />
|
|
5
|
+
<h1 class="text-4xl font-bold mb-2" :class="isDark ? 'text-gray-200' : 'text-gray-800'">
|
|
6
|
+
{{ code }}
|
|
7
|
+
</h1>
|
|
8
|
+
<p class="text-lg mb-6" :class="t.dimTextAlt">{{ message }}</p>
|
|
9
|
+
<button
|
|
10
|
+
@click="goHome"
|
|
11
|
+
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-white transition-colors cursor-pointer"
|
|
12
|
+
:class="isDark ? 'bg-emerald-600 hover:bg-emerald-500' : 'bg-emerald-600 hover:bg-emerald-700'"
|
|
13
|
+
>
|
|
14
|
+
{{ homeLabel }}
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import { useRouter } from 'vue-router'
|
|
22
|
+
import { ExclamationTriangleIcon } from '@heroicons/vue/24/outline'
|
|
23
|
+
import { useTheme } from '../composables/useTheme'
|
|
24
|
+
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
/** Big heading. Defaults to '404'. */
|
|
28
|
+
code?: string
|
|
29
|
+
/** Subtitle text. Defaults to 'Page not found'. */
|
|
30
|
+
message?: string
|
|
31
|
+
/** Vue Router named route to redirect to when the button is clicked. */
|
|
32
|
+
homeRouteName?: string
|
|
33
|
+
/** Button label. Defaults to 'Go home'. */
|
|
34
|
+
homeLabel?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
38
|
+
code: '404',
|
|
39
|
+
message: 'Page not found',
|
|
40
|
+
homeRouteName: 'home',
|
|
41
|
+
homeLabel: 'Go home',
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const router = useRouter()
|
|
45
|
+
const { isDark } = useTheme()
|
|
46
|
+
const t = useThemeClasses()
|
|
47
|
+
|
|
48
|
+
function goHome() {
|
|
49
|
+
router.push({ name: props.homeRouteName })
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
|
3
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
4
|
+
import { Bars3Icon, XMarkIcon } from '@heroicons/vue/24/outline'
|
|
5
|
+
import { useTheme } from '../composables/useTheme'
|
|
6
|
+
import { useMobileSidebar } from '../composables/useMobileSidebar'
|
|
7
|
+
import type { NavSection } from '../types/sidebar'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
/** Nav sections (groups of items). */
|
|
11
|
+
sections: NavSection[]
|
|
12
|
+
/** Route name to navigate to when the logo / home button is clicked. */
|
|
13
|
+
homeRouteName?: string
|
|
14
|
+
/** Optional app name shown in the footer next to the version. */
|
|
15
|
+
appName?: string
|
|
16
|
+
/** Optional version string shown in the footer. */
|
|
17
|
+
version?: string
|
|
18
|
+
/** Whether to render the dark/light toggle in the footer. */
|
|
19
|
+
showThemeToggle?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
23
|
+
homeRouteName: 'home',
|
|
24
|
+
appName: '',
|
|
25
|
+
version: '',
|
|
26
|
+
showThemeToggle: true,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const router = useRouter()
|
|
30
|
+
const route = useRoute()
|
|
31
|
+
const { isDark, toggleTheme } = useTheme()
|
|
32
|
+
const { mobileOpen, close: closeMobile, toggle: toggleMobile } = useMobileSidebar()
|
|
33
|
+
|
|
34
|
+
// Refs needed for the mobile focus management.
|
|
35
|
+
const closeMobileButton = ref<HTMLButtonElement | null>(null)
|
|
36
|
+
let lastFocusedBeforeOpen: HTMLElement | null = null
|
|
37
|
+
|
|
38
|
+
// When the mobile sidebar opens, move focus into it so keyboard users
|
|
39
|
+
// don't have to Tab back from <main> (which the surrounding layout
|
|
40
|
+
// marks `inert`); when it closes, restore focus to whatever opened the
|
|
41
|
+
// menu.
|
|
42
|
+
watch(mobileOpen, async (open) => {
|
|
43
|
+
if (open) {
|
|
44
|
+
lastFocusedBeforeOpen = document.activeElement instanceof HTMLElement
|
|
45
|
+
? document.activeElement
|
|
46
|
+
: null
|
|
47
|
+
await nextTick()
|
|
48
|
+
closeMobileButton.value?.focus()
|
|
49
|
+
} else {
|
|
50
|
+
lastFocusedBeforeOpen?.focus()
|
|
51
|
+
lastFocusedBeforeOpen = null
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
onMounted(() => {
|
|
56
|
+
// The visibility-change health-check hook that lived in WireMate's
|
|
57
|
+
// AppSidebar was application-specific; consumers can wire any
|
|
58
|
+
// periodic work in the `#status` slot themselves.
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
onUnmounted(() => {})
|
|
62
|
+
|
|
63
|
+
function goHome() {
|
|
64
|
+
router.push({ name: props.homeRouteName })
|
|
65
|
+
closeMobile()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isActive(name: string): boolean {
|
|
69
|
+
return route.name === name
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function navigate(name: string) {
|
|
73
|
+
router.push({ name })
|
|
74
|
+
closeMobile()
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<div>
|
|
80
|
+
<!-- Mobile top bar (hidden on desktop via scoped CSS to avoid flex/hidden conflict) -->
|
|
81
|
+
<div class="mobile-topbar fixed top-0 inset-x-0 z-30 flex items-center justify-between px-4 py-3 border-b transition-colors"
|
|
82
|
+
:class="isDark ? 'bg-gray-900 border-gray-800' : 'bg-white border-gray-200'">
|
|
83
|
+
<button type="button" @click="goHome" class="cursor-pointer" aria-label="Go to home">
|
|
84
|
+
<!--
|
|
85
|
+
`logo` slot: consumers drop their brand logo in here. Receives
|
|
86
|
+
the current size hint so a logo component can scale to match.
|
|
87
|
+
-->
|
|
88
|
+
<slot name="logo" :size="28" />
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
@click="toggleMobile"
|
|
93
|
+
class="p-2 rounded-lg cursor-pointer"
|
|
94
|
+
:class="isDark ? 'text-gray-400 hover:bg-gray-800' : 'text-gray-600 hover:bg-gray-100'"
|
|
95
|
+
:aria-label="mobileOpen ? 'Close navigation menu' : 'Open navigation menu'"
|
|
96
|
+
:aria-expanded="mobileOpen"
|
|
97
|
+
>
|
|
98
|
+
<Bars3Icon v-if="!mobileOpen" class="w-6 h-6" aria-hidden="true" />
|
|
99
|
+
<XMarkIcon v-else class="w-6 h-6" aria-hidden="true" />
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Mobile overlay (decorative). -->
|
|
104
|
+
<div
|
|
105
|
+
v-if="mobileOpen"
|
|
106
|
+
class="mobile-overlay fixed inset-0 z-40 bg-black/50"
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
@click="closeMobile"
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<aside
|
|
112
|
+
role="navigation"
|
|
113
|
+
aria-label="Main navigation"
|
|
114
|
+
class="fixed top-0 left-0 h-screen w-60 flex flex-col border-r transition-transform duration-300 lg:pointer-events-auto"
|
|
115
|
+
:class="[
|
|
116
|
+
isDark ? 'bg-gray-900 border-gray-800' : 'bg-white border-gray-200',
|
|
117
|
+
mobileOpen ? 'translate-x-0 z-50' : '-translate-x-full max-lg:pointer-events-none lg:translate-x-0 z-50',
|
|
118
|
+
]"
|
|
119
|
+
>
|
|
120
|
+
<div class="flex flex-col h-full">
|
|
121
|
+
<!-- Logo -->
|
|
122
|
+
<div class="px-5 pt-5 pb-4 flex items-center justify-center relative">
|
|
123
|
+
<button type="button" @click="goHome" class="cursor-pointer" aria-label="Go to home">
|
|
124
|
+
<slot name="logo" :size="52" />
|
|
125
|
+
</button>
|
|
126
|
+
<button
|
|
127
|
+
ref="closeMobileButton"
|
|
128
|
+
type="button"
|
|
129
|
+
@click="closeMobile"
|
|
130
|
+
class="lg:hidden p-1.5 rounded-lg cursor-pointer absolute right-3 top-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
|
|
131
|
+
:class="isDark ? 'text-gray-400 hover:bg-gray-800' : 'text-gray-500 hover:bg-gray-100'"
|
|
132
|
+
aria-label="Close navigation menu"
|
|
133
|
+
>
|
|
134
|
+
<XMarkIcon class="w-5 h-5" aria-hidden="true" />
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- Nav -->
|
|
139
|
+
<nav class="flex-1 overflow-y-auto px-3 space-y-6">
|
|
140
|
+
<div v-for="section in sections" :key="section.title">
|
|
141
|
+
<p class="px-2 mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
|
142
|
+
{{ section.title }}
|
|
143
|
+
</p>
|
|
144
|
+
<ul class="space-y-0.5">
|
|
145
|
+
<li v-for="item in section.items" :key="item.name">
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
:aria-current="isActive(item.name) ? 'page' : undefined"
|
|
149
|
+
@click="navigate(item.name)"
|
|
150
|
+
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors w-full text-left cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
|
|
151
|
+
:class="isActive(item.name)
|
|
152
|
+
? (isDark
|
|
153
|
+
? 'bg-emerald-500/15 text-emerald-300'
|
|
154
|
+
: 'bg-emerald-50 text-emerald-700')
|
|
155
|
+
: (isDark
|
|
156
|
+
? 'text-gray-400 hover:text-gray-100 hover:bg-gray-800'
|
|
157
|
+
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100')"
|
|
158
|
+
>
|
|
159
|
+
<component :is="item.icon" class="w-[18px] h-[18px] shrink-0" />
|
|
160
|
+
{{ item.label }}
|
|
161
|
+
</button>
|
|
162
|
+
</li>
|
|
163
|
+
</ul>
|
|
164
|
+
</div>
|
|
165
|
+
</nav>
|
|
166
|
+
|
|
167
|
+
<!-- Bottom: Theme toggle, status slot, footer slot -->
|
|
168
|
+
<div class="px-4 py-4 border-t transition-colors space-y-3"
|
|
169
|
+
:class="isDark ? 'border-gray-800' : 'border-gray-200'">
|
|
170
|
+
<button
|
|
171
|
+
v-if="showThemeToggle"
|
|
172
|
+
type="button"
|
|
173
|
+
@click="toggleTheme"
|
|
174
|
+
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
175
|
+
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
176
|
+
:aria-pressed="isDark"
|
|
177
|
+
class="flex items-center gap-3 w-full px-3 py-2.5 rounded-lg text-sm font-medium transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
|
|
178
|
+
:class="isDark
|
|
179
|
+
? 'text-gray-400 hover:text-gray-100 hover:bg-gray-800'
|
|
180
|
+
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'"
|
|
181
|
+
>
|
|
182
|
+
{{ isDark ? 'Switch to light' : 'Switch to dark' }}
|
|
183
|
+
<span class="ml-auto relative w-9 h-5 rounded-full transition-colors"
|
|
184
|
+
:class="isDark ? 'bg-emerald-600' : 'bg-gray-300'">
|
|
185
|
+
<span class="absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow-sm transition-transform duration-300"
|
|
186
|
+
:class="isDark ? 'translate-x-4' : 'translate-x-0'" />
|
|
187
|
+
</span>
|
|
188
|
+
</button>
|
|
189
|
+
|
|
190
|
+
<!--
|
|
191
|
+
`status` slot: consumers drop in their own indicator (e.g.
|
|
192
|
+
health check, sync status). The wrapper handles spacing only.
|
|
193
|
+
-->
|
|
194
|
+
<slot name="status" />
|
|
195
|
+
|
|
196
|
+
<!--
|
|
197
|
+
`footer` slot: links / version / etc. If not provided and
|
|
198
|
+
either `appName` or `version` is set, we render a default
|
|
199
|
+
"appName vVersion" line.
|
|
200
|
+
-->
|
|
201
|
+
<slot name="footer">
|
|
202
|
+
<div
|
|
203
|
+
v-if="appName || version"
|
|
204
|
+
class="flex items-center justify-center gap-2 text-xs"
|
|
205
|
+
:class="isDark ? 'text-gray-500' : 'text-gray-500'"
|
|
206
|
+
>
|
|
207
|
+
<span>{{ appName }}<template v-if="appName && version"> </template><template v-if="version">v{{ version }}</template></span>
|
|
208
|
+
</div>
|
|
209
|
+
</slot>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</aside>
|
|
213
|
+
</div>
|
|
214
|
+
</template>
|
|
215
|
+
|
|
216
|
+
<style scoped>
|
|
217
|
+
@media (min-width: 1024px) {
|
|
218
|
+
/* Hide mobile top bar on desktop */
|
|
219
|
+
.mobile-topbar {
|
|
220
|
+
display: none !important;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
</style>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Generic "ask the user for a single string and confirm" dialog.
|
|
4
|
+
Used for clone-name prompts, rename dialogs, etc.
|
|
5
|
+
|
|
6
|
+
Slots:
|
|
7
|
+
• icon — replace the default emerald-tinted document icon.
|
|
8
|
+
|
|
9
|
+
Preserves typed input on stray backdrop clicks (only treats the
|
|
10
|
+
click as cancel if the user hasn't modified the initial value).
|
|
11
|
+
Escape is always treated as an explicit dismissal.
|
|
12
|
+
-->
|
|
13
|
+
<BaseModalShell
|
|
14
|
+
:title="title"
|
|
15
|
+
manual-close
|
|
16
|
+
@backdrop="handleBackdropClick"
|
|
17
|
+
>
|
|
18
|
+
<template #icon>
|
|
19
|
+
<slot name="icon">
|
|
20
|
+
<div
|
|
21
|
+
class="flex-shrink-0 flex h-12 w-12 items-center justify-center rounded-full"
|
|
22
|
+
:class="isDark ? 'bg-emerald-900/30' : 'bg-emerald-50'"
|
|
23
|
+
>
|
|
24
|
+
<DocumentDuplicateIcon class="h-6 w-6 text-emerald-600" aria-hidden="true" />
|
|
25
|
+
</div>
|
|
26
|
+
</slot>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<p class="text-sm" :class="isDark ? 'text-gray-300' : 'text-gray-600'">
|
|
30
|
+
{{ message }}
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<div class="mt-4">
|
|
34
|
+
<label
|
|
35
|
+
class="block text-sm font-medium mb-1"
|
|
36
|
+
:class="t.label"
|
|
37
|
+
>
|
|
38
|
+
{{ inputLabel }}
|
|
39
|
+
</label>
|
|
40
|
+
<input
|
|
41
|
+
ref="nameInput"
|
|
42
|
+
v-model="inputValue"
|
|
43
|
+
type="text"
|
|
44
|
+
:placeholder="placeholder"
|
|
45
|
+
class="w-full px-3 py-2 rounded-md border text-sm transition-colors"
|
|
46
|
+
:class="isDark
|
|
47
|
+
? 'bg-gray-800 border-gray-600 text-gray-100 placeholder-gray-500 focus:outline-none focus:border-emerald-500'
|
|
48
|
+
: 'bg-white border-gray-300 text-gray-900 placeholder-gray-400 focus:outline-none focus:border-emerald-500'"
|
|
49
|
+
@keyup.enter="handleConfirm"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<template #footer>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
class="inline-flex items-center justify-center px-4 py-2 border rounded-md shadow-sm text-sm font-medium transition-colors duration-150"
|
|
57
|
+
:class="t.ghostButton"
|
|
58
|
+
@click="handleCancel"
|
|
59
|
+
>
|
|
60
|
+
{{ cancelText }}
|
|
61
|
+
</button>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
:disabled="!inputValue.trim() || submitting"
|
|
65
|
+
class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white transition-colors duration-150"
|
|
66
|
+
:class="[
|
|
67
|
+
!inputValue.trim() || submitting
|
|
68
|
+
? 'bg-emerald-400 cursor-not-allowed'
|
|
69
|
+
: 'bg-emerald-600 hover:bg-emerald-700 cursor-pointer'
|
|
70
|
+
]"
|
|
71
|
+
@click="handleConfirm"
|
|
72
|
+
>
|
|
73
|
+
{{ submitting ? submittingText : confirmText }}
|
|
74
|
+
</button>
|
|
75
|
+
</template>
|
|
76
|
+
</BaseModalShell>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script setup lang="ts">
|
|
80
|
+
import { ref, onMounted, nextTick } from 'vue'
|
|
81
|
+
import { DocumentDuplicateIcon } from '@heroicons/vue/24/outline'
|
|
82
|
+
import BaseModalShell from './BaseModalShell.vue'
|
|
83
|
+
import { useTheme } from '../composables/useTheme'
|
|
84
|
+
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
85
|
+
import { useEscapeKey } from '../composables/useEscapeKey'
|
|
86
|
+
|
|
87
|
+
interface Props {
|
|
88
|
+
title: string
|
|
89
|
+
message: string
|
|
90
|
+
initialValue?: string
|
|
91
|
+
placeholder?: string
|
|
92
|
+
inputLabel?: string
|
|
93
|
+
confirmText?: string
|
|
94
|
+
cancelText?: string
|
|
95
|
+
submittingText?: string
|
|
96
|
+
submitting?: boolean
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
100
|
+
initialValue: '',
|
|
101
|
+
placeholder: 'Enter value...',
|
|
102
|
+
inputLabel: 'Name',
|
|
103
|
+
confirmText: 'Confirm',
|
|
104
|
+
cancelText: 'Cancel',
|
|
105
|
+
submittingText: 'Working...',
|
|
106
|
+
submitting: false,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const emit = defineEmits<{
|
|
110
|
+
confirm: [value: string]
|
|
111
|
+
cancel: []
|
|
112
|
+
}>()
|
|
113
|
+
|
|
114
|
+
const { isDark } = useTheme()
|
|
115
|
+
const t = useThemeClasses()
|
|
116
|
+
const inputValue = ref(props.initialValue)
|
|
117
|
+
const nameInput = ref<HTMLInputElement | null>(null)
|
|
118
|
+
|
|
119
|
+
onMounted(async () => {
|
|
120
|
+
await nextTick()
|
|
121
|
+
nameInput.value?.focus()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
function handleConfirm() {
|
|
125
|
+
if (!inputValue.value.trim() || props.submitting) return
|
|
126
|
+
emit('confirm', inputValue.value.trim())
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function handleCancel() {
|
|
130
|
+
emit('cancel')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Backdrop click: only treat it as cancel if the user hasn't modified
|
|
134
|
+
// the value. Otherwise a stray click outside the dialog would silently
|
|
135
|
+
// throw away the typed input.
|
|
136
|
+
function handleBackdropClick() {
|
|
137
|
+
if (inputValue.value.trim() === (props.initialValue ?? '').trim()) {
|
|
138
|
+
handleCancel()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Escape always cancels.
|
|
143
|
+
useEscapeKey(handleCancel)
|
|
144
|
+
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="
|
|
2
|
+
<div v-if="isVisible" id="toast-top-right" :class="computeMainComponentCss()" role="alert">
|
|
3
3
|
|
|
4
4
|
<div class="flex items-center w-full max-w-md p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800">
|
|
5
5
|
<div v-if="mode == BaseToastEnum.ERROR" :class="computeModeCss()">
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<span class="sr-only">Close</span>
|
|
22
22
|
<XMarkIcon
|
|
23
23
|
class="h-6 w-6 text-gray-500 cursor-pointer"
|
|
24
|
-
@click="
|
|
24
|
+
@click="close"
|
|
25
25
|
/>
|
|
26
26
|
</button>
|
|
27
27
|
</div>
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
<script lang="ts" setup>
|
|
33
33
|
import { ExclamationCircleIcon, CheckCircleIcon, ExclamationTriangleIcon, XMarkIcon } from '@heroicons/vue/24/outline';
|
|
34
|
-
import { ref, PropType } from "vue";
|
|
34
|
+
import { ref, onMounted, onUnmounted, PropType } from "vue";
|
|
35
35
|
import { BaseToastEnum } from "../enums/BaseToastEnum";
|
|
36
36
|
import { PositioningEnum } from "../enums/PositioningEnum";
|
|
37
37
|
|
|
@@ -52,34 +52,63 @@ const props = defineProps({
|
|
|
52
52
|
positioning: {
|
|
53
53
|
type: String,
|
|
54
54
|
required: false,
|
|
55
|
-
default:
|
|
55
|
+
default: PositioningEnum.TOP_RIGHT
|
|
56
|
+
},
|
|
57
|
+
duration: {
|
|
58
|
+
type: Number,
|
|
59
|
+
required: false,
|
|
60
|
+
default: 5000
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const isVisible = ref(true);
|
|
65
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
66
|
+
|
|
67
|
+
function close() {
|
|
68
|
+
isVisible.value = false;
|
|
69
|
+
if (timer) {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
timer = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onMounted(() => {
|
|
76
|
+
timer = setTimeout(() => isVisible.value = false, props.duration);
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
onUnmounted(() => {
|
|
80
|
+
if (timer) {
|
|
81
|
+
clearTimeout(timer);
|
|
56
82
|
}
|
|
57
83
|
})
|
|
58
84
|
|
|
59
85
|
function computeModeCss() {
|
|
60
|
-
let
|
|
86
|
+
let modeCss = "";
|
|
61
87
|
switch (props.mode) {
|
|
62
88
|
case (BaseToastEnum.ERROR):
|
|
63
|
-
|
|
89
|
+
modeCss = "text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200"
|
|
64
90
|
break;
|
|
65
91
|
case (BaseToastEnum.SUCCESS):
|
|
66
|
-
|
|
92
|
+
modeCss = "text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200"
|
|
67
93
|
break;
|
|
68
94
|
case (BaseToastEnum.WARNING):
|
|
69
|
-
|
|
95
|
+
modeCss = "text-orange-500 bg-orange-100 dark:bg-orange-700 dark:text-orange-200"
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
modeCss = "text-gray-500 bg-gray-100 dark:bg-gray-800 dark:text-gray-200"
|
|
70
99
|
break;
|
|
71
100
|
}
|
|
72
|
-
return 'inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ' +
|
|
101
|
+
return 'inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg ' + modeCss;
|
|
73
102
|
}
|
|
74
103
|
|
|
75
104
|
function computeMainComponentCss() {
|
|
76
105
|
let basePositioning = "top-5 right-5";
|
|
77
106
|
switch (props.positioning) {
|
|
78
107
|
case (PositioningEnum.BOTTOM_LEFT):
|
|
79
|
-
basePositioning = "bottom-5"
|
|
108
|
+
basePositioning = "bottom-5 left-5"
|
|
80
109
|
break;
|
|
81
110
|
case (PositioningEnum.TOP_LEFT):
|
|
82
|
-
basePositioning = "top-5"
|
|
111
|
+
basePositioning = "top-5 left-5"
|
|
83
112
|
break;
|
|
84
113
|
case (PositioningEnum.TOP_RIGHT):
|
|
85
114
|
basePositioning = "top-5 right-5"
|
|
@@ -88,14 +117,6 @@ function computeMainComponentCss() {
|
|
|
88
117
|
basePositioning = "bottom-5 right-5"
|
|
89
118
|
break;
|
|
90
119
|
}
|
|
91
|
-
return 'fixed flex items-center w-full max-w-md p-4 space-x-4 divide-x rounded-lg ' + basePositioning
|
|
120
|
+
return 'fixed flex items-center w-full max-w-md p-4 space-x-4 divide-x rounded-lg ' + basePositioning;
|
|
92
121
|
}
|
|
93
|
-
|
|
94
|
-
const isLoading = ref(true);
|
|
95
|
-
|
|
96
|
-
function disable() {
|
|
97
|
-
setTimeout(() => isLoading.value = false, 1000000);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
disable()
|
|
101
122
|
</script>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
</template>
|
|
12
12
|
|
|
13
13
|
<script setup lang="ts">
|
|
14
|
-
import {
|
|
14
|
+
import { ref } from 'vue'
|
|
15
15
|
import { ColorsEnums } from "../enums/ColorsEnums";
|
|
16
16
|
|
|
17
17
|
const props = defineProps({
|
|
@@ -33,9 +33,7 @@ const pastelColors = [
|
|
|
33
33
|
'bg-pink-300'
|
|
34
34
|
]
|
|
35
35
|
|
|
36
|
-
const bgColor =
|
|
37
|
-
return pastelColors[Math.floor(Math.random() * pastelColors.length)]
|
|
38
|
-
})
|
|
36
|
+
const bgColor = ref(pastelColors[Math.floor(Math.random() * pastelColors.length)])
|
|
39
37
|
|
|
40
38
|
function computeCss() {
|
|
41
39
|
let color = 'gray';
|