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,330 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<BaseModalShell
|
|
3
|
+
:title="title"
|
|
4
|
+
:manual-close="submitting"
|
|
5
|
+
@cancel="handleCancel"
|
|
6
|
+
>
|
|
7
|
+
<template #icon>
|
|
8
|
+
<slot name="icon">
|
|
9
|
+
<div
|
|
10
|
+
class="flex-shrink-0 flex h-12 w-12 items-center justify-center rounded-full"
|
|
11
|
+
:class="palette.iconBg"
|
|
12
|
+
>
|
|
13
|
+
<FolderIcon class="h-6 w-6" :class="palette.iconText" aria-hidden="true" />
|
|
14
|
+
</div>
|
|
15
|
+
</slot>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<p v-if="message" class="text-sm" :class="isDark ? 'text-gray-300' : 'text-gray-600'">
|
|
19
|
+
{{ message }}
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<!-- Loading -->
|
|
23
|
+
<div v-if="loading" class="mt-4 flex justify-center py-6">
|
|
24
|
+
<BaseSpinner />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Error -->
|
|
28
|
+
<div v-else-if="loadError" class="mt-4 text-sm text-red-500" role="alert">
|
|
29
|
+
{{ loadError }}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Search input -->
|
|
33
|
+
<div v-if="!loading && !loadError && items.length > 0" class="mt-4">
|
|
34
|
+
<div class="relative">
|
|
35
|
+
<MagnifyingGlassIcon
|
|
36
|
+
class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4"
|
|
37
|
+
:class="t.dimText"
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
/>
|
|
40
|
+
<input
|
|
41
|
+
ref="searchInputRef"
|
|
42
|
+
v-model="searchQuery"
|
|
43
|
+
type="text"
|
|
44
|
+
:placeholder="searchPlaceholder"
|
|
45
|
+
class="w-full pl-9 pr-3 py-2 text-sm rounded-md border outline-none transition-colors"
|
|
46
|
+
:class="[
|
|
47
|
+
isDark
|
|
48
|
+
? 'bg-gray-800 border-gray-600 text-gray-200 placeholder-gray-500'
|
|
49
|
+
: 'bg-white border-gray-300 text-gray-700 placeholder-gray-400',
|
|
50
|
+
palette.inputFocus,
|
|
51
|
+
]"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Item list -->
|
|
57
|
+
<div v-if="!loading && !loadError" class="mt-3 max-h-64 overflow-y-auto">
|
|
58
|
+
<div
|
|
59
|
+
v-for="item in filteredItems"
|
|
60
|
+
:key="item.id"
|
|
61
|
+
class="flex items-center gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-colors"
|
|
62
|
+
:class="[
|
|
63
|
+
selectedId === item.id
|
|
64
|
+
? palette.selectedRow
|
|
65
|
+
: isDark
|
|
66
|
+
? 'hover:bg-gray-800 border border-transparent'
|
|
67
|
+
: 'hover:bg-gray-50 border border-transparent'
|
|
68
|
+
]"
|
|
69
|
+
@click="selectedId = item.id"
|
|
70
|
+
>
|
|
71
|
+
<FolderIcon class="w-5 h-5 flex-shrink-0" :class="t.dimText" />
|
|
72
|
+
<div class="flex-1 min-w-0">
|
|
73
|
+
<p class="text-sm font-medium truncate" :class="t.bodyText">
|
|
74
|
+
{{ item.label }}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
<div v-if="selectedId === item.id"
|
|
78
|
+
class="w-5 h-5 rounded-full flex items-center justify-center"
|
|
79
|
+
:class="palette.checkBg">
|
|
80
|
+
<svg class="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3" aria-hidden="true">
|
|
81
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
82
|
+
</svg>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Empty states -->
|
|
87
|
+
<div v-if="items.length === 0" class="text-center py-6">
|
|
88
|
+
<p class="text-sm" :class="t.dimText">
|
|
89
|
+
{{ emptyMessage }}
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
<div v-else-if="filteredItems.length === 0" class="text-center py-6">
|
|
93
|
+
<p class="text-sm" :class="t.dimText">
|
|
94
|
+
{{ noMatchMessage }}
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<template #footer>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
:disabled="submitting"
|
|
103
|
+
class="inline-flex items-center justify-center px-4 py-2 border rounded-md shadow-sm text-sm font-medium transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
104
|
+
:class="t.ghostButton"
|
|
105
|
+
@click="handleCancel"
|
|
106
|
+
>
|
|
107
|
+
{{ cancelText }}
|
|
108
|
+
</button>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
:disabled="!selectedId || submitting"
|
|
112
|
+
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"
|
|
113
|
+
:class="!selectedId || submitting ? palette.confirmDisabled : palette.confirmActive"
|
|
114
|
+
@click="handleConfirm"
|
|
115
|
+
>
|
|
116
|
+
{{ submitting ? submittingText : confirmText }}
|
|
117
|
+
</button>
|
|
118
|
+
</template>
|
|
119
|
+
</BaseModalShell>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<script setup lang="ts">
|
|
123
|
+
import { ref, computed, onMounted, nextTick } from 'vue'
|
|
124
|
+
import { FolderIcon, MagnifyingGlassIcon } from '@heroicons/vue/24/outline'
|
|
125
|
+
import BaseModalShell from './BaseModalShell.vue'
|
|
126
|
+
import BaseSpinner from './BaseSpinner.vue'
|
|
127
|
+
import { useTheme } from '../composables/useTheme'
|
|
128
|
+
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
129
|
+
import type { EntityPickerItem } from '../types/entityPicker'
|
|
130
|
+
|
|
131
|
+
type Variant = 'emerald' | 'purple' | 'blue' | 'red' | 'amber'
|
|
132
|
+
|
|
133
|
+
interface PaletteClasses {
|
|
134
|
+
iconBg: string
|
|
135
|
+
iconText: string
|
|
136
|
+
inputFocus: string
|
|
137
|
+
selectedRow: string
|
|
138
|
+
checkBg: string
|
|
139
|
+
confirmActive: string
|
|
140
|
+
confirmDisabled: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface Props {
|
|
144
|
+
title: string
|
|
145
|
+
message?: string
|
|
146
|
+
/** Pre-loaded items. Pass these instead of `loader` when the items are already in memory. */
|
|
147
|
+
items?: EntityPickerItem[]
|
|
148
|
+
/** Async loader called on mount. Use when items live in a backend service. */
|
|
149
|
+
loader?: () => Promise<EntityPickerItem[]>
|
|
150
|
+
/** ID to exclude from the list (e.g. the current entity). */
|
|
151
|
+
excludeId?: string
|
|
152
|
+
/** Accent colour for selection / confirm. Defaults to 'emerald'. */
|
|
153
|
+
variant?: Variant
|
|
154
|
+
searchPlaceholder?: string
|
|
155
|
+
emptyMessage?: string
|
|
156
|
+
noMatchMessage?: string
|
|
157
|
+
confirmText?: string
|
|
158
|
+
cancelText?: string
|
|
159
|
+
submittingText?: string
|
|
160
|
+
submitting?: boolean
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
164
|
+
items: () => [],
|
|
165
|
+
variant: 'emerald',
|
|
166
|
+
searchPlaceholder: 'Search...',
|
|
167
|
+
emptyMessage: 'No items available.',
|
|
168
|
+
noMatchMessage: 'No items match your search.',
|
|
169
|
+
confirmText: 'Select',
|
|
170
|
+
cancelText: 'Cancel',
|
|
171
|
+
submittingText: 'Working...',
|
|
172
|
+
submitting: false,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const emit = defineEmits<{
|
|
176
|
+
confirm: [itemId: string]
|
|
177
|
+
cancel: []
|
|
178
|
+
}>()
|
|
179
|
+
|
|
180
|
+
const { isDark } = useTheme()
|
|
181
|
+
const t = useThemeClasses()
|
|
182
|
+
|
|
183
|
+
const loadedItems = ref<EntityPickerItem[]>([])
|
|
184
|
+
const loading = ref(false)
|
|
185
|
+
const loadError = ref('')
|
|
186
|
+
const selectedId = ref<string | null>(null)
|
|
187
|
+
const searchQuery = ref('')
|
|
188
|
+
const searchInputRef = ref<HTMLInputElement | null>(null)
|
|
189
|
+
|
|
190
|
+
const items = computed<EntityPickerItem[]>(() => {
|
|
191
|
+
const source = props.loader ? loadedItems.value : props.items
|
|
192
|
+
return props.excludeId
|
|
193
|
+
? source.filter(i => i.id !== props.excludeId)
|
|
194
|
+
: source
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const filteredItems = computed(() =>
|
|
198
|
+
items.value.filter(i =>
|
|
199
|
+
i.label.toLowerCase().includes(searchQuery.value.toLowerCase()),
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// Static palette maps — Tailwind's JIT can't see classes built from
|
|
204
|
+
// template strings (`bg-${variant}-500`), so we expand each variant to a
|
|
205
|
+
// concrete bundle of utilities that the compiler can find at build time.
|
|
206
|
+
const LIGHT_PALETTES: Record<Variant, PaletteClasses> = {
|
|
207
|
+
emerald: {
|
|
208
|
+
iconBg: 'bg-emerald-50',
|
|
209
|
+
iconText: 'text-emerald-600',
|
|
210
|
+
inputFocus: 'focus:border-emerald-500',
|
|
211
|
+
selectedRow: 'bg-emerald-50 border border-emerald-200',
|
|
212
|
+
checkBg: 'bg-emerald-600',
|
|
213
|
+
confirmActive: 'bg-emerald-600 hover:bg-emerald-700 cursor-pointer',
|
|
214
|
+
confirmDisabled: 'bg-emerald-400 cursor-not-allowed',
|
|
215
|
+
},
|
|
216
|
+
purple: {
|
|
217
|
+
iconBg: 'bg-purple-50',
|
|
218
|
+
iconText: 'text-purple-600',
|
|
219
|
+
inputFocus: 'focus:border-purple-500',
|
|
220
|
+
selectedRow: 'bg-purple-50 border border-purple-200',
|
|
221
|
+
checkBg: 'bg-purple-600',
|
|
222
|
+
confirmActive: 'bg-purple-600 hover:bg-purple-700 cursor-pointer',
|
|
223
|
+
confirmDisabled: 'bg-purple-400 cursor-not-allowed',
|
|
224
|
+
},
|
|
225
|
+
blue: {
|
|
226
|
+
iconBg: 'bg-blue-50',
|
|
227
|
+
iconText: 'text-blue-600',
|
|
228
|
+
inputFocus: 'focus:border-blue-500',
|
|
229
|
+
selectedRow: 'bg-blue-50 border border-blue-200',
|
|
230
|
+
checkBg: 'bg-blue-600',
|
|
231
|
+
confirmActive: 'bg-blue-600 hover:bg-blue-700 cursor-pointer',
|
|
232
|
+
confirmDisabled: 'bg-blue-400 cursor-not-allowed',
|
|
233
|
+
},
|
|
234
|
+
red: {
|
|
235
|
+
iconBg: 'bg-red-50',
|
|
236
|
+
iconText: 'text-red-600',
|
|
237
|
+
inputFocus: 'focus:border-red-500',
|
|
238
|
+
selectedRow: 'bg-red-50 border border-red-200',
|
|
239
|
+
checkBg: 'bg-red-600',
|
|
240
|
+
confirmActive: 'bg-red-600 hover:bg-red-700 cursor-pointer',
|
|
241
|
+
confirmDisabled: 'bg-red-400 cursor-not-allowed',
|
|
242
|
+
},
|
|
243
|
+
amber: {
|
|
244
|
+
iconBg: 'bg-amber-50',
|
|
245
|
+
iconText: 'text-amber-600',
|
|
246
|
+
inputFocus: 'focus:border-amber-500',
|
|
247
|
+
selectedRow: 'bg-amber-50 border border-amber-200',
|
|
248
|
+
checkBg: 'bg-amber-600',
|
|
249
|
+
confirmActive: 'bg-amber-600 hover:bg-amber-700 cursor-pointer',
|
|
250
|
+
confirmDisabled: 'bg-amber-400 cursor-not-allowed',
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const DARK_PALETTES: Record<Variant, PaletteClasses> = {
|
|
255
|
+
emerald: {
|
|
256
|
+
iconBg: 'bg-emerald-900/30',
|
|
257
|
+
iconText: 'text-emerald-400',
|
|
258
|
+
inputFocus: 'focus:border-emerald-500',
|
|
259
|
+
selectedRow: 'bg-emerald-500/15 border border-emerald-500/30',
|
|
260
|
+
checkBg: 'bg-emerald-600',
|
|
261
|
+
confirmActive: 'bg-emerald-600 hover:bg-emerald-700 cursor-pointer',
|
|
262
|
+
confirmDisabled: 'bg-emerald-400 cursor-not-allowed',
|
|
263
|
+
},
|
|
264
|
+
purple: {
|
|
265
|
+
iconBg: 'bg-purple-900/30',
|
|
266
|
+
iconText: 'text-purple-400',
|
|
267
|
+
inputFocus: 'focus:border-purple-500',
|
|
268
|
+
selectedRow: 'bg-purple-500/15 border border-purple-500/30',
|
|
269
|
+
checkBg: 'bg-purple-600',
|
|
270
|
+
confirmActive: 'bg-purple-600 hover:bg-purple-700 cursor-pointer',
|
|
271
|
+
confirmDisabled: 'bg-purple-400 cursor-not-allowed',
|
|
272
|
+
},
|
|
273
|
+
blue: {
|
|
274
|
+
iconBg: 'bg-blue-900/30',
|
|
275
|
+
iconText: 'text-blue-400',
|
|
276
|
+
inputFocus: 'focus:border-blue-500',
|
|
277
|
+
selectedRow: 'bg-blue-500/15 border border-blue-500/30',
|
|
278
|
+
checkBg: 'bg-blue-600',
|
|
279
|
+
confirmActive: 'bg-blue-600 hover:bg-blue-700 cursor-pointer',
|
|
280
|
+
confirmDisabled: 'bg-blue-400 cursor-not-allowed',
|
|
281
|
+
},
|
|
282
|
+
red: {
|
|
283
|
+
iconBg: 'bg-red-900/30',
|
|
284
|
+
iconText: 'text-red-400',
|
|
285
|
+
inputFocus: 'focus:border-red-500',
|
|
286
|
+
selectedRow: 'bg-red-500/15 border border-red-500/30',
|
|
287
|
+
checkBg: 'bg-red-600',
|
|
288
|
+
confirmActive: 'bg-red-600 hover:bg-red-700 cursor-pointer',
|
|
289
|
+
confirmDisabled: 'bg-red-400 cursor-not-allowed',
|
|
290
|
+
},
|
|
291
|
+
amber: {
|
|
292
|
+
iconBg: 'bg-amber-900/30',
|
|
293
|
+
iconText: 'text-amber-400',
|
|
294
|
+
inputFocus: 'focus:border-amber-500',
|
|
295
|
+
selectedRow: 'bg-amber-500/15 border border-amber-500/30',
|
|
296
|
+
checkBg: 'bg-amber-600',
|
|
297
|
+
confirmActive: 'bg-amber-600 hover:bg-amber-700 cursor-pointer',
|
|
298
|
+
confirmDisabled: 'bg-amber-400 cursor-not-allowed',
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const palette = computed<PaletteClasses>(() =>
|
|
303
|
+
(isDark.value ? DARK_PALETTES : LIGHT_PALETTES)[props.variant],
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
onMounted(async () => {
|
|
307
|
+
if (props.loader) {
|
|
308
|
+
loading.value = true
|
|
309
|
+
try {
|
|
310
|
+
loadedItems.value = await props.loader()
|
|
311
|
+
} catch (_e) {
|
|
312
|
+
loadError.value = 'Failed to load items'
|
|
313
|
+
} finally {
|
|
314
|
+
loading.value = false
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
await nextTick()
|
|
318
|
+
searchInputRef.value?.focus()
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
function handleConfirm() {
|
|
322
|
+
if (!selectedId.value || props.submitting) return
|
|
323
|
+
emit('confirm', selectedId.value)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function handleCancel() {
|
|
327
|
+
if (props.submitting) return
|
|
328
|
+
emit('cancel')
|
|
329
|
+
}
|
|
330
|
+
</script>
|
|
@@ -5,22 +5,22 @@
|
|
|
5
5
|
</template>
|
|
6
6
|
|
|
7
7
|
<script lang="ts" setup>
|
|
8
|
-
import {
|
|
8
|
+
import { BaseLogoEnum } from "../enums/BaseLogoEnum";
|
|
9
9
|
|
|
10
10
|
const props = defineProps({
|
|
11
11
|
size: {
|
|
12
12
|
type: String,
|
|
13
13
|
required: false,
|
|
14
|
-
default:
|
|
14
|
+
default: BaseLogoEnum.MEDIUM
|
|
15
15
|
}
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
function computePx() {
|
|
19
19
|
let commonCss = ''
|
|
20
20
|
switch (props.size) {
|
|
21
|
-
case
|
|
22
|
-
case
|
|
23
|
-
case
|
|
21
|
+
case BaseLogoEnum.SMALL: { commonCss = '50px'; break; }
|
|
22
|
+
case BaseLogoEnum.MEDIUM: { commonCss = '100px'; break; }
|
|
23
|
+
case BaseLogoEnum.LARGE: { commonCss = '150px'; break; }
|
|
24
24
|
}
|
|
25
25
|
return commonCss;
|
|
26
26
|
}
|
|
@@ -1,75 +1,95 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="outside" @click="$emit('closeModal')"></div>
|
|
3
|
-
|
|
4
|
-
<div
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
</
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<div class="outside" @click="$emit('closeModal')"></div>
|
|
3
|
+
|
|
4
|
+
<div
|
|
5
|
+
class="dialog relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white"
|
|
6
|
+
role="dialog"
|
|
7
|
+
aria-modal="true"
|
|
8
|
+
:aria-labelledby="'modal-title-' + title"
|
|
9
|
+
>
|
|
10
|
+
|
|
11
|
+
<div v-if="mode === BaseModalEnum.DELETE" class="p-6 text-center">
|
|
12
|
+
<ExclamationCircleIcon class="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
13
|
+
|
|
14
|
+
<h3 :id="'modal-title-' + title" class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">
|
|
15
|
+
Are you sure you want to delete this {{ title }}?
|
|
16
|
+
</h3>
|
|
17
|
+
|
|
18
|
+
<div class="flex justify-center">
|
|
19
|
+
<base-button :color="BaseButtonEnum.RED" @click="$emit('confirmModal')" description="Yes, I'm sure"/>
|
|
20
|
+
<base-button :color="BaseButtonEnum.WHITE" @click="$emit('closeModal')" description="No, cancel"/>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script lang="ts" setup>
|
|
28
|
+
import { onMounted, onUnmounted } from 'vue';
|
|
29
|
+
import { BaseModalEnum } from "../enums/BaseModalEnum";
|
|
30
|
+
import { BaseButtonEnum } from "../enums/BaseButtonEnum";
|
|
31
|
+
import { ExclamationCircleIcon } from '@heroicons/vue/24/outline'
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits(['closeModal', 'confirmModal'])
|
|
34
|
+
|
|
35
|
+
const props = defineProps({
|
|
36
|
+
title: {
|
|
37
|
+
type: String,
|
|
38
|
+
required: true
|
|
39
|
+
},
|
|
40
|
+
description: {
|
|
41
|
+
type: String,
|
|
42
|
+
required: false
|
|
43
|
+
},
|
|
44
|
+
to: {
|
|
45
|
+
type: String,
|
|
46
|
+
required: false,
|
|
47
|
+
default: "/"
|
|
48
|
+
},
|
|
49
|
+
mode: {
|
|
50
|
+
type: String,
|
|
51
|
+
required: false,
|
|
52
|
+
default: BaseModalEnum.SUCCESS
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
57
|
+
if (e.key === 'Escape') {
|
|
58
|
+
emit('closeModal');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onMounted(() => {
|
|
63
|
+
document.addEventListener('keydown', handleKeydown);
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
onUnmounted(() => {
|
|
67
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
68
|
+
})
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<style lang="scss" scoped>
|
|
72
|
+
.outside {
|
|
73
|
+
position: fixed;
|
|
74
|
+
top: 0;
|
|
75
|
+
left: 0;
|
|
76
|
+
height: 100vh;
|
|
77
|
+
width: 100%;
|
|
78
|
+
background-color: rgba(0, 0, 0, 0.75);
|
|
79
|
+
z-index: 10;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dialog {
|
|
83
|
+
position: fixed;
|
|
84
|
+
top: 20vh;
|
|
85
|
+
left: 40%;
|
|
86
|
+
width: 20%;
|
|
87
|
+
z-index: 100;
|
|
88
|
+
border-radius: 12px;
|
|
89
|
+
border: none;
|
|
90
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
|
|
91
|
+
padding: 0;
|
|
92
|
+
margin: 0;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Shared modal shell — knows how a dialog is structured (teleport,
|
|
4
|
+
backdrop, dark-mode card, escape key, aria-modal). Concrete modals
|
|
5
|
+
should compose this rather than re-implementing the chrome.
|
|
6
|
+
|
|
7
|
+
Slots:
|
|
8
|
+
• icon — small circular icon shown next to the title
|
|
9
|
+
• default — body content
|
|
10
|
+
• footer — action buttons (cancel / confirm / etc.)
|
|
11
|
+
|
|
12
|
+
Backdrop click:
|
|
13
|
+
• Emits `backdrop`. By default this also fires `cancel` (the
|
|
14
|
+
common case). Parents that want to suppress this — e.g. a
|
|
15
|
+
modal with unsaved input — should pass `manual-close` and
|
|
16
|
+
listen on `backdrop` to decide what to do.
|
|
17
|
+
-->
|
|
18
|
+
<Teleport to="body">
|
|
19
|
+
<div
|
|
20
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
|
21
|
+
role="dialog"
|
|
22
|
+
aria-modal="true"
|
|
23
|
+
:aria-labelledby="titleId"
|
|
24
|
+
@click="onBackdrop"
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
class="relative w-full mx-4 rounded-lg shadow-xl"
|
|
28
|
+
:class="[maxWidthClass, shellClass]"
|
|
29
|
+
@click.stop
|
|
30
|
+
>
|
|
31
|
+
<div class="p-6">
|
|
32
|
+
<!-- Title row -->
|
|
33
|
+
<div class="flex items-start gap-4">
|
|
34
|
+
<slot name="icon" />
|
|
35
|
+
<div class="flex-1">
|
|
36
|
+
<h3
|
|
37
|
+
:id="titleId"
|
|
38
|
+
class="text-lg font-medium"
|
|
39
|
+
:class="t.primaryText"
|
|
40
|
+
>
|
|
41
|
+
{{ title }}
|
|
42
|
+
</h3>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Body -->
|
|
47
|
+
<div class="mt-4">
|
|
48
|
+
<slot />
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<!-- Footer -->
|
|
52
|
+
<div class="mt-6 flex gap-3 justify-end">
|
|
53
|
+
<slot name="footer" />
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</Teleport>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script setup lang="ts">
|
|
62
|
+
import { computed } from 'vue'
|
|
63
|
+
import { useTheme } from '../composables/useTheme'
|
|
64
|
+
import { useThemeClasses } from '../composables/useThemeClasses'
|
|
65
|
+
import { useEscapeKey } from '../composables/useEscapeKey'
|
|
66
|
+
|
|
67
|
+
interface Props {
|
|
68
|
+
title: string
|
|
69
|
+
/**
|
|
70
|
+
* Max-width of the dialog card. Defaults to `max-w-md`. Pass any
|
|
71
|
+
* Tailwind max-w utility, or empty string to opt out.
|
|
72
|
+
*/
|
|
73
|
+
maxWidthClass?: string
|
|
74
|
+
/**
|
|
75
|
+
* When true, the backdrop click and Escape key are NOT auto-bound
|
|
76
|
+
* to `cancel`. Use when the parent wants to gate dismissal on
|
|
77
|
+
* something (e.g. unsaved input). The parent should listen on
|
|
78
|
+
* `backdrop` / handle their own Escape.
|
|
79
|
+
*/
|
|
80
|
+
manualClose?: boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
84
|
+
maxWidthClass: 'max-w-md',
|
|
85
|
+
manualClose: false,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const emit = defineEmits<{
|
|
89
|
+
cancel: []
|
|
90
|
+
backdrop: []
|
|
91
|
+
}>()
|
|
92
|
+
|
|
93
|
+
const { isDark } = useTheme()
|
|
94
|
+
|
|
95
|
+
const t = useThemeClasses()
|
|
96
|
+
// Stable id derived per-instance so aria-labelledby points at this
|
|
97
|
+
// dialog's own heading. Suffix with a random short string to avoid
|
|
98
|
+
// collisions when two modals are momentarily stacked.
|
|
99
|
+
const titleId = `modal-title-${Math.random().toString(36).slice(2, 9)}`
|
|
100
|
+
|
|
101
|
+
const shellClass = computed(() =>
|
|
102
|
+
isDark.value
|
|
103
|
+
? 'bg-gray-900 border border-gray-700'
|
|
104
|
+
: 'bg-white border border-gray-200',
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
function onBackdrop() {
|
|
108
|
+
emit('backdrop')
|
|
109
|
+
if (!props.manualClose) emit('cancel')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
useEscapeKey(() => {
|
|
113
|
+
if (!props.manualClose) emit('cancel')
|
|
114
|
+
})
|
|
115
|
+
</script>
|