mgv-backoffice 1.35.0 → 1.36.1
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 -1686
- package/dist/components/BaseBarDistribution.vue.d.ts +24 -0
- package/dist/components/BasePageHeader.vue.d.ts +8 -2
- package/dist/index.d.ts +2 -0
- package/dist/types/distribution.d.ts +7 -0
- package/dist/ui-lib.css +1 -1
- package/dist/ui-lib.js +634 -574
- 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/BaseBarDistribution.test.ts +51 -0
- package/src/components/BaseBarDistribution.vue +73 -0
- 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 -103
- 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 -90
- 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 -136
- package/src/style.css +32 -32
- package/src/types/distribution.ts +7 -0
- 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
package/dist/utils/specForm.d.ts
CHANGED
|
@@ -15,6 +15,6 @@ export declare function buildSpecParams(specs: readonly SpecField[] | undefined,
|
|
|
15
15
|
* '' under number coercion), or null when every numeric field holds a number.
|
|
16
16
|
* A spec whose `default` is null is OPTIONAL: blank means "knob disabled" and
|
|
17
17
|
* must pass; a non-blank value on an optional field is still validated as a
|
|
18
|
-
* number.
|
|
18
|
+
* number. Integer specs additionally reject fractional values.
|
|
19
19
|
*/
|
|
20
20
|
export declare function firstInvalidNumericSpec(specs: readonly SpecField[] | undefined, params: Record<string, SpecFieldValue>): string | null;
|
package/package.json
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mgv-backoffice",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Shared Vue 3 UI component library",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/ui-lib.umd.cjs",
|
|
7
|
-
"module": "./dist/ui-lib.js",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/ui-lib.js",
|
|
13
|
-
"require": "./dist/ui-lib.umd.cjs"
|
|
14
|
-
},
|
|
15
|
-
"./dist/style.css": "./dist/ui-lib.css",
|
|
16
|
-
"./tailwind.safelist": "./tailwind.safelist.js",
|
|
17
|
-
"./tailwind.safelist.css": "./tailwind.safelist.css"
|
|
18
|
-
},
|
|
19
|
-
"files": [
|
|
20
|
-
"dist",
|
|
21
|
-
"src",
|
|
22
|
-
"tailwind.safelist.js",
|
|
23
|
-
"tailwind.safelist.css"
|
|
24
|
-
],
|
|
25
|
-
"scripts": {
|
|
26
|
-
"safelist": "node scripts/generate-safelist.mjs",
|
|
27
|
-
"prebuild": "npm run safelist",
|
|
28
|
-
"build": "vite build",
|
|
29
|
-
"dev": "vite build --watch",
|
|
30
|
-
"test": "vitest run",
|
|
31
|
-
"test:watch": "vitest"
|
|
32
|
-
},
|
|
33
|
-
"peerDependencies": {
|
|
34
|
-
"@heroicons/vue": "^2.0.0",
|
|
35
|
-
"vue": "^3.5.0",
|
|
36
|
-
"vue-router": "^5.0.0"
|
|
37
|
-
},
|
|
38
|
-
"devDependencies": {
|
|
39
|
-
"@heroicons/vue": "^2.2.0",
|
|
40
|
-
"@tailwindcss/vite": "^4.3.1",
|
|
41
|
-
"@vitejs/plugin-vue": "^6.0.7",
|
|
42
|
-
"@vue/language-core": "^3.3.5",
|
|
43
|
-
"@vue/test-utils": "^2.4.11",
|
|
44
|
-
"jsdom": "^29.1.1",
|
|
45
|
-
"sass": "^1.101.0",
|
|
46
|
-
"tailwindcss": "^4.3.1",
|
|
47
|
-
"typescript": "^6.0.3",
|
|
48
|
-
"vite": "^8.1.0",
|
|
49
|
-
"vite-plugin-dts": "^5.0.3",
|
|
50
|
-
"vitest": "^4.1.9",
|
|
51
|
-
"vue": "^3.5.38",
|
|
52
|
-
"vue-router": "^5.1.0"
|
|
53
|
-
}
|
|
54
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "mgv-backoffice",
|
|
3
|
+
"version": "1.36.1",
|
|
4
|
+
"description": "Shared Vue 3 UI component library",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/ui-lib.umd.cjs",
|
|
7
|
+
"module": "./dist/ui-lib.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/ui-lib.js",
|
|
13
|
+
"require": "./dist/ui-lib.umd.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./dist/style.css": "./dist/ui-lib.css",
|
|
16
|
+
"./tailwind.safelist": "./tailwind.safelist.js",
|
|
17
|
+
"./tailwind.safelist.css": "./tailwind.safelist.css"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src",
|
|
22
|
+
"tailwind.safelist.js",
|
|
23
|
+
"tailwind.safelist.css"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"safelist": "node scripts/generate-safelist.mjs",
|
|
27
|
+
"prebuild": "npm run safelist",
|
|
28
|
+
"build": "vite build",
|
|
29
|
+
"dev": "vite build --watch",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@heroicons/vue": "^2.0.0",
|
|
35
|
+
"vue": "^3.5.0",
|
|
36
|
+
"vue-router": "^5.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@heroicons/vue": "^2.2.0",
|
|
40
|
+
"@tailwindcss/vite": "^4.3.1",
|
|
41
|
+
"@vitejs/plugin-vue": "^6.0.7",
|
|
42
|
+
"@vue/language-core": "^3.3.5",
|
|
43
|
+
"@vue/test-utils": "^2.4.11",
|
|
44
|
+
"jsdom": "^29.1.1",
|
|
45
|
+
"sass": "^1.101.0",
|
|
46
|
+
"tailwindcss": "^4.3.1",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"vite": "^8.1.0",
|
|
49
|
+
"vite-plugin-dts": "^5.0.3",
|
|
50
|
+
"vitest": "^4.1.9",
|
|
51
|
+
"vue": "^3.5.38",
|
|
52
|
+
"vue-router": "^5.1.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!--
|
|
3
|
-
Inline notice panel (error / warning / success / info).
|
|
4
|
-
|
|
5
|
-
v2 (1.27.0): rewritten from the legacy dynamic-class version —
|
|
6
|
-
• theme-aware via the shared `isDark` ref (same pattern as the other
|
|
7
|
-
extracted components; no runtime-composed class names, so every
|
|
8
|
-
class survives Tailwind's static scan)
|
|
9
|
-
• optional default slot for body content below the title
|
|
10
|
-
• `compact` variant for slim in-form warnings (text-xs, small icon)
|
|
11
|
-
Backward compatible: `<BaseAlert title="..." :color="AlertEnum.X" />`
|
|
12
|
-
renders the same single-line alert as before, mb-4 included.
|
|
13
|
-
-->
|
|
14
|
-
<div
|
|
15
|
-
role="alert"
|
|
16
|
-
class="flex gap-3 mb-4 rounded-lg border text-sm transition-colors"
|
|
17
|
-
:class="[compact ? 'items-start px-3 py-2 text-xs' : 'items-start p-4', palette]"
|
|
18
|
-
>
|
|
19
|
-
<component
|
|
20
|
-
:is="iconComponent"
|
|
21
|
-
class="shrink-0"
|
|
22
|
-
:class="[compact ? 'w-4 h-4 mt-px' : 'w-5 h-5 mt-0.5', iconColorClass]"
|
|
23
|
-
aria-hidden="true"
|
|
24
|
-
/>
|
|
25
|
-
<div class="min-w-0 flex-1">
|
|
26
|
-
<p v-if="title" class="font-medium">{{ title }}</p>
|
|
27
|
-
<!-- Body — caller-styled free-form content under the title. -->
|
|
28
|
-
<slot />
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
</template>
|
|
32
|
-
|
|
33
|
-
<script lang="ts" setup>
|
|
34
|
-
import {
|
|
35
|
-
CheckCircleIcon,
|
|
36
|
-
ExclamationCircleIcon,
|
|
37
|
-
ExclamationTriangleIcon,
|
|
38
|
-
InformationCircleIcon,
|
|
39
|
-
} from '@heroicons/vue/24/outline'
|
|
40
|
-
import { AlertEnum } from '../enums/AlertEnum'
|
|
41
|
-
import { computed } from 'vue'
|
|
42
|
-
import { useTheme } from '../composables/useTheme'
|
|
43
|
-
|
|
44
|
-
interface Props {
|
|
45
|
-
/** Bold headline. Optional when the slot carries the whole message. */
|
|
46
|
-
title?: string
|
|
47
|
-
/** Alert mode — drives palette and icon. Defaults to ERROR. */
|
|
48
|
-
color?: AlertEnum
|
|
49
|
-
/** Slim variant for in-form warnings: text-xs, smaller icon/padding. */
|
|
50
|
-
compact?: boolean
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
54
|
-
title: '',
|
|
55
|
-
color: AlertEnum.ERROR,
|
|
56
|
-
compact: false,
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
const { isDark } = useTheme()
|
|
60
|
-
|
|
61
|
-
// Literal palette table so Tailwind's scanner sees every class.
|
|
62
|
-
const PALETTES: Record<AlertEnum, { light: string; dark: string }> = {
|
|
63
|
-
[AlertEnum.ERROR]: {
|
|
64
|
-
light: 'bg-red-50 border-red-200 text-red-700',
|
|
65
|
-
dark: 'bg-red-500/10 border-red-500/30 text-red-300',
|
|
66
|
-
},
|
|
67
|
-
[AlertEnum.WARNING]: {
|
|
68
|
-
light: 'bg-amber-50 border-amber-200 text-amber-700',
|
|
69
|
-
dark: 'bg-amber-500/10 border-amber-500/30 text-amber-300',
|
|
70
|
-
},
|
|
71
|
-
[AlertEnum.SUCCESS]: {
|
|
72
|
-
light: 'bg-emerald-50 border-emerald-200 text-emerald-700',
|
|
73
|
-
dark: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-300',
|
|
74
|
-
},
|
|
75
|
-
[AlertEnum.INFO]: {
|
|
76
|
-
light: 'bg-sky-50 border-sky-200 text-sky-700',
|
|
77
|
-
dark: 'bg-sky-500/10 border-sky-500/30 text-sky-300',
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const palette = computed(() =>
|
|
82
|
-
isDark.value ? PALETTES[props.color].dark : PALETTES[props.color].light,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
// Icon + tint follow the alert mode so an error doesn't render with a
|
|
86
|
-
// neutral info glyph.
|
|
87
|
-
const iconComponent = computed(() => {
|
|
88
|
-
switch (props.color) {
|
|
89
|
-
case AlertEnum.SUCCESS: return CheckCircleIcon
|
|
90
|
-
case AlertEnum.WARNING: return ExclamationTriangleIcon
|
|
91
|
-
case AlertEnum.INFO: return InformationCircleIcon
|
|
92
|
-
default: return ExclamationCircleIcon
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
const iconColorClass = computed(() => {
|
|
97
|
-
switch (props.color) {
|
|
98
|
-
case AlertEnum.SUCCESS: return 'text-emerald-500'
|
|
99
|
-
case AlertEnum.WARNING: return 'text-amber-500'
|
|
100
|
-
case AlertEnum.INFO: return 'text-sky-500'
|
|
101
|
-
default: return 'text-red-500'
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Inline notice panel (error / warning / success / info).
|
|
4
|
+
|
|
5
|
+
v2 (1.27.0): rewritten from the legacy dynamic-class version —
|
|
6
|
+
• theme-aware via the shared `isDark` ref (same pattern as the other
|
|
7
|
+
extracted components; no runtime-composed class names, so every
|
|
8
|
+
class survives Tailwind's static scan)
|
|
9
|
+
• optional default slot for body content below the title
|
|
10
|
+
• `compact` variant for slim in-form warnings (text-xs, small icon)
|
|
11
|
+
Backward compatible: `<BaseAlert title="..." :color="AlertEnum.X" />`
|
|
12
|
+
renders the same single-line alert as before, mb-4 included.
|
|
13
|
+
-->
|
|
14
|
+
<div
|
|
15
|
+
role="alert"
|
|
16
|
+
class="flex gap-3 mb-4 rounded-lg border text-sm transition-colors"
|
|
17
|
+
:class="[compact ? 'items-start px-3 py-2 text-xs' : 'items-start p-4', palette]"
|
|
18
|
+
>
|
|
19
|
+
<component
|
|
20
|
+
:is="iconComponent"
|
|
21
|
+
class="shrink-0"
|
|
22
|
+
:class="[compact ? 'w-4 h-4 mt-px' : 'w-5 h-5 mt-0.5', iconColorClass]"
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
/>
|
|
25
|
+
<div class="min-w-0 flex-1">
|
|
26
|
+
<p v-if="title" class="font-medium">{{ title }}</p>
|
|
27
|
+
<!-- Body — caller-styled free-form content under the title. -->
|
|
28
|
+
<slot />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script lang="ts" setup>
|
|
34
|
+
import {
|
|
35
|
+
CheckCircleIcon,
|
|
36
|
+
ExclamationCircleIcon,
|
|
37
|
+
ExclamationTriangleIcon,
|
|
38
|
+
InformationCircleIcon,
|
|
39
|
+
} from '@heroicons/vue/24/outline'
|
|
40
|
+
import { AlertEnum } from '../enums/AlertEnum'
|
|
41
|
+
import { computed } from 'vue'
|
|
42
|
+
import { useTheme } from '../composables/useTheme'
|
|
43
|
+
|
|
44
|
+
interface Props {
|
|
45
|
+
/** Bold headline. Optional when the slot carries the whole message. */
|
|
46
|
+
title?: string
|
|
47
|
+
/** Alert mode — drives palette and icon. Defaults to ERROR. */
|
|
48
|
+
color?: AlertEnum
|
|
49
|
+
/** Slim variant for in-form warnings: text-xs, smaller icon/padding. */
|
|
50
|
+
compact?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
54
|
+
title: '',
|
|
55
|
+
color: AlertEnum.ERROR,
|
|
56
|
+
compact: false,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const { isDark } = useTheme()
|
|
60
|
+
|
|
61
|
+
// Literal palette table so Tailwind's scanner sees every class.
|
|
62
|
+
const PALETTES: Record<AlertEnum, { light: string; dark: string }> = {
|
|
63
|
+
[AlertEnum.ERROR]: {
|
|
64
|
+
light: 'bg-red-50 border-red-200 text-red-700',
|
|
65
|
+
dark: 'bg-red-500/10 border-red-500/30 text-red-300',
|
|
66
|
+
},
|
|
67
|
+
[AlertEnum.WARNING]: {
|
|
68
|
+
light: 'bg-amber-50 border-amber-200 text-amber-700',
|
|
69
|
+
dark: 'bg-amber-500/10 border-amber-500/30 text-amber-300',
|
|
70
|
+
},
|
|
71
|
+
[AlertEnum.SUCCESS]: {
|
|
72
|
+
light: 'bg-emerald-50 border-emerald-200 text-emerald-700',
|
|
73
|
+
dark: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-300',
|
|
74
|
+
},
|
|
75
|
+
[AlertEnum.INFO]: {
|
|
76
|
+
light: 'bg-sky-50 border-sky-200 text-sky-700',
|
|
77
|
+
dark: 'bg-sky-500/10 border-sky-500/30 text-sky-300',
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const palette = computed(() =>
|
|
82
|
+
isDark.value ? PALETTES[props.color].dark : PALETTES[props.color].light,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// Icon + tint follow the alert mode so an error doesn't render with a
|
|
86
|
+
// neutral info glyph.
|
|
87
|
+
const iconComponent = computed(() => {
|
|
88
|
+
switch (props.color) {
|
|
89
|
+
case AlertEnum.SUCCESS: return CheckCircleIcon
|
|
90
|
+
case AlertEnum.WARNING: return ExclamationTriangleIcon
|
|
91
|
+
case AlertEnum.INFO: return InformationCircleIcon
|
|
92
|
+
default: return ExclamationCircleIcon
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const iconColorClass = computed(() => {
|
|
97
|
+
switch (props.color) {
|
|
98
|
+
case AlertEnum.SUCCESS: return 'text-emerald-500'
|
|
99
|
+
case AlertEnum.WARNING: return 'text-amber-500'
|
|
100
|
+
case AlertEnum.INFO: return 'text-sky-500'
|
|
101
|
+
default: return 'text-red-500'
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
</script>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import BaseBarDistribution from './BaseBarDistribution.vue'
|
|
4
|
+
import type { DistributionBar } from '../types/distribution'
|
|
5
|
+
|
|
6
|
+
const BARS: DistributionBar[] = [
|
|
7
|
+
{ label: '0.5', count: 1 },
|
|
8
|
+
{ label: '1', count: 4 },
|
|
9
|
+
{ label: '2', count: 2 },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
describe('BaseBarDistribution', () => {
|
|
13
|
+
it('renders one bar per entry with its count and label', () => {
|
|
14
|
+
const wrap = mount(BaseBarDistribution, { props: { bars: BARS } })
|
|
15
|
+
const bars = wrap.findAll('div[title]')
|
|
16
|
+
expect(bars).toHaveLength(3)
|
|
17
|
+
expect(wrap.text()).toContain('4')
|
|
18
|
+
expect(wrap.text()).toContain('0.5')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('scales bar heights against the tallest count, with a 3px floor', () => {
|
|
22
|
+
const wrap = mount(BaseBarDistribution, {
|
|
23
|
+
props: { bars: [...BARS, { label: '9', count: 0 }], maxBarHeight: 56 },
|
|
24
|
+
})
|
|
25
|
+
const heights = wrap
|
|
26
|
+
.findAll('.rounded-t')
|
|
27
|
+
.map((el) => el.attributes('style'))
|
|
28
|
+
// count 4 → full height; count 2 → half; count 0 → 3px floor.
|
|
29
|
+
expect(heights[1]).toContain('height: 56px')
|
|
30
|
+
expect(heights[2]).toContain('height: 28px')
|
|
31
|
+
expect(heights[3]).toContain('height: 3px')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('builds singular/plural tooltips with the prefix and noun', () => {
|
|
35
|
+
const wrap = mount(BaseBarDistribution, {
|
|
36
|
+
props: { bars: BARS, titlePrefix: 'Take profit', countNoun: 'variant' },
|
|
37
|
+
})
|
|
38
|
+
const titles = wrap
|
|
39
|
+
.findAll('div[title]')
|
|
40
|
+
.map((el) => el.attributes('title'))
|
|
41
|
+
expect(titles[0]).toBe('Take profit 0.5: 1 variant')
|
|
42
|
+
expect(titles[1]).toBe('Take profit 1: 4 variants')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('exposes the aria label on the chart container', () => {
|
|
46
|
+
const wrap = mount(BaseBarDistribution, {
|
|
47
|
+
props: { bars: BARS, ariaLabel: 'Distribution of X' },
|
|
48
|
+
})
|
|
49
|
+
expect(wrap.get('[role="img"]').attributes('aria-label')).toBe('Distribution of X')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Compact value-distribution chart: one thin rounded bar per distinct value,
|
|
3
|
+
// count labelled on top and the value underneath — scrolls sideways when
|
|
4
|
+
// there are many bars. Pure Tailwind, no chart library. Extracted from
|
|
5
|
+
// TradeAutomation's variant-stats modal, where it summarises how often each
|
|
6
|
+
// parameter value occurs across generated strategy variants.
|
|
7
|
+
import { computed } from 'vue'
|
|
8
|
+
import type { DistributionBar } from '../types/distribution'
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(
|
|
11
|
+
defineProps<{
|
|
12
|
+
/** Bars in display order (callers sort ascending for numeric values). */
|
|
13
|
+
bars: DistributionBar[]
|
|
14
|
+
/** Accessible description of what the chart shows. */
|
|
15
|
+
ariaLabel?: string
|
|
16
|
+
/** Noun for each bar's tooltip count, e.g. "variant" → "3 variants". */
|
|
17
|
+
countNoun?: string
|
|
18
|
+
/** Tooltip prefix before the value, e.g. the field name. */
|
|
19
|
+
titlePrefix?: string
|
|
20
|
+
/** Height of the tallest bar, in px. */
|
|
21
|
+
maxBarHeight?: number
|
|
22
|
+
/** Tailwind classes for the bar fill. */
|
|
23
|
+
barClass?: string
|
|
24
|
+
}>(),
|
|
25
|
+
{
|
|
26
|
+
ariaLabel: 'Value distribution',
|
|
27
|
+
countNoun: 'item',
|
|
28
|
+
titlePrefix: '',
|
|
29
|
+
maxBarHeight: 56,
|
|
30
|
+
barClass: 'bg-emerald-500/80 dark:bg-emerald-400/80',
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const maxCount = computed(() => Math.max(1, ...props.bars.map((b) => b.count)))
|
|
35
|
+
|
|
36
|
+
const barHeight = (b: DistributionBar): number =>
|
|
37
|
+
Math.max(3, Math.round((b.count / maxCount.value) * props.maxBarHeight))
|
|
38
|
+
|
|
39
|
+
const barTitle = (b: DistributionBar): string =>
|
|
40
|
+
`${props.titlePrefix ? `${props.titlePrefix} ` : ''}${b.label}: ${b.count} ${props.countNoun}${
|
|
41
|
+
b.count === 1 ? '' : 's'
|
|
42
|
+
}`
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div
|
|
47
|
+
class="flex items-end gap-1.5 overflow-x-auto pb-1"
|
|
48
|
+
role="img"
|
|
49
|
+
:aria-label="ariaLabel"
|
|
50
|
+
>
|
|
51
|
+
<div
|
|
52
|
+
v-for="b in bars"
|
|
53
|
+
:key="b.label"
|
|
54
|
+
class="flex min-w-7 flex-1 flex-col items-center gap-0.5"
|
|
55
|
+
:title="barTitle(b)"
|
|
56
|
+
>
|
|
57
|
+
<span class="text-[10px] tabular-nums text-slate-400 dark:text-slate-500">
|
|
58
|
+
{{ b.count }}
|
|
59
|
+
</span>
|
|
60
|
+
<div
|
|
61
|
+
class="w-full max-w-8 rounded-t"
|
|
62
|
+
:class="barClass"
|
|
63
|
+
:style="{ height: `${barHeight(b)}px` }"
|
|
64
|
+
/>
|
|
65
|
+
<span
|
|
66
|
+
class="max-w-full truncate font-mono text-[10px] text-slate-500 dark:text-slate-400"
|
|
67
|
+
:title="b.label"
|
|
68
|
+
>
|
|
69
|
+
{{ b.label }}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!--
|
|
3
|
-
Small tinted "chip" action button — the compact emerald "+ Add" pill
|
|
4
|
-
used above repeatable form rows (extracted from WireMate's CreateMock,
|
|
5
|
-
where the identical class string appeared seven times).
|
|
6
|
-
|
|
7
|
-
Label comes from the default slot so callers can mix text and icons.
|
|
8
|
-
-->
|
|
9
|
-
<button
|
|
10
|
-
type="button"
|
|
11
|
-
:disabled="disabled"
|
|
12
|
-
class="inline-flex items-center gap-1 font-medium rounded-md border transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
|
|
13
|
-
:class="[
|
|
14
|
-
size === 'xs' ? 'text-xs px-2 py-0.5' : 'text-xs px-2.5 py-1',
|
|
15
|
-
isDark
|
|
16
|
-
? 'text-emerald-300 border-emerald-700 bg-emerald-900/30 hover:bg-emerald-800/50'
|
|
17
|
-
: 'text-emerald-700 border-emerald-200 bg-emerald-50 hover:bg-emerald-100',
|
|
18
|
-
]"
|
|
19
|
-
@click="emit('click')"
|
|
20
|
-
>
|
|
21
|
-
<slot />
|
|
22
|
-
</button>
|
|
23
|
-
</template>
|
|
24
|
-
|
|
25
|
-
<script lang="ts" setup>
|
|
26
|
-
import { useTheme } from '../composables/useTheme'
|
|
27
|
-
|
|
28
|
-
interface Props {
|
|
29
|
-
/** 'sm' = px-2.5 py-1 (default); 'xs' = px-2 py-0.5 for tight corners. */
|
|
30
|
-
size?: 'xs' | 'sm'
|
|
31
|
-
disabled?: boolean
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
withDefaults(defineProps<Props>(), {
|
|
35
|
-
size: 'sm',
|
|
36
|
-
disabled: false,
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
const emit = defineEmits<{ click: [] }>()
|
|
40
|
-
|
|
41
|
-
const { isDark } = useTheme()
|
|
42
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<!--
|
|
3
|
+
Small tinted "chip" action button — the compact emerald "+ Add" pill
|
|
4
|
+
used above repeatable form rows (extracted from WireMate's CreateMock,
|
|
5
|
+
where the identical class string appeared seven times).
|
|
6
|
+
|
|
7
|
+
Label comes from the default slot so callers can mix text and icons.
|
|
8
|
+
-->
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
class="inline-flex items-center gap-1 font-medium rounded-md border transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
|
|
13
|
+
:class="[
|
|
14
|
+
size === 'xs' ? 'text-xs px-2 py-0.5' : 'text-xs px-2.5 py-1',
|
|
15
|
+
isDark
|
|
16
|
+
? 'text-emerald-300 border-emerald-700 bg-emerald-900/30 hover:bg-emerald-800/50'
|
|
17
|
+
: 'text-emerald-700 border-emerald-200 bg-emerald-50 hover:bg-emerald-100',
|
|
18
|
+
]"
|
|
19
|
+
@click="emit('click')"
|
|
20
|
+
>
|
|
21
|
+
<slot />
|
|
22
|
+
</button>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script lang="ts" setup>
|
|
26
|
+
import { useTheme } from '../composables/useTheme'
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
/** 'sm' = px-2.5 py-1 (default); 'xs' = px-2 py-0.5 for tight corners. */
|
|
30
|
+
size?: 'xs' | 'sm'
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
withDefaults(defineProps<Props>(), {
|
|
35
|
+
size: 'sm',
|
|
36
|
+
disabled: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits<{ click: [] }>()
|
|
40
|
+
|
|
41
|
+
const { isDark } = useTheme()
|
|
42
|
+
</script>
|