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
|
@@ -1,136 +1,137 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<ul>
|
|
4
|
-
<li v-for="cell in cells" :key="
|
|
5
|
-
<a v-if="cell!==0" @click="changePage(cell)" :class="{ active: cell === currentPage }">{{ cell }}</a>
|
|
6
|
-
<span v-if="cell===0" style="cursor: not-allowed;">...</span>
|
|
7
|
-
</li>
|
|
8
|
-
</ul>
|
|
9
|
-
</div>
|
|
10
|
-
</template>
|
|
11
|
-
|
|
12
|
-
<script lang="ts" setup>
|
|
13
|
-
import { ref, watch } from "vue";
|
|
14
|
-
|
|
15
|
-
const props = defineProps({
|
|
16
|
-
totalItems: {
|
|
17
|
-
type: Number,
|
|
18
|
-
required: true
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
cells.value.push(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
cells.value.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
cells.value.push(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
cells.value.push(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
border
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
border
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
color:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
color: #
|
|
135
|
-
|
|
136
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<ul>
|
|
4
|
+
<li v-for="(cell, index) in cells" :key="index">
|
|
5
|
+
<a v-if="cell!==0" role="button" tabindex="0" @click="changePage(cell)" @keydown.enter="changePage(cell)" :class="{ active: cell === currentPage }">{{ cell }}</a>
|
|
6
|
+
<span v-if="cell===0" style="cursor: not-allowed;">...</span>
|
|
7
|
+
</li>
|
|
8
|
+
</ul>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script lang="ts" setup>
|
|
13
|
+
import { ref, watch } from "vue";
|
|
14
|
+
|
|
15
|
+
const props = defineProps({
|
|
16
|
+
totalItems: {
|
|
17
|
+
type: Number,
|
|
18
|
+
required: true
|
|
19
|
+
},
|
|
20
|
+
itemsPerPage: {
|
|
21
|
+
type: Number,
|
|
22
|
+
default: 20
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const currentPage = ref(1);
|
|
27
|
+
const totalPages = ref(0);
|
|
28
|
+
const cells = ref([] as number[]);
|
|
29
|
+
const emit = defineEmits(['page-changed']);
|
|
30
|
+
|
|
31
|
+
function changePage(pageNumber: number) {
|
|
32
|
+
if (pageNumber !== currentPage.value && pageNumber >= 1 && pageNumber <= totalPages.value) {
|
|
33
|
+
currentPage.value = pageNumber;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (totalPages.value > 5) {
|
|
37
|
+
cells.value = [];
|
|
38
|
+
|
|
39
|
+
let isAwayFromStartPoint = (pageNumber - 3) > 0;
|
|
40
|
+
if (isAwayFromStartPoint) {
|
|
41
|
+
cells.value.push(1);
|
|
42
|
+
cells.value.push(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let i = 1; i <= 2; i++) {
|
|
46
|
+
let previousCell = pageNumber - i;
|
|
47
|
+
if (previousCell > 0) {
|
|
48
|
+
cells.value.push(previousCell);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Sort previous pages in ascending order
|
|
53
|
+
const currentIdx = cells.value.length;
|
|
54
|
+
cells.value.push(currentPage.value);
|
|
55
|
+
|
|
56
|
+
let nextPages = totalPages.value - pageNumber;
|
|
57
|
+
let hasNextPages = nextPages > 2;
|
|
58
|
+
let nextPagesSize = 2;
|
|
59
|
+
|
|
60
|
+
if (hasNextPages) {
|
|
61
|
+
for (let i = 1; i <= nextPagesSize; i++) {
|
|
62
|
+
cells.value.push(pageNumber + i);
|
|
63
|
+
}
|
|
64
|
+
cells.value.push(0);
|
|
65
|
+
cells.value.push(totalPages.value);
|
|
66
|
+
} else {
|
|
67
|
+
for (let i = 1; i <= nextPages; i++) {
|
|
68
|
+
cells.value.push(pageNumber + i);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
emit('page-changed', pageNumber);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
watch(() => props.totalItems, () => {
|
|
77
|
+
totalPages.value = Math.ceil(props.totalItems / props.itemsPerPage);
|
|
78
|
+
|
|
79
|
+
cells.value = [];
|
|
80
|
+
if (totalPages.value > 5) {
|
|
81
|
+
for (let i = 1; i < 5; i++) {
|
|
82
|
+
cells.value.push(i);
|
|
83
|
+
}
|
|
84
|
+
cells.value.push(0);
|
|
85
|
+
cells.value.push(totalPages.value);
|
|
86
|
+
} else {
|
|
87
|
+
for (let i = 1; i <= totalPages.value; i++) {
|
|
88
|
+
cells.value.push(i);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}, { immediate: true })
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<style lang="scss" scoped>
|
|
95
|
+
span {
|
|
96
|
+
border: 1px solid #d1d5db;
|
|
97
|
+
border-radius: 10px;
|
|
98
|
+
padding: 0.75rem;
|
|
99
|
+
text-decoration: none;
|
|
100
|
+
color: black;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ul {
|
|
104
|
+
display: flex;
|
|
105
|
+
list-style: none;
|
|
106
|
+
padding: 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
li {
|
|
110
|
+
margin-right: 3px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
a {
|
|
114
|
+
border: 1px solid #d1d5db;
|
|
115
|
+
border-radius: 10px;
|
|
116
|
+
padding: 0.75rem;
|
|
117
|
+
text-decoration: none;
|
|
118
|
+
color: black;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
a:hover {
|
|
122
|
+
background-color: rgb(195 197 201);
|
|
123
|
+
color: #000000;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
a.active:hover {
|
|
128
|
+
background-color: rgb(195 197 201);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
a.active {
|
|
132
|
+
font-weight: 700;
|
|
133
|
+
border-radius: 10px;
|
|
134
|
+
background-color: #e5e7eb;
|
|
135
|
+
color: #000000;
|
|
136
|
+
}
|
|
137
|
+
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="number">
|
|
2
|
+
<div v-if="number !== null && number !== undefined">
|
|
3
3
|
<span v-if="isNegative" class="flex items-center space-x-2">
|
|
4
4
|
<BaseBadge :color="ColorsEnums.RED">{{ number }}{{ props.icon }}</BaseBadge>
|
|
5
5
|
<ArrowDownIcon class="h-5 w-5 text-red-500" />
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ref, watch, onScopeDispose, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a debounced ref that updates after a delay.
|
|
5
|
+
* Useful for search inputs where you want instant UI feedback
|
|
6
|
+
* but debounced side-effects (URL sync, API calls, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Must be called within a component setup or active effect scope — the
|
|
9
|
+
* pending timer is cleared automatically when the scope is disposed.
|
|
10
|
+
*/
|
|
11
|
+
export function useDebouncedRef<T>(source: Ref<T>, delay = 300): Ref<T> {
|
|
12
|
+
const debounced = ref(source.value) as Ref<T>
|
|
13
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
14
|
+
|
|
15
|
+
watch(source, (val) => {
|
|
16
|
+
if (timeout) clearTimeout(timeout)
|
|
17
|
+
timeout = setTimeout(() => {
|
|
18
|
+
debounced.value = val
|
|
19
|
+
timeout = null
|
|
20
|
+
}, delay)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Clear any pending timer if the consuming scope is torn down before it fires.
|
|
24
|
+
onScopeDispose(() => {
|
|
25
|
+
if (timeout) {
|
|
26
|
+
clearTimeout(timeout)
|
|
27
|
+
timeout = null
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return debounced
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { onBeforeUnmount, onMounted } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wire up a callback to fire on Escape key press for the lifetime of the
|
|
5
|
+
* calling component. Listener is attached on mount and detached on unmount,
|
|
6
|
+
* so it's safe to use in modals that may mount/unmount many times.
|
|
7
|
+
*/
|
|
8
|
+
export function useEscapeKey(handler: () => void): void {
|
|
9
|
+
function onKeydown(e: KeyboardEvent) {
|
|
10
|
+
if (e.key === 'Escape') handler()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
onMounted(() => document.addEventListener('keydown', onKeydown))
|
|
14
|
+
onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared state for the responsive sidebar's open/closed status.
|
|
5
|
+
*
|
|
6
|
+
* Module-scoped singleton so the sidebar component and the page layout
|
|
7
|
+
* (which typically marks `<main>` as `inert` while the menu is open)
|
|
8
|
+
* stay in sync. There's only ever one sidebar at a time.
|
|
9
|
+
*/
|
|
10
|
+
const mobileOpen = ref(false)
|
|
11
|
+
|
|
12
|
+
export function useMobileSidebar() {
|
|
13
|
+
function open() {
|
|
14
|
+
mobileOpen.value = true
|
|
15
|
+
}
|
|
16
|
+
function close() {
|
|
17
|
+
mobileOpen.value = false
|
|
18
|
+
}
|
|
19
|
+
function toggle() {
|
|
20
|
+
mobileOpen.value = !mobileOpen.value
|
|
21
|
+
}
|
|
22
|
+
return { mobileOpen, open, close, toggle }
|
|
23
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ref, watch, effectScope } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared dark/light theme controller.
|
|
5
|
+
*
|
|
6
|
+
* Singleton ref + `<html class="dark">` sync + localStorage persistence.
|
|
7
|
+
* The first consumer can pass an options object to override the
|
|
8
|
+
* localStorage key (each consuming app should pick a unique key so two
|
|
9
|
+
* backoffices on the same origin don't fight). Defaults to `'mgv-theme'`.
|
|
10
|
+
*
|
|
11
|
+
* Subsequent callers get the same singleton; the options are only
|
|
12
|
+
* consulted on the first call.
|
|
13
|
+
*/
|
|
14
|
+
export interface UseThemeOptions {
|
|
15
|
+
/** localStorage key used to persist the preference. */
|
|
16
|
+
storageKey?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isDark = ref(false)
|
|
20
|
+
|
|
21
|
+
// Idempotent flag: initialization and the DOM-sync watcher should run once,
|
|
22
|
+
// and only when the composable is first invoked in a browser environment.
|
|
23
|
+
let initialized = false
|
|
24
|
+
let configuredStorageKey = 'mgv-theme'
|
|
25
|
+
|
|
26
|
+
function initialize(storageKey: string) {
|
|
27
|
+
if (initialized) return
|
|
28
|
+
initialized = true
|
|
29
|
+
configuredStorageKey = storageKey
|
|
30
|
+
|
|
31
|
+
// Read saved preference / system preference.
|
|
32
|
+
if (typeof window !== 'undefined') {
|
|
33
|
+
const saved = window.localStorage?.getItem(configuredStorageKey)
|
|
34
|
+
if (saved) {
|
|
35
|
+
isDark.value = saved === 'dark'
|
|
36
|
+
} else if (typeof window.matchMedia === 'function') {
|
|
37
|
+
isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Use a detached effect scope so the watcher lives for the app's lifetime
|
|
42
|
+
// and is NOT torn down when the first component that called useTheme() unmounts.
|
|
43
|
+
if (typeof document !== 'undefined') {
|
|
44
|
+
const scope = effectScope(true)
|
|
45
|
+
scope.run(() => {
|
|
46
|
+
watch(
|
|
47
|
+
isDark,
|
|
48
|
+
(value) => {
|
|
49
|
+
document.documentElement.classList.toggle('dark', value)
|
|
50
|
+
window.localStorage?.setItem(configuredStorageKey, value ? 'dark' : 'light')
|
|
51
|
+
},
|
|
52
|
+
{ immediate: true },
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function useTheme(options?: UseThemeOptions) {
|
|
59
|
+
initialize(options?.storageKey ?? configuredStorageKey)
|
|
60
|
+
|
|
61
|
+
function toggleTheme() {
|
|
62
|
+
isDark.value = !isDark.value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
isDark,
|
|
67
|
+
toggleTheme,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from 'vue'
|
|
2
|
+
import { useTheme } from './useTheme'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Centralized dark/light Tailwind class strings.
|
|
6
|
+
*
|
|
7
|
+
* Named "roles" that map to dark/light class strings, so consumers don't
|
|
8
|
+
* spell out `isDark ? '...' : '...'` ternaries across every view.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* const t = useThemeClasses()
|
|
13
|
+
* <div :class="t.card">
|
|
14
|
+
* <p :class="t.mutedText">...</p>
|
|
15
|
+
* </div>
|
|
16
|
+
*
|
|
17
|
+
* Vue auto-unwraps the computed refs inside template `:class` bindings,
|
|
18
|
+
* so callers do not write `.value`.
|
|
19
|
+
*/
|
|
20
|
+
export interface ThemeClasses {
|
|
21
|
+
// -- Surfaces --
|
|
22
|
+
/** Card surface: bg-gray-800 / bg-white, with border. */
|
|
23
|
+
card: ComputedRef<string>
|
|
24
|
+
/** Slightly darker card surface: bg-gray-900 / bg-white, with border. */
|
|
25
|
+
cardAlt: ComputedRef<string>
|
|
26
|
+
/** Page background. */
|
|
27
|
+
pageBg: ComputedRef<string>
|
|
28
|
+
|
|
29
|
+
// -- Borders --
|
|
30
|
+
/** Standard border between cards/sections. */
|
|
31
|
+
border: ComputedRef<string>
|
|
32
|
+
/** Subtle divider (lighter than border). */
|
|
33
|
+
divider: ComputedRef<string>
|
|
34
|
+
|
|
35
|
+
// -- Text hierarchy --
|
|
36
|
+
/** Page headings: white / gray-900. */
|
|
37
|
+
primaryText: ComputedRef<string>
|
|
38
|
+
/** Softer headings: gray-100 / gray-800. */
|
|
39
|
+
primaryTextSoft: ComputedRef<string>
|
|
40
|
+
/** Body copy on cards: gray-200 / gray-700. */
|
|
41
|
+
bodyText: ComputedRef<string>
|
|
42
|
+
/** Form labels: gray-300 / gray-700. */
|
|
43
|
+
label: ComputedRef<string>
|
|
44
|
+
/** Secondary text: gray-400 / gray-600. */
|
|
45
|
+
mutedText: ComputedRef<string>
|
|
46
|
+
/** Tertiary text: gray-500 / gray-600. */
|
|
47
|
+
subtleText: ComputedRef<string>
|
|
48
|
+
/** Dimmest text: gray-500 in dark / gray-400 in light. */
|
|
49
|
+
dimText: ComputedRef<string>
|
|
50
|
+
/** Slightly more contrast than dimText: gray-400 / gray-500. */
|
|
51
|
+
dimTextAlt: ComputedRef<string>
|
|
52
|
+
/** Big illustration / empty-state icon: gray-600 / gray-300. */
|
|
53
|
+
illustration: ComputedRef<string>
|
|
54
|
+
|
|
55
|
+
// -- Inputs --
|
|
56
|
+
/** Standard text input. */
|
|
57
|
+
input: ComputedRef<string>
|
|
58
|
+
/** Input + placeholder colour. */
|
|
59
|
+
inputWithPlaceholder: ComputedRef<string>
|
|
60
|
+
|
|
61
|
+
// -- Buttons --
|
|
62
|
+
/** Ghost / Cancel button. */
|
|
63
|
+
ghostButton: ComputedRef<string>
|
|
64
|
+
|
|
65
|
+
// -- Accent text colours --
|
|
66
|
+
/** Emerald accent text: emerald-400 / emerald-600. */
|
|
67
|
+
emeraldText: ComputedRef<string>
|
|
68
|
+
/** Amber accent text: amber-400 / amber-600. */
|
|
69
|
+
amberText: ComputedRef<string>
|
|
70
|
+
/** Amber heading-weight: amber-300 / amber-700. */
|
|
71
|
+
amberTextStrong: ComputedRef<string>
|
|
72
|
+
/** Red accent text: red-400 / red-500. */
|
|
73
|
+
redText: ComputedRef<string>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function useThemeClasses(): ThemeClasses {
|
|
77
|
+
const { isDark } = useTheme()
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
card: computed(() =>
|
|
81
|
+
isDark.value
|
|
82
|
+
? 'bg-gray-800 border-gray-700'
|
|
83
|
+
: 'bg-white border-gray-200',
|
|
84
|
+
),
|
|
85
|
+
cardAlt: computed(() =>
|
|
86
|
+
isDark.value
|
|
87
|
+
? 'bg-gray-900 border-gray-700'
|
|
88
|
+
: 'bg-white border-gray-200',
|
|
89
|
+
),
|
|
90
|
+
pageBg: computed(() => (isDark.value ? 'bg-gray-900' : 'bg-gray-50')),
|
|
91
|
+
|
|
92
|
+
border: computed(() => (isDark.value ? 'border-gray-700' : 'border-gray-200')),
|
|
93
|
+
divider: computed(() => (isDark.value ? 'border-gray-800' : 'border-gray-100')),
|
|
94
|
+
|
|
95
|
+
primaryText: computed(() => (isDark.value ? 'text-white' : 'text-gray-900')),
|
|
96
|
+
primaryTextSoft: computed(() => (isDark.value ? 'text-gray-100' : 'text-gray-800')),
|
|
97
|
+
bodyText: computed(() => (isDark.value ? 'text-gray-200' : 'text-gray-700')),
|
|
98
|
+
label: computed(() => (isDark.value ? 'text-gray-300' : 'text-gray-700')),
|
|
99
|
+
mutedText: computed(() => (isDark.value ? 'text-gray-400' : 'text-gray-600')),
|
|
100
|
+
subtleText: computed(() => (isDark.value ? 'text-gray-500' : 'text-gray-600')),
|
|
101
|
+
dimText: computed(() => (isDark.value ? 'text-gray-500' : 'text-gray-400')),
|
|
102
|
+
dimTextAlt: computed(() => (isDark.value ? 'text-gray-400' : 'text-gray-500')),
|
|
103
|
+
illustration: computed(() => (isDark.value ? 'text-gray-600' : 'text-gray-300')),
|
|
104
|
+
|
|
105
|
+
input: computed(() =>
|
|
106
|
+
isDark.value
|
|
107
|
+
? 'bg-gray-700 border-gray-600 text-white'
|
|
108
|
+
: 'bg-gray-50 border-gray-300 text-gray-900',
|
|
109
|
+
),
|
|
110
|
+
inputWithPlaceholder: computed(() =>
|
|
111
|
+
isDark.value
|
|
112
|
+
? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400'
|
|
113
|
+
: 'bg-gray-50 border-gray-300 text-gray-900 placeholder-gray-500',
|
|
114
|
+
),
|
|
115
|
+
|
|
116
|
+
ghostButton: computed(() =>
|
|
117
|
+
isDark.value
|
|
118
|
+
? 'bg-gray-800 text-gray-200 border-gray-600 hover:bg-gray-700'
|
|
119
|
+
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50',
|
|
120
|
+
),
|
|
121
|
+
|
|
122
|
+
emeraldText: computed(() =>
|
|
123
|
+
isDark.value ? 'text-emerald-400' : 'text-emerald-600',
|
|
124
|
+
),
|
|
125
|
+
amberText: computed(() =>
|
|
126
|
+
isDark.value ? 'text-amber-400' : 'text-amber-600',
|
|
127
|
+
),
|
|
128
|
+
amberTextStrong: computed(() =>
|
|
129
|
+
isDark.value ? 'text-amber-300' : 'text-amber-700',
|
|
130
|
+
),
|
|
131
|
+
redText: computed(() => (isDark.value ? 'text-red-400' : 'text-red-500')),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ref, onScopeDispose } from 'vue'
|
|
2
|
+
import { BaseToastEnum } from '../enums/BaseToastEnum'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-component toast controller.
|
|
6
|
+
*
|
|
7
|
+
* Each component gets its own state refs (not a global singleton) so
|
|
8
|
+
* multiple mounted views don't fight over the same toast slot. The pending
|
|
9
|
+
* auto-hide timer is cleared on unmount so it cannot fire after the
|
|
10
|
+
* component is gone.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const { showToast, toastMessage, toastType, showToastMessage } = useToast()
|
|
14
|
+
* showToastMessage('Saved', BaseToastEnum.SUCCESS)
|
|
15
|
+
*/
|
|
16
|
+
export function useToast(defaultDurationMs = 3000) {
|
|
17
|
+
const showToast = ref(false)
|
|
18
|
+
const toastMessage = ref('')
|
|
19
|
+
const toastType = ref<BaseToastEnum>(BaseToastEnum.SUCCESS)
|
|
20
|
+
|
|
21
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
22
|
+
|
|
23
|
+
function clearTimer() {
|
|
24
|
+
if (timer) {
|
|
25
|
+
clearTimeout(timer)
|
|
26
|
+
timer = null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function showToastMessage(
|
|
31
|
+
message: string,
|
|
32
|
+
type: BaseToastEnum = BaseToastEnum.SUCCESS,
|
|
33
|
+
durationMs: number = defaultDurationMs,
|
|
34
|
+
) {
|
|
35
|
+
toastMessage.value = message
|
|
36
|
+
toastType.value = type
|
|
37
|
+
showToast.value = true
|
|
38
|
+
|
|
39
|
+
clearTimer()
|
|
40
|
+
timer = setTimeout(() => {
|
|
41
|
+
showToast.value = false
|
|
42
|
+
timer = null
|
|
43
|
+
}, durationMs)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onScopeDispose(() => {
|
|
47
|
+
clearTimer()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
showToast,
|
|
52
|
+
toastMessage,
|
|
53
|
+
toastType,
|
|
54
|
+
showToastMessage,
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/enums/AlertEnum.ts
CHANGED