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.
Files changed (83) hide show
  1. package/README.md +735 -70
  2. package/dist/index.d.ts +1 -27
  3. package/dist/{components → src/components}/BaseAlert.vue.d.ts +2 -2
  4. package/dist/src/components/BaseAppLayout.vue.d.ts +31 -0
  5. package/dist/{components → src/components}/BaseButton.vue.d.ts +5 -2
  6. package/dist/src/components/BaseCollapsibleSection.vue.d.ts +35 -0
  7. package/dist/src/components/BaseConfirmModal.vue.d.ts +23 -0
  8. package/dist/src/components/BaseEntityPickerModal.vue.d.ts +58 -0
  9. package/dist/{components → src/components}/BaseLogo.vue.d.ts +3 -3
  10. package/dist/{components → src/components}/BaseModal.vue.d.ts +4 -3
  11. package/dist/src/components/BaseModalShell.vue.d.ts +43 -0
  12. package/dist/src/components/BaseNotFoundPage.vue.d.ts +17 -0
  13. package/dist/src/components/BaseSidebar.vue.d.ts +46 -0
  14. package/dist/src/components/BaseTextInputModal.vue.d.ts +46 -0
  15. package/dist/{components → src/components}/BaseToast.vue.d.ts +14 -2
  16. package/dist/{components → src/components}/Pagination.vue.d.ts +0 -3
  17. package/dist/src/composables/useDebounce.d.ts +10 -0
  18. package/dist/src/composables/useEscapeKey.d.ts +6 -0
  19. package/dist/src/composables/useMobileSidebar.d.ts +6 -0
  20. package/dist/src/composables/useTheme.d.ts +19 -0
  21. package/dist/src/composables/useThemeClasses.d.ts +62 -0
  22. package/dist/src/composables/useToast.d.ts +19 -0
  23. package/dist/{enums → src/enums}/AlertEnum.d.ts +1 -1
  24. package/dist/{enums → src/enums}/BaseButtonSizeEnum.d.ts +1 -1
  25. package/dist/src/enums/BaseLogoEnum.d.ts +7 -0
  26. package/dist/src/index.d.ts +46 -0
  27. package/dist/src/types/entityPicker.d.ts +11 -0
  28. package/dist/src/types/sidebar.d.ts +22 -0
  29. package/dist/src/utils/httpColors.d.ts +19 -0
  30. package/dist/ui-lib.css +3 -0
  31. package/dist/ui-lib.js +1624 -1011
  32. package/dist/ui-lib.umd.cjs +1 -1
  33. package/package.json +14 -14
  34. package/src/components/BaseAlert.vue +2 -2
  35. package/src/components/BaseAppLayout.vue +65 -0
  36. package/src/components/BaseBreadcrumb.vue +2 -1
  37. package/src/components/BaseButton.vue +152 -132
  38. package/src/components/BaseCollapsibleSection.vue +107 -0
  39. package/src/components/BaseConfirmModal.vue +110 -0
  40. package/src/components/BaseEntityPickerModal.vue +330 -0
  41. package/src/components/BaseLogo.vue +5 -5
  42. package/src/components/BaseModal.vue +95 -75
  43. package/src/components/BaseModalShell.vue +115 -0
  44. package/src/components/BaseNotFoundPage.vue +51 -0
  45. package/src/components/BaseSidebar.vue +223 -0
  46. package/src/components/BaseTextInputModal.vue +144 -0
  47. package/src/components/BaseToast.vue +41 -20
  48. package/src/components/ColoredSquares.vue +2 -4
  49. package/src/components/Pagination.vue +137 -136
  50. package/src/components/TrendArrow.vue +1 -1
  51. package/src/composables/useDebounce.ts +32 -0
  52. package/src/composables/useEscapeKey.ts +15 -0
  53. package/src/composables/useMobileSidebar.ts +23 -0
  54. package/src/composables/useTheme.ts +69 -0
  55. package/src/composables/useThemeClasses.ts +133 -0
  56. package/src/composables/useToast.ts +56 -0
  57. package/src/enums/AlertEnum.ts +1 -1
  58. package/src/enums/BaseButtonSizeEnum.ts +1 -1
  59. package/src/enums/BaseLogoEnum.ts +4 -1
  60. package/src/index.ts +64 -36
  61. package/src/types/entityPicker.ts +11 -0
  62. package/src/types/sidebar.ts +24 -0
  63. package/src/utils/httpColors.ts +68 -0
  64. package/src/utils/util.ts +61 -53
  65. package/dist/enums/BaseLogoEnum.d.ts +0 -5
  66. package/dist/style.css +0 -1
  67. /package/dist/{components → src/components}/BaseBadge.vue.d.ts +0 -0
  68. /package/dist/{components → src/components}/BaseBreadcrumb.vue.d.ts +0 -0
  69. /package/dist/{components → src/components}/BaseLine.vue.d.ts +0 -0
  70. /package/dist/{components → src/components}/BaseRow.vue.d.ts +0 -0
  71. /package/dist/{components → src/components}/BaseSpinner.vue.d.ts +0 -0
  72. /package/dist/{components → src/components}/ColoredSquares.vue.d.ts +0 -0
  73. /package/dist/{components → src/components}/EarningsCard.vue.d.ts +0 -0
  74. /package/dist/{components → src/components}/EuroAmount.vue.d.ts +0 -0
  75. /package/dist/{components → src/components}/TrendArrow.vue.d.ts +0 -0
  76. /package/dist/{enums → src/enums}/BaseBadgeEnum.d.ts +0 -0
  77. /package/dist/{enums → src/enums}/BaseButtonEnum.d.ts +0 -0
  78. /package/dist/{enums → src/enums}/BaseModalEnum.d.ts +0 -0
  79. /package/dist/{enums → src/enums}/BaseToastEnum.d.ts +0 -0
  80. /package/dist/{enums → src/enums}/ColorsEnums.d.ts +0 -0
  81. /package/dist/{enums → src/enums}/LineEnum.d.ts +0 -0
  82. /package/dist/{enums → src/enums}/PositioningEnum.d.ts +0 -0
  83. /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 { BaseLoginEnum } from "../enums/BaseLogoEnum";
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: BaseLoginEnum.MEDIUM
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 BaseLoginEnum.SMALL: { commonCss = '50px'; break; }
22
- case BaseLoginEnum.MEDIUM: { commonCss = '100px'; break; }
23
- case BaseLoginEnum.LARGE: { commonCss = '150px'; break; }
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 class="dialog relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
5
-
6
- <div v-if="mode === BaseModalEnum.DELETE" class="p-6 text-center">
7
- <ExclamationCircleIcon class="w-12 h-12 text-gray-400 mx-auto mb-4" />
8
-
9
- <h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">
10
- Are you sure you want to delete this {{ title }}?
11
- </h3>
12
-
13
- <div class="flex justify-center">
14
- <base-button :color="BaseButtonEnum.RED" @click="$emit('confirmModal')" description="Yes, I'm sure"/>
15
- <base-button :color="BaseButtonEnum.WHITE" @click="$emit('closeModal')" description="No, cancel"/>
16
- </div>
17
- </div>
18
-
19
- </div>
20
- </template>
21
-
22
- <script lang="ts" setup>
23
- import { BaseModalEnum } from "../enums/BaseModalEnum";
24
- import { BaseButtonEnum } from "../enums/BaseButtonEnum";
25
- import { ExclamationCircleIcon } from '@heroicons/vue/24/outline'
26
-
27
- defineEmits(['closeModal', 'confirmModal'])
28
-
29
- const props = defineProps({
30
- title: {
31
- type: String,
32
- required: true
33
- },
34
- description: {
35
- type: String,
36
- required: false
37
- },
38
- to: {
39
- type: String,
40
- required: false,
41
- default: "/"
42
- },
43
- mode: {
44
- type: String,
45
- required: false,
46
- default: 'SUCCESS'
47
- }
48
- });
49
- </script>
50
-
51
- <style lang="scss" scoped>
52
- .outside {
53
- position: fixed;
54
- top: 0;
55
- left: 0;
56
- height: 100vh;
57
- width: 100%;
58
- background-color: rgba(0, 0, 0, 0.75);
59
- z-index: 10;
60
- }
61
-
62
- .dialog {
63
- position: fixed;
64
- top: 20vh;
65
- left: 40%;
66
- width: 20%;
67
- z-index: 100;
68
- border-radius: 12px;
69
- border: none;
70
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
71
- padding: 0;
72
- margin: 0;
73
- overflow: hidden;
74
- }
75
- </style>
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>