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.
Files changed (60) hide show
  1. package/README.md +1794 -1719
  2. package/dist/components/BaseBadge.vue.d.ts +1 -11
  3. package/dist/components/BaseDropdown.vue.d.ts +1 -1
  4. package/dist/ui-lib.js +942 -925
  5. package/dist/ui-lib.umd.cjs +1 -1
  6. package/dist/utils/specForm.d.ts +1 -1
  7. package/package.json +54 -54
  8. package/src/components/BaseAlert.vue +104 -104
  9. package/src/components/BaseBadge.vue +12 -2
  10. package/src/components/BaseBarDistribution.test.ts +51 -51
  11. package/src/components/BaseBarDistribution.vue +73 -73
  12. package/src/components/BaseChipButton.vue +42 -42
  13. package/src/components/BaseCodeBlock.test.ts +70 -70
  14. package/src/components/BaseCodeBlock.vue +74 -74
  15. package/src/components/BaseCollapsibleSection.vue +113 -113
  16. package/src/components/BaseConfirmModal.vue +114 -114
  17. package/src/components/BaseCredentialsForm.test.ts +108 -108
  18. package/src/components/BaseCredentialsForm.vue +241 -241
  19. package/src/components/BaseDropdown.test.ts +87 -87
  20. package/src/components/BaseDropdown.vue +172 -172
  21. package/src/components/BaseEntityPickerModal.vue +330 -330
  22. package/src/components/BaseFileDropzone.vue +94 -94
  23. package/src/components/BaseFilterChip.test.ts +44 -44
  24. package/src/components/BaseFilterChip.vue +67 -67
  25. package/src/components/BaseModalShell.vue +209 -209
  26. package/src/components/BasePageHeader.vue +120 -120
  27. package/src/components/BasePillPickerModal.test.ts +95 -95
  28. package/src/components/BaseRemoveButton.vue +22 -22
  29. package/src/components/BaseSpecFields.test.ts +39 -0
  30. package/src/components/BaseSpecFields.vue +27 -3
  31. package/src/components/BaseStatBreakdown.test.ts +56 -56
  32. package/src/components/BaseStatBreakdown.vue +49 -49
  33. package/src/components/BaseStatusPill.vue +54 -54
  34. package/src/components/BaseTextInputModal.vue +140 -140
  35. package/src/components/BaseToolbarButton.test.ts +48 -48
  36. package/src/components/BaseToolbarButton.vue +94 -94
  37. package/src/composables/useFieldClasses.test.ts +55 -55
  38. package/src/composables/useFieldClasses.ts +56 -56
  39. package/src/composables/usePolling.test.ts +112 -112
  40. package/src/composables/usePolling.ts +102 -102
  41. package/src/composables/useQueryParamSync.test.ts +56 -56
  42. package/src/composables/useQueryParamSync.ts +42 -42
  43. package/src/composables/useThemeClasses.test.ts +34 -34
  44. package/src/composables/useThemeClasses.ts +137 -137
  45. package/src/index.ts +138 -138
  46. package/src/style.css +32 -32
  47. package/src/types/distribution.ts +7 -7
  48. package/src/types/pillPicker.ts +14 -14
  49. package/src/types/specField.ts +30 -30
  50. package/src/types/statBreakdown.ts +6 -6
  51. package/src/utils/format.test.ts +264 -264
  52. package/src/utils/format.ts +216 -216
  53. package/src/utils/httpColors.test.ts +100 -100
  54. package/src/utils/httpColors.ts +99 -99
  55. package/src/utils/kvRows.test.ts +47 -47
  56. package/src/utils/kvRows.ts +34 -34
  57. package/src/utils/specForm.test.ts +64 -53
  58. package/src/utils/specForm.ts +49 -48
  59. package/src/utils/validate.test.ts +82 -82
  60. package/src/utils/validate.ts +76 -76
