mgv-backoffice 1.36.0 → 1.36.2
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 +1794 -1719
- package/dist/components/BaseBadge.vue.d.ts +1 -11
- package/dist/components/BaseDropdown.vue.d.ts +1 -1
- package/dist/ui-lib.js +942 -925
- package/dist/ui-lib.umd.cjs +1 -1
- package/dist/utils/specForm.d.ts +1 -1
- package/package.json +54 -54
- package/src/components/BaseAlert.vue +104 -104
- package/src/components/BaseBadge.vue +12 -2
- package/src/components/BaseBarDistribution.test.ts +51 -51
- package/src/components/BaseBarDistribution.vue +73 -73
- package/src/components/BaseChipButton.vue +42 -42
- package/src/components/BaseCodeBlock.test.ts +70 -70
- package/src/components/BaseCodeBlock.vue +74 -74
- package/src/components/BaseCollapsibleSection.vue +113 -113
- package/src/components/BaseConfirmModal.vue +114 -114
- package/src/components/BaseCredentialsForm.test.ts +108 -108
- package/src/components/BaseCredentialsForm.vue +241 -241
- package/src/components/BaseDropdown.test.ts +87 -87
- package/src/components/BaseDropdown.vue +172 -172
- package/src/components/BaseEntityPickerModal.vue +330 -330
- package/src/components/BaseFileDropzone.vue +94 -94
- package/src/components/BaseFilterChip.test.ts +44 -44
- package/src/components/BaseFilterChip.vue +67 -67
- package/src/components/BaseModalShell.vue +209 -209
- package/src/components/BasePageHeader.vue +120 -120
- package/src/components/BasePillPickerModal.test.ts +95 -95
- package/src/components/BaseRemoveButton.vue +22 -22
- package/src/components/BaseSpecFields.test.ts +39 -0
- package/src/components/BaseSpecFields.vue +27 -3
- package/src/components/BaseStatBreakdown.test.ts +56 -56
- package/src/components/BaseStatBreakdown.vue +49 -49
- package/src/components/BaseStatusPill.vue +54 -54
- package/src/components/BaseTextInputModal.vue +140 -140
- package/src/components/BaseToolbarButton.test.ts +48 -48
- package/src/components/BaseToolbarButton.vue +94 -94
- package/src/composables/useFieldClasses.test.ts +55 -55
- package/src/composables/useFieldClasses.ts +56 -56
- package/src/composables/usePolling.test.ts +112 -112
- package/src/composables/usePolling.ts +102 -102
- package/src/composables/useQueryParamSync.test.ts +56 -56
- package/src/composables/useQueryParamSync.ts +42 -42
- package/src/composables/useThemeClasses.test.ts +34 -34
- package/src/composables/useThemeClasses.ts +137 -137
- package/src/index.ts +138 -138
- package/src/style.css +32 -32
- package/src/types/distribution.ts +7 -7
- package/src/types/pillPicker.ts +14 -14
- package/src/types/specField.ts +30 -30
- package/src/types/statBreakdown.ts +6 -6
- package/src/utils/format.test.ts +264 -264
- package/src/utils/format.ts +216 -216
- package/src/utils/httpColors.test.ts +100 -100
- package/src/utils/httpColors.ts +99 -99
- package/src/utils/kvRows.test.ts +47 -47
- package/src/utils/kvRows.ts +34 -34
- package/src/utils/specForm.test.ts +64 -53
- package/src/utils/specForm.ts +49 -48
- package/src/utils/validate.test.ts +82 -82
- package/src/utils/validate.ts +76 -76
|
@@ -1,172 +1,172 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!--
|
|
3
|
-
Button-style single-select dropdown ("Select Social User ⌄"). Unlike
|
|
4
|
-
BaseSelect (a themed native <select>), this renders a trigger button plus
|
|
5
|
-
a floating menu, so the closed control shows a placeholder and a chevron
|
|
6
|
-
that rotates while open — matching the app's filter dropdowns.
|
|
7
|
-
|
|
8
|
-
Options are passed as data (not <option> slots) so the menu rows can carry
|
|
9
|
-
tooltips and disabled state. Selecting a row emits its `value` and closes
|
|
10
|
-
the menu; Escape and an outside click also close it.
|
|
11
|
-
|
|
12
|
-
Same `dark:`-variant note as BaseSelect: the slate skin ships static
|
|
13
|
-
light/dark classes so consumers get both themes without extra config.
|
|
14
|
-
-->
|
|
15
|
-
<div
|
|
16
|
-
ref="rootEl"
|
|
17
|
-
:class="block ? 'relative w-full' : 'relative inline-block'"
|
|
18
|
-
>
|
|
19
|
-
<button
|
|
20
|
-
type="button"
|
|
21
|
-
:class="[triggerClass, block ? 'w-full' : '']"
|
|
22
|
-
:disabled="disabled"
|
|
23
|
-
:aria-label="ariaLabel || undefined"
|
|
24
|
-
aria-haspopup="listbox"
|
|
25
|
-
:aria-expanded="open"
|
|
26
|
-
@click="toggle"
|
|
27
|
-
>
|
|
28
|
-
<!-- min-w-0 + truncate so long labels ellipsize inside narrow
|
|
29
|
-
(fixed-column) triggers instead of wrapping or widening them. -->
|
|
30
|
-
<span class="min-w-0 truncate" :class="selectedOption ? '' : 'text-slate-400 dark:text-slate-500'">
|
|
31
|
-
{{ selectedOption ? selectedOption.label : placeholder }}
|
|
32
|
-
</span>
|
|
33
|
-
<ChevronDownIcon
|
|
34
|
-
class="shrink-0 transition-transform"
|
|
35
|
-
:class="[chevronClass, open ? 'rotate-180' : '']"
|
|
36
|
-
aria-hidden="true"
|
|
37
|
-
/>
|
|
38
|
-
</button>
|
|
39
|
-
|
|
40
|
-
<ul
|
|
41
|
-
v-if="open"
|
|
42
|
-
role="listbox"
|
|
43
|
-
:aria-label="ariaLabel || placeholder"
|
|
44
|
-
class="absolute z-20 mt-1 w-full min-w-max max-w-xs max-h-60 overflow-auto rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 py-1 shadow-lg focus:outline-none"
|
|
45
|
-
>
|
|
46
|
-
<li
|
|
47
|
-
v-for="opt in options"
|
|
48
|
-
:key="String(opt.value)"
|
|
49
|
-
role="option"
|
|
50
|
-
:aria-selected="opt.value === modelValue"
|
|
51
|
-
:title="opt.title"
|
|
52
|
-
>
|
|
53
|
-
<button
|
|
54
|
-
type="button"
|
|
55
|
-
:disabled="opt.disabled"
|
|
56
|
-
:class="rowClass(opt)"
|
|
57
|
-
@click="select(opt)"
|
|
58
|
-
>
|
|
59
|
-
{{ opt.label }}
|
|
60
|
-
</button>
|
|
61
|
-
</li>
|
|
62
|
-
</ul>
|
|
63
|
-
</div>
|
|
64
|
-
</template>
|
|
65
|
-
|
|
66
|
-
<script setup lang="ts">
|
|
67
|
-
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
68
|
-
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
|
|
69
|
-
import { useEscapeKey } from '../composables/useEscapeKey'
|
|
70
|
-
import type { DropdownOption } from '../types/dropdown'
|
|
71
|
-
|
|
72
|
-
// NOT generic on purpose: vite-plugin-dts flattens SFC generics out of the
|
|
73
|
-
// emitted declarations, so consumers would see the erased constraint anyway.
|
|
74
|
-
// The update:modelValue payload is `any` so `v-model` on a union-typed ref
|
|
75
|
-
// type-checks; the actual payload is always one of the options' values.
|
|
76
|
-
interface Props {
|
|
77
|
-
options: readonly DropdownOption[]
|
|
78
|
-
modelValue?: string | number | null
|
|
79
|
-
/** Trigger text shown when nothing is selected. */
|
|
80
|
-
placeholder?: string
|
|
81
|
-
/** 'md' = px-4 py-2.5 (default, the app filter height); 'sm' = px-3 py-2. */
|
|
82
|
-
size?: 'sm' | 'md'
|
|
83
|
-
/** Full-width (w-full). Set false for an inline, content-width dropdown. */
|
|
84
|
-
block?: boolean
|
|
85
|
-
disabled?: boolean
|
|
86
|
-
/** Accessible name for the trigger/listbox when there is no visible label. */
|
|
87
|
-
ariaLabel?: string
|
|
88
|
-
/**
|
|
89
|
-
* Replaces the trigger's default slate skin (border/background/text/padding)
|
|
90
|
-
* with the given classes — layout, focus ring and disabled classes are kept.
|
|
91
|
-
* Lets a consumer render the trigger as e.g. a coloured status pill while
|
|
92
|
-
* keeping the shared menu behaviour. `size` is ignored when this is set.
|
|
93
|
-
*/
|
|
94
|
-
triggerClass?: string
|
|
95
|
-
/** Chevron sizing/colour classes; shrink for compact triggers. */
|
|
96
|
-
chevronClass?: string
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
100
|
-
modelValue: null,
|
|
101
|
-
placeholder: 'Select',
|
|
102
|
-
size: 'md',
|
|
103
|
-
block: true,
|
|
104
|
-
disabled: false,
|
|
105
|
-
ariaLabel: '',
|
|
106
|
-
triggerClass: '',
|
|
107
|
-
chevronClass: 'w-5 h-5 text-slate-500 dark:text-slate-400',
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
const emit = defineEmits<{
|
|
111
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
-
(e: 'update:modelValue', value: any): void
|
|
113
|
-
}>()
|
|
114
|
-
|
|
115
|
-
const open = ref(false)
|
|
116
|
-
const rootEl = ref<HTMLElement | null>(null)
|
|
117
|
-
|
|
118
|
-
const selectedOption = computed(() =>
|
|
119
|
-
props.options.find((o) => o.value === props.modelValue) ?? null,
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
const triggerClass = computed(() => {
|
|
123
|
-
const layout =
|
|
124
|
-
'flex items-center justify-between gap-2 text-left transition-colors ' +
|
|
125
|
-
'focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 ' +
|
|
126
|
-
'cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed '
|
|
127
|
-
if (props.triggerClass) return layout + props.triggerClass
|
|
128
|
-
const padding = props.size === 'sm' ? 'px-3 py-2' : 'px-4 py-2.5'
|
|
129
|
-
return (
|
|
130
|
-
layout +
|
|
131
|
-
'rounded-lg border text-sm ' +
|
|
132
|
-
'border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-100 ' +
|
|
133
|
-
'hover:bg-slate-50 dark:hover:bg-slate-800 ' +
|
|
134
|
-
padding
|
|
135
|
-
)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
function rowClass(opt: DropdownOption): string {
|
|
139
|
-
const selected = opt.value === props.modelValue
|
|
140
|
-
const base =
|
|
141
|
-
'block w-full px-4 py-2 text-left text-sm transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed'
|
|
142
|
-
const fill = selected
|
|
143
|
-
? 'bg-emerald-500 text-white'
|
|
144
|
-
: 'text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800'
|
|
145
|
-
return `${base} ${fill}`
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function toggle() {
|
|
149
|
-
if (props.disabled) return
|
|
150
|
-
open.value = !open.value
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function select(opt: DropdownOption) {
|
|
154
|
-
if (opt.disabled) return
|
|
155
|
-
emit('update:modelValue', opt.value)
|
|
156
|
-
open.value = false
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
useEscapeKey(() => {
|
|
160
|
-
open.value = false
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
function onDocumentClick(e: MouseEvent) {
|
|
164
|
-
if (!open.value) return
|
|
165
|
-
if (rootEl.value && !rootEl.value.contains(e.target as Node)) {
|
|
166
|
-
open.value = false
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
onMounted(() => document.addEventListener('click', onDocumentClick))
|
|
171
|
-
onBeforeUnmount(() => document.removeEventListener('click', onDocumentClick))
|
|
172
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Button-style single-select dropdown ("Select Social User ⌄"). Unlike
|
|
4
|
+
BaseSelect (a themed native <select>), this renders a trigger button plus
|
|
5
|
+
a floating menu, so the closed control shows a placeholder and a chevron
|
|
6
|
+
that rotates while open — matching the app's filter dropdowns.
|
|
7
|
+
|
|
8
|
+
Options are passed as data (not <option> slots) so the menu rows can carry
|
|
9
|
+
tooltips and disabled state. Selecting a row emits its `value` and closes
|
|
10
|
+
the menu; Escape and an outside click also close it.
|
|
11
|
+
|
|
12
|
+
Same `dark:`-variant note as BaseSelect: the slate skin ships static
|
|
13
|
+
light/dark classes so consumers get both themes without extra config.
|
|
14
|
+
-->
|
|
15
|
+
<div
|
|
16
|
+
ref="rootEl"
|
|
17
|
+
:class="block ? 'relative w-full' : 'relative inline-block'"
|
|
18
|
+
>
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
:class="[triggerClass, block ? 'w-full' : '']"
|
|
22
|
+
:disabled="disabled"
|
|
23
|
+
:aria-label="ariaLabel || undefined"
|
|
24
|
+
aria-haspopup="listbox"
|
|
25
|
+
:aria-expanded="open"
|
|
26
|
+
@click="toggle"
|
|
27
|
+
>
|
|
28
|
+
<!-- min-w-0 + truncate so long labels ellipsize inside narrow
|
|
29
|
+
(fixed-column) triggers instead of wrapping or widening them. -->
|
|
30
|
+
<span class="min-w-0 truncate" :class="selectedOption ? '' : 'text-slate-400 dark:text-slate-500'">
|
|
31
|
+
{{ selectedOption ? selectedOption.label : placeholder }}
|
|
32
|
+
</span>
|
|
33
|
+
<ChevronDownIcon
|
|
34
|
+
class="shrink-0 transition-transform"
|
|
35
|
+
:class="[chevronClass, open ? 'rotate-180' : '']"
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
/>
|
|
38
|
+
</button>
|
|
39
|
+
|
|
40
|
+
<ul
|
|
41
|
+
v-if="open"
|
|
42
|
+
role="listbox"
|
|
43
|
+
:aria-label="ariaLabel || placeholder"
|
|
44
|
+
class="absolute z-20 mt-1 w-full min-w-max max-w-xs max-h-60 overflow-auto rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 py-1 shadow-lg focus:outline-none"
|
|
45
|
+
>
|
|
46
|
+
<li
|
|
47
|
+
v-for="opt in options"
|
|
48
|
+
:key="String(opt.value)"
|
|
49
|
+
role="option"
|
|
50
|
+
:aria-selected="opt.value === modelValue"
|
|
51
|
+
:title="opt.title"
|
|
52
|
+
>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
:disabled="opt.disabled"
|
|
56
|
+
:class="rowClass(opt)"
|
|
57
|
+
@click="select(opt)"
|
|
58
|
+
>
|
|
59
|
+
{{ opt.label }}
|
|
60
|
+
</button>
|
|
61
|
+
</li>
|
|
62
|
+
</ul>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<script setup lang="ts">
|
|
67
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
68
|
+
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
|
|
69
|
+
import { useEscapeKey } from '../composables/useEscapeKey'
|
|
70
|
+
import type { DropdownOption } from '../types/dropdown'
|
|
71
|
+
|
|
72
|
+
// NOT generic on purpose: vite-plugin-dts flattens SFC generics out of the
|
|
73
|
+
// emitted declarations, so consumers would see the erased constraint anyway.
|
|
74
|
+
// The update:modelValue payload is `any` so `v-model` on a union-typed ref
|
|
75
|
+
// type-checks; the actual payload is always one of the options' values.
|
|
76
|
+
interface Props {
|
|
77
|
+
options: readonly DropdownOption[]
|
|
78
|
+
modelValue?: string | number | null
|
|
79
|
+
/** Trigger text shown when nothing is selected. */
|
|
80
|
+
placeholder?: string
|
|
81
|
+
/** 'md' = px-4 py-2.5 (default, the app filter height); 'sm' = px-3 py-2. */
|
|
82
|
+
size?: 'sm' | 'md'
|
|
83
|
+
/** Full-width (w-full). Set false for an inline, content-width dropdown. */
|
|
84
|
+
block?: boolean
|
|
85
|
+
disabled?: boolean
|
|
86
|
+
/** Accessible name for the trigger/listbox when there is no visible label. */
|
|
87
|
+
ariaLabel?: string
|
|
88
|
+
/**
|
|
89
|
+
* Replaces the trigger's default slate skin (border/background/text/padding)
|
|
90
|
+
* with the given classes — layout, focus ring and disabled classes are kept.
|
|
91
|
+
* Lets a consumer render the trigger as e.g. a coloured status pill while
|
|
92
|
+
* keeping the shared menu behaviour. `size` is ignored when this is set.
|
|
93
|
+
*/
|
|
94
|
+
triggerClass?: string
|
|
95
|
+
/** Chevron sizing/colour classes; shrink for compact triggers. */
|
|
96
|
+
chevronClass?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
100
|
+
modelValue: null,
|
|
101
|
+
placeholder: 'Select',
|
|
102
|
+
size: 'md',
|
|
103
|
+
block: true,
|
|
104
|
+
disabled: false,
|
|
105
|
+
ariaLabel: '',
|
|
106
|
+
triggerClass: '',
|
|
107
|
+
chevronClass: 'w-5 h-5 text-slate-500 dark:text-slate-400',
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const emit = defineEmits<{
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
+
(e: 'update:modelValue', value: any): void
|
|
113
|
+
}>()
|
|
114
|
+
|
|
115
|
+
const open = ref(false)
|
|
116
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
117
|
+
|
|
118
|
+
const selectedOption = computed(() =>
|
|
119
|
+
props.options.find((o) => o.value === props.modelValue) ?? null,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const triggerClass = computed(() => {
|
|
123
|
+
const layout =
|
|
124
|
+
'flex items-center justify-between gap-2 text-left transition-colors ' +
|
|
125
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 ' +
|
|
126
|
+
'cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed '
|
|
127
|
+
if (props.triggerClass) return layout + props.triggerClass
|
|
128
|
+
const padding = props.size === 'sm' ? 'px-3 py-2' : 'px-4 py-2.5'
|
|
129
|
+
return (
|
|
130
|
+
layout +
|
|
131
|
+
'rounded-lg border text-sm ' +
|
|
132
|
+
'border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-100 ' +
|
|
133
|
+
'hover:bg-slate-50 dark:hover:bg-slate-800 ' +
|
|
134
|
+
padding
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
function rowClass(opt: DropdownOption): string {
|
|
139
|
+
const selected = opt.value === props.modelValue
|
|
140
|
+
const base =
|
|
141
|
+
'block w-full px-4 py-2 text-left text-sm transition-colors cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed'
|
|
142
|
+
const fill = selected
|
|
143
|
+
? 'bg-emerald-500 text-white'
|
|
144
|
+
: 'text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800'
|
|
145
|
+
return `${base} ${fill}`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toggle() {
|
|
149
|
+
if (props.disabled) return
|
|
150
|
+
open.value = !open.value
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function select(opt: DropdownOption) {
|
|
154
|
+
if (opt.disabled) return
|
|
155
|
+
emit('update:modelValue', opt.value)
|
|
156
|
+
open.value = false
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
useEscapeKey(() => {
|
|
160
|
+
open.value = false
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
function onDocumentClick(e: MouseEvent) {
|
|
164
|
+
if (!open.value) return
|
|
165
|
+
if (rootEl.value && !rootEl.value.contains(e.target as Node)) {
|
|
166
|
+
open.value = false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
onMounted(() => document.addEventListener('click', onDocumentClick))
|
|
171
|
+
onBeforeUnmount(() => document.removeEventListener('click', onDocumentClick))
|
|
172
|
+
</script>
|