mgv-backoffice 1.48.0 → 1.49.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 +17 -0
- package/dist/components/BaseBooleanIcon.vue.d.ts +14 -0
- package/dist/components/BaseDetailList.vue.d.ts +13 -1
- package/dist/components/BasePhotoViewer.vue.d.ts +24 -0
- package/dist/components/BaseRating.vue.d.ts +9 -0
- package/dist/components/BaseRowButton.vue.d.ts +18 -0
- package/dist/components/BaseSelect.vue.d.ts +15 -3
- package/dist/components/BaseSidebarUser.vue.d.ts +3 -0
- package/dist/components/BaseStatCard.vue.d.ts +6 -4
- package/dist/components/BaseTextarea.vue.d.ts +12 -0
- package/dist/composables/useRefreshOnReturn.d.ts +7 -0
- package/dist/index.d.ts +8 -1
- package/dist/types/photoViewer.d.ts +6 -0
- package/dist/ui-lib.css +1 -1
- package/dist/ui-lib.js +1018 -819
- package/dist/ui-lib.umd.cjs +1 -1
- package/dist/utils/format.d.ts +9 -0
- package/package.json +1 -1
- package/src/components/BaseBadge.vue +7 -7
- package/src/components/BaseBooleanIcon.vue +22 -0
- package/src/components/BaseDetailList.vue +50 -47
- package/src/components/BaseField.label.test.ts +30 -0
- package/src/components/BaseField.vue +19 -3
- package/src/components/BasePhotoViewer.test.ts +22 -0
- package/src/components/BasePhotoViewer.vue +93 -0
- package/src/components/BaseRating.test.ts +19 -0
- package/src/components/BaseRating.vue +21 -0
- package/src/components/BaseRowButton.vue +24 -0
- package/src/components/BaseSelect.vue +4 -2
- package/src/components/BaseSidebarUser.vue +85 -75
- package/src/components/BaseStatCard.vue +11 -3
- package/src/components/BaseTextarea.test.ts +17 -0
- package/src/components/BaseTextarea.vue +24 -0
- package/src/composables/useRefreshOnReturn.test.ts +47 -0
- package/src/composables/useRefreshOnReturn.ts +24 -0
- package/src/index.ts +10 -0
- package/src/types/photoViewer.ts +6 -0
- package/src/utils/format.test.ts +342 -322
- package/src/utils/format.ts +23 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
{{ title }}
|
|
12
12
|
</p>
|
|
13
13
|
<p class="mt-1 truncate text-3xl font-bold" :class="cls.value">
|
|
14
|
-
{{ value }}
|
|
14
|
+
<slot name="value">{{ value }}</slot>
|
|
15
15
|
</p>
|
|
16
16
|
<p v-if="subtitle" class="mt-1 text-sm font-medium" :class="cls.subtitle">
|
|
17
17
|
{{ subtitle }}
|
|
@@ -35,13 +35,14 @@
|
|
|
35
35
|
* bold title, big value, accent subtitle, tinted icon chip) but for any
|
|
36
36
|
* value — counts, labels, pre-formatted numbers. Pass `to` to make the whole
|
|
37
37
|
* card a router link. The `icon` slot fills the chip (receives `iconClass`);
|
|
38
|
-
* the `
|
|
38
|
+
* the `value` slot replaces the value text (e.g. <EuroAmount>); the `default`
|
|
39
|
+
* slot adds extra content under the subtitle.
|
|
39
40
|
*
|
|
40
41
|
*/
|
|
41
42
|
import { computed } from 'vue'
|
|
42
43
|
import { RouterLink, type RouteLocationRaw } from 'vue-router'
|
|
43
44
|
|
|
44
|
-
type Accent = 'emerald' | 'sky' | 'amber' | 'red' | 'slate'
|
|
45
|
+
type Accent = 'emerald' | 'sky' | 'amber' | 'violet' | 'red' | 'slate'
|
|
45
46
|
|
|
46
47
|
interface Props {
|
|
47
48
|
title: string
|
|
@@ -69,6 +70,13 @@ const ACCENTS: Record<Accent, { border: string; value: string; subtitle: string;
|
|
|
69
70
|
iconBox: 'bg-emerald-50 dark:bg-emerald-900/30',
|
|
70
71
|
icon: 'text-emerald-500',
|
|
71
72
|
},
|
|
73
|
+
violet: {
|
|
74
|
+
border: 'border-violet-300 dark:border-violet-700',
|
|
75
|
+
value: 'text-violet-500 dark:text-violet-400',
|
|
76
|
+
subtitle: 'text-violet-500 dark:text-violet-400',
|
|
77
|
+
iconBox: 'bg-violet-50 dark:bg-violet-900/30',
|
|
78
|
+
icon: 'text-violet-500',
|
|
79
|
+
},
|
|
72
80
|
sky: {
|
|
73
81
|
border: 'border-sky-300 dark:border-sky-700',
|
|
74
82
|
value: 'text-sky-500 dark:text-sky-400',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import BaseTextarea from './BaseTextarea.vue'
|
|
4
|
+
|
|
5
|
+
describe('BaseTextarea', () => {
|
|
6
|
+
it('emits the typed value and passes attrs through', async () => {
|
|
7
|
+
const wrapper = mount(BaseTextarea, { props: { modelValue: 'a' }, attrs: { rows: '3' } })
|
|
8
|
+
expect(wrapper.attributes('rows')).toBe('3')
|
|
9
|
+
await wrapper.setValue('ab')
|
|
10
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['ab'])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('renders null as empty', () => {
|
|
14
|
+
const wrapper = mount(BaseTextarea, { props: { modelValue: null } })
|
|
15
|
+
expect((wrapper.element as HTMLTextAreaElement).value).toBe('')
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<textarea
|
|
3
|
+
:value="modelValue ?? ''"
|
|
4
|
+
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 text-sm px-3 py-2 placeholder:text-slate-400 dark:placeholder:text-slate-500 transition-colors hover:border-slate-400 dark:hover:border-slate-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/40 focus-visible:border-emerald-500 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
5
|
+
@input="onInput"
|
|
6
|
+
/>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
// Themed multi-line input, sharing BaseInput's field skin. rows, maxlength, placeholder, required and extra classes fall through via attrs.
|
|
11
|
+
interface Props {
|
|
12
|
+
modelValue?: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
withDefaults(defineProps<Props>(), { modelValue: '' })
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
(e: 'update:modelValue', value: string): void
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
function onInput(e: Event) {
|
|
22
|
+
emit('update:modelValue', (e.target as HTMLTextAreaElement).value)
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createApp, defineComponent, h } from 'vue'
|
|
3
|
+
import { REFRESH_AFTER_AWAY_MS, useRefreshOnReturn } from './useRefreshOnReturn'
|
|
4
|
+
|
|
5
|
+
function setVisibility(state: 'hidden' | 'visible') {
|
|
6
|
+
Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => state })
|
|
7
|
+
document.dispatchEvent(new Event('visibilitychange'))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('useRefreshOnReturn', () => {
|
|
11
|
+
afterEach(() => vi.useRealTimers())
|
|
12
|
+
|
|
13
|
+
function mountWith(refresh: () => void) {
|
|
14
|
+
const app = createApp(
|
|
15
|
+
defineComponent({
|
|
16
|
+
setup() {
|
|
17
|
+
useRefreshOnReturn(refresh)
|
|
18
|
+
return () => h('div')
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
)
|
|
22
|
+
app.mount(document.createElement('div'))
|
|
23
|
+
return app
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
it('refreshes after a long absence', () => {
|
|
27
|
+
vi.useFakeTimers()
|
|
28
|
+
const refresh = vi.fn()
|
|
29
|
+
const wrapper = mountWith(refresh)
|
|
30
|
+
setVisibility('hidden')
|
|
31
|
+
vi.advanceTimersByTime(REFRESH_AFTER_AWAY_MS)
|
|
32
|
+
setVisibility('visible')
|
|
33
|
+
expect(refresh).toHaveBeenCalledOnce()
|
|
34
|
+
wrapper.unmount()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('keeps the page after a quick switch', () => {
|
|
38
|
+
vi.useFakeTimers()
|
|
39
|
+
const refresh = vi.fn()
|
|
40
|
+
const wrapper = mountWith(refresh)
|
|
41
|
+
setVisibility('hidden')
|
|
42
|
+
vi.advanceTimersByTime(REFRESH_AFTER_AWAY_MS - 1)
|
|
43
|
+
setVisibility('visible')
|
|
44
|
+
expect(refresh).not.toHaveBeenCalled()
|
|
45
|
+
wrapper.unmount()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { onBeforeUnmount, onMounted } from 'vue'
|
|
2
|
+
|
|
3
|
+
/** How long the tab must be hidden before coming back reloads the page's data; quick switches keep it. */
|
|
4
|
+
export const REFRESH_AFTER_AWAY_MS = 30_000
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calls `refresh` when the browser tab becomes visible again after `afterMs` or more hidden, since server data
|
|
8
|
+
* changes while the user is elsewhere. The first load is left to the component.
|
|
9
|
+
*/
|
|
10
|
+
export function useRefreshOnReturn(refresh: () => unknown, afterMs = REFRESH_AFTER_AWAY_MS): void {
|
|
11
|
+
let hiddenAt: number | null = null
|
|
12
|
+
|
|
13
|
+
function onVisibilityChange() {
|
|
14
|
+
if (document.visibilityState === 'hidden') {
|
|
15
|
+
hiddenAt ??= Date.now()
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
if (hiddenAt !== null && Date.now() - hiddenAt >= afterMs) void refresh()
|
|
19
|
+
hiddenAt = null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onMounted(() => document.addEventListener('visibilitychange', onVisibilityChange))
|
|
23
|
+
onBeforeUnmount(() => document.removeEventListener('visibilitychange', onVisibilityChange))
|
|
24
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,13 @@ export { default as BaseSidebarUser } from './components/BaseSidebarUser.vue'
|
|
|
68
68
|
export { default as BaseAuthLayout } from './components/BaseAuthLayout.vue'
|
|
69
69
|
export { default as BaseBooleanBadge } from './components/BaseBooleanBadge.vue'
|
|
70
70
|
export { default as BaseDetailList } from './components/BaseDetailList.vue'
|
|
71
|
+
export { default as BaseTextarea } from './components/BaseTextarea.vue'
|
|
72
|
+
export { default as BaseRating } from './components/BaseRating.vue'
|
|
73
|
+
export { default as BaseRowButton } from './components/BaseRowButton.vue'
|
|
74
|
+
export { default as BaseBooleanIcon } from './components/BaseBooleanIcon.vue'
|
|
75
|
+
export { default as BasePhotoViewer } from './components/BasePhotoViewer.vue'
|
|
76
|
+
export type { ViewerPhoto } from './types/photoViewer'
|
|
77
|
+
export { useRefreshOnReturn, REFRESH_AFTER_AWAY_MS } from './composables/useRefreshOnReturn'
|
|
71
78
|
export type { DetailItem } from './types/detail'
|
|
72
79
|
|
|
73
80
|
// Composables
|
|
@@ -149,6 +156,9 @@ export {
|
|
|
149
156
|
fmtCurrencyAmount,
|
|
150
157
|
fmtDuration,
|
|
151
158
|
formatJson,
|
|
159
|
+
fmtTime,
|
|
160
|
+
toLocalInput,
|
|
161
|
+
fromLocalInput,
|
|
152
162
|
stringifyValue,
|
|
153
163
|
} from './utils/format'
|
|
154
164
|
export {
|