@@ -1,94 +1,94 @@
1
- <template>
2
- <!--
3
- Dashed "click to select a file" upload zone — extracted from WireMate's
4
- ImportPostmanModal file picker. Also accepts drag-and-drop.
5
-
6
- The hidden input is reset after every selection so choosing the same
7
- file twice still fires `files` — callers don't need their own reset.
8
- -->
9
- <label
10
- class="flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-6 py-10 text-center transition-colors"
11
- :class="[
12
- disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
13
- dragging
14
- ? (isDark ? 'border-emerald-500 text-emerald-400' : 'border-emerald-500 text-emerald-600')
15
- : (isDark
16
- ? 'border-gray-700 hover:border-emerald-500 text-gray-400 hover:text-emerald-400'
17
- : 'border-gray-300 hover:border-emerald-500 text-gray-500 hover:text-emerald-600'),
18
- ]"
19
- @dragover.prevent="onDragOver"
20
- @dragleave.prevent="dragging = false"
21
- @drop.prevent="onDrop"
22
- >
23
- <slot name="icon">
24
- <svg class="w-10 h-10" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
25
- <path
26
- stroke-linecap="round"
27
- stroke-linejoin="round"
28
- d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m6.75 12-3-3m0 0-3 3m3-3v6m-1.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
29
- />
30
- </svg>
31
- </slot>
32
- <p class="text-sm font-medium">{{ label }}</p>
33
- <p v-if="hint" class="text-xs" :class="isDark ? 'text-gray-500' : 'text-gray-400'">
34
- {{ hint }}
35
- </p>
36
- <input
37
- type="file"
38
- class="hidden"
39
- :accept="accept"
40
- :multiple="multiple"
41
- :disabled="disabled"
42
- @change="onChange"
43
- />
44
- </label>
45
- </template>
46
-
47
- <script lang="ts" setup>
48
- import { ref } from 'vue'
49
- import { useTheme } from '../composables/useTheme'
50
-
51
- interface Props {
52
- /** Main line, e.g. "Click to select a Postman collection (.json)". */
53
- label: string
54
- /** Optional dimmed helper line below the label. */
55
- hint?: string
56
- /** Forwarded to the hidden input's `accept`. Dropped files are NOT filtered by it. */
57
- accept?: string
58
- multiple?: boolean
59
- disabled?: boolean
60
- }
61
-
62
- const props = withDefaults(defineProps<Props>(), {
63
- hint: '',
64
- accept: '',
65
- multiple: false,
66
- disabled: false,
67
- })
68
-
69
- const emit = defineEmits<{ files: [files: File[]] }>()
70
-
71
- const { isDark } = useTheme()
72
-
73
- const dragging = ref(false)
74
-
75
- function onChange(e: Event) {
76
- const input = e.target as HTMLInputElement
77
- const files = Array.from(input.files ?? [])
78
- // Reset so re-selecting the same file re-triggers change.
79
- input.value = ''
80
- if (files.length) emit('files', files)
81
- }
82
-
83
- function onDragOver() {
84
- if (!props.disabled) dragging.value = true
85
- }
86
-
87
- function onDrop(e: DragEvent) {
88
- dragging.value = false
89
- if (props.disabled) return
90
- const dropped = Array.from(e.dataTransfer?.files ?? [])
91
- if (!dropped.length) return
92
- emit('files', props.multiple ? dropped : dropped.slice(0, 1))
93
- }
94
- </script>
1
+ <template>
2
+ <!--
3
+ Dashed "click to select a file" upload zone — extracted from WireMate's
4
+ ImportPostmanModal file picker. Also accepts drag-and-drop.
5
+
6
+ The hidden input is reset after every selection so choosing the same
7
+ file twice still fires `files` — callers don't need their own reset.
8
+ -->
9
+ <label
10
+ class="flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed px-6 py-10 text-center transition-colors"
11
+ :class="[
12
+ disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
13
+ dragging
14
+ ? (isDark ? 'border-emerald-500 text-emerald-400' : 'border-emerald-500 text-emerald-600')
15
+ : (isDark
16
+ ? 'border-gray-700 hover:border-emerald-500 text-gray-400 hover:text-emerald-400'
17
+ : 'border-gray-300 hover:border-emerald-500 text-gray-500 hover:text-emerald-600'),
18
+ ]"
19
+ @dragover.prevent="onDragOver"
20
+ @dragleave.prevent="dragging = false"
21
+ @drop.prevent="onDrop"
22
+ >
23
+ <slot name="icon">
24
+ <svg class="w-10 h-10" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
25
+ <path
26
+ stroke-linecap="round"
27
+ stroke-linejoin="round"
28
+ d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m6.75 12-3-3m0 0-3 3m3-3v6m-1.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
29
+ />
30
+ </svg>
31
+ </slot>
32
+ <p class="text-sm font-medium">{{ label }}</p>
33
+ <p v-if="hint" class="text-xs" :class="isDark ? 'text-gray-500' : 'text-gray-400'">
34
+ {{ hint }}
35
+ </p>
36
+ <input
37
+ type="file"
38
+ class="hidden"
39
+ :accept="accept"
40
+ :multiple="multiple"
41
+ :disabled="disabled"
42
+ @change="onChange"
43
+ />
44
+ </label>
45
+ </template>
46
+
47
+ <script lang="ts" setup>
48
+ import { ref } from 'vue'
49
+ import { useTheme } from '../composables/useTheme'
50
+
51
+ interface Props {
52
+ /** Main line, e.g. "Click to select a Postman collection (.json)". */
53
+ label: string
54
+ /** Optional dimmed helper line below the label. */
55
+ hint?: string
56
+ /** Forwarded to the hidden input's `accept`. Dropped files are NOT filtered by it. */
57
+ accept?: string
58
+ multiple?: boolean
59
+ disabled?: boolean
60
+ }
61
+
62
+ const props = withDefaults(defineProps<Props>(), {
63
+ hint: '',
64
+ accept: '',
65
+ multiple: false,
66
+ disabled: false,
67
+ })
68
+
69
+ const emit = defineEmits<{ files: [files: File[]] }>()
70
+
71
+ const { isDark } = useTheme()
72
+
73
+ const dragging = ref(false)
74
+
75
+ function onChange(e: Event) {
76
+ const input = e.target as HTMLInputElement
77
+ const files = Array.from(input.files ?? [])
78
+ // Reset so re-selecting the same file re-triggers change.
79
+ input.value = ''
80
+ if (files.length) emit('files', files)
81
+ }
82
+
83
+ function onDragOver() {
84
+ if (!props.disabled) dragging.value = true
85
+ }
86
+
87
+ function onDrop(e: DragEvent) {
88
+ dragging.value = false
89
+ if (props.disabled) return
90
+ const dropped = Array.from(e.dataTransfer?.files ?? [])
91
+ if (!dropped.length) return
92
+ emit('files', props.multiple ? dropped : dropped.slice(0, 1))
93
+ }
94
+ </script>
@@ -1,44 +1,44 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { mount } from '@vue/test-utils'
3
- import BaseFilterChip from './BaseFilterChip.vue'
4
-
5
- describe('BaseFilterChip', () => {
6
- it('renders the label prop, overridden by the default slot', () => {
7
- expect(mount(BaseFilterChip, { props: { label: 'Buy' } }).text()).toBe('Buy')
8
- expect(
9
- mount(BaseFilterChip, { props: { label: 'x' }, slots: { default: 'Sell' } }).text(),
10
- ).toBe('Sell')
11
- })
12
-
13
- it('switches from the tinted idle skin to the solid active fill', () => {
14
- const idle = mount(BaseFilterChip, { props: { color: 'sky', active: false } })
15
- expect(idle.get('button').classes()).toContain('bg-sky-50')
16
- expect(idle.get('button').attributes('aria-pressed')).toBe('false')
17
-
18
- const active = mount(BaseFilterChip, { props: { color: 'sky', active: true } })
19
- expect(active.get('button').classes()).toContain('bg-sky-500')
20
- expect(active.get('button').classes()).toContain('text-white')
21
- expect(active.get('button').attributes('aria-pressed')).toBe('true')
22
- })
23
-
24
- it('lets native click listeners through, blocked when disabled', async () => {
25
- let clicks = 0
26
- const chip = mount(BaseFilterChip, {
27
- props: { label: 'Buy' },
28
- attrs: { onClick: () => clicks++ },
29
- })
30
- await chip.get('button').trigger('click')
31
- expect(clicks).toBe(1)
32
-
33
- const off = mount(BaseFilterChip, {
34
- props: { label: 'Buy', disabled: true },
35
- attrs: { onClick: () => clicks++ },
36
- })
37
- expect(off.get('button').attributes('disabled')).toBeDefined()
38
- })
39
-
40
- it('forwards the title tooltip', () => {
41
- const chip = mount(BaseFilterChip, { props: { label: 'Buy', title: 'Show buys' } })
42
- expect(chip.get('button').attributes('title')).toBe('Show buys')
43
- })
44
- })
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import BaseFilterChip from './BaseFilterChip.vue'
4
+
5
+ describe('BaseFilterChip', () => {
6
+ it('renders the label prop, overridden by the default slot', () => {
7
+ expect(mount(BaseFilterChip, { props: { label: 'Buy' } }).text()).toBe('Buy')
8
+ expect(
9
+ mount(BaseFilterChip, { props: { label: 'x' }, slots: { default: 'Sell' } }).text(),
10
+ ).toBe('Sell')
11
+ })
12
+
13
+ it('switches from the tinted idle skin to the solid active fill', () => {
14
+ const idle = mount(BaseFilterChip, { props: { color: 'sky', active: false } })
15
+ expect(idle.get('button').classes()).toContain('bg-sky-50')
16
+ expect(idle.get('button').attributes('aria-pressed')).toBe('false')
17
+
18
+ const active = mount(BaseFilterChip, { props: { color: 'sky', active: true } })
19
+ expect(active.get('button').classes()).toContain('bg-sky-500')
20
+ expect(active.get('button').classes()).toContain('text-white')
21
+ expect(active.get('button').attributes('aria-pressed')).toBe('true')
22
+ })
23
+
24
+ it('lets native click listeners through, blocked when disabled', async () => {
25
+ let clicks = 0
26
+ const chip = mount(BaseFilterChip, {
27
+ props: { label: 'Buy' },
28
+ attrs: { onClick: () => clicks++ },
29
+ })
30
+ await chip.get('button').trigger('click')
31
+ expect(clicks).toBe(1)
32
+
33
+ const off = mount(BaseFilterChip, {
34
+ props: { label: 'Buy', disabled: true },
35
+ attrs: { onClick: () => clicks++ },
36
+ })
37
+ expect(off.get('button').attributes('disabled')).toBeDefined()
38
+ })
39
+
40
+ it('forwards the title tooltip', () => {
41
+ const chip = mount(BaseFilterChip, { props: { label: 'Buy', title: 'Show buys' } })
42
+ expect(chip.get('button').attributes('title')).toBe('Show buys')
43
+ })
44
+ })
@@ -1,67 +1,67 @@
1
- <template>
2
- <!--
3
- Colour-coded toggleable filter chip — the one-click event/category filter
4
- buttons above a data feed (extracted from TradeAutomation's Trade Log).
5
- Idle: tinted border/background in the semantic colour. Active: solid fill
6
- with white text. `aria-pressed` reflects the toggle state.
7
-
8
- Layout (height, flex sizing) is the caller's: pass classes like
9
- `h-9 flex-1` through the class attribute.
10
-
11
- The click handler falls through natively to the root <button> — bind
12
- @click on the component as usual; a disabled chip blocks it.
13
- -->
14
- <button
15
- type="button"
16
- :disabled="disabled"
17
- :title="title"
18
- :aria-pressed="active"
19
- class="whitespace-nowrap rounded-md border px-3 text-sm font-semibold text-center transition-colors focus:outline-none focus-visible:ring-2 disabled:opacity-50 disabled:cursor-not-allowed"
20
- :class="active ? ACTIVE[color] : IDLE[color]"
21
- >
22
- <slot>{{ label }}</slot>
23
- </button>
24
- </template>
25
-
26
- <script setup lang="ts">
27
- type FilterChipColor = 'emerald' | 'sky' | 'amber' | 'red' | 'slate'
28
-
29
- withDefaults(
30
- defineProps<{
31
- /** Chip text; the default slot overrides it. */
32
- label?: string
33
- /** Semantic colour of both the idle tint and the active solid fill. */
34
- color?: FilterChipColor
35
- /** Whether the chip's filter is currently applied (solid fill). */
36
- active?: boolean
37
- disabled?: boolean
38
- /** Native tooltip. */
39
- title?: string
40
- }>(),
41
- { label: '', color: 'slate', active: false, disabled: false, title: undefined },
42
- )
43
-
44
- // Full literal class strings (no dynamic concatenation) so Tailwind detects
45
- // them at build time. Idle: tinted; active: solid with white text.
46
- const IDLE: Record<FilterChipColor, string> = {
47
- emerald:
48
- 'border-emerald-300 dark:border-emerald-700/60 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/30 focus-visible:ring-emerald-500',
49
- sky:
50
- 'border-sky-300 dark:border-sky-700/60 bg-sky-50 dark:bg-sky-900/20 text-sky-700 dark:text-sky-400 hover:bg-sky-100 dark:hover:bg-sky-900/30 focus-visible:ring-sky-500',
51
- amber:
52
- 'border-amber-300 dark:border-amber-700/60 bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/30 focus-visible:ring-amber-500',
53
- red:
54
- 'border-red-300 dark:border-red-700/60 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 focus-visible:ring-red-500',
55
- slate:
56
- 'border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-900 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 focus-visible:ring-emerald-500',
57
- }
58
-
59
- const ACTIVE: Record<FilterChipColor, string> = {
60
- emerald:
61
- 'border-emerald-500 bg-emerald-500 text-white hover:bg-emerald-600 focus-visible:ring-emerald-500',
62
- sky: 'border-sky-500 bg-sky-500 text-white hover:bg-sky-600 focus-visible:ring-sky-500',
63
- amber: 'border-amber-500 bg-amber-500 text-white hover:bg-amber-600 focus-visible:ring-amber-500',
64
- red: 'border-red-500 bg-red-500 text-white hover:bg-red-600 focus-visible:ring-red-500',
65
- slate: 'border-slate-600 bg-slate-600 text-white hover:bg-slate-700 focus-visible:ring-emerald-500',
66
- }
67
- </script>
1
+ <template>
2
+ <!--
3
+ Colour-coded toggleable filter chip — the one-click event/category filter
4
+ buttons above a data feed (extracted from TradeAutomation's Trade Log).
5
+ Idle: tinted border/background in the semantic colour. Active: solid fill
6
+ with white text. `aria-pressed` reflects the toggle state.
7
+
8
+ Layout (height, flex sizing) is the caller's: pass classes like
9
+ `h-9 flex-1` through the class attribute.
10
+
11
+ The click handler falls through natively to the root <button> — bind
12
+ @click on the component as usual; a disabled chip blocks it.
13
+ -->
14
+ <button
15
+ type="button"
16
+ :disabled="disabled"
17
+ :title="title"
18
+ :aria-pressed="active"
19
+ class="whitespace-nowrap rounded-md border px-3 text-sm font-semibold text-center transition-colors focus:outline-none focus-visible:ring-2 disabled:opacity-50 disabled:cursor-not-allowed"
20
+ :class="active ? ACTIVE[color] : IDLE[color]"
21
+ >
22
+ <slot>{{ label }}</slot>
23
+ </button>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ type FilterChipColor = 'emerald' | 'sky' | 'amber' | 'red' | 'slate'
28
+
29
+ withDefaults(
30
+ defineProps<{
31
+ /** Chip text; the default slot overrides it. */
32
+ label?: string
33
+ /** Semantic colour of both the idle tint and the active solid fill. */
34
+ color?: FilterChipColor
35
+ /** Whether the chip's filter is currently applied (solid fill). */
36
+ active?: boolean
37
+ disabled?: boolean
38
+ /** Native tooltip. */
39
+ title?: string
40
+ }>(),
41
+ { label: '', color: 'slate', active: false, disabled: false, title: undefined },
42
+ )
43
+
44
+ // Full literal class strings (no dynamic concatenation) so Tailwind detects
45
+ // them at build time. Idle: tinted; active: solid with white text.
46
+ const IDLE: Record<FilterChipColor, string> = {
47
+ emerald:
48
+ 'border-emerald-300 dark:border-emerald-700/60 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/30 focus-visible:ring-emerald-500',
49
+ sky:
50
+ 'border-sky-300 dark:border-sky-700/60 bg-sky-50 dark:bg-sky-900/20 text-sky-700 dark:text-sky-400 hover:bg-sky-100 dark:hover:bg-sky-900/30 focus-visible:ring-sky-500',
51
+ amber:
52
+ 'border-amber-300 dark:border-amber-700/60 bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/30 focus-visible:ring-amber-500',
53
+ red:
54
+ 'border-red-300 dark:border-red-700/60 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 focus-visible:ring-red-500',
55
+ slate:
56
+ 'border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-900 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 focus-visible:ring-emerald-500',
57
+ }
58
+
59
+ const ACTIVE: Record<FilterChipColor, string> = {
60
+ emerald:
61
+ 'border-emerald-500 bg-emerald-500 text-white hover:bg-emerald-600 focus-visible:ring-emerald-500',
62
+ sky: 'border-sky-500 bg-sky-500 text-white hover:bg-sky-600 focus-visible:ring-sky-500',
63
+ amber: 'border-amber-500 bg-amber-500 text-white hover:bg-amber-600 focus-visible:ring-amber-500',
64
+ red: 'border-red-500 bg-red-500 text-white hover:bg-red-600 focus-visible:ring-red-500',
65
+ slate: 'border-slate-600 bg-slate-600 text-white hover:bg-slate-700 focus-visible:ring-emerald-500',
66
+ }
67
+ </script>