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.
Files changed (62) hide show
  1. package/README.md +1794 -1686
  2. package/dist/components/BaseBarDistribution.vue.d.ts +24 -0
  3. package/dist/components/BasePageHeader.vue.d.ts +8 -2
  4. package/dist/index.d.ts +2 -0
  5. package/dist/types/distribution.d.ts +7 -0
  6. package/dist/ui-lib.css +1 -1
  7. package/dist/ui-lib.js +634 -574
  8. package/dist/ui-lib.umd.cjs +1 -1
  9. package/dist/utils/specForm.d.ts +1 -1
  10. package/package.json +54 -54
  11. package/src/components/BaseAlert.vue +104 -104
  12. package/src/components/BaseBarDistribution.test.ts +51 -0
  13. package/src/components/BaseBarDistribution.vue +73 -0
  14. package/src/components/BaseChipButton.vue +42 -42
  15. package/src/components/BaseCodeBlock.test.ts +70 -70
  16. package/src/components/BaseCodeBlock.vue +74 -74
  17. package/src/components/BaseCollapsibleSection.vue +113 -113
  18. package/src/components/BaseConfirmModal.vue +114 -114
  19. package/src/components/BaseCredentialsForm.test.ts +108 -108
  20. package/src/components/BaseCredentialsForm.vue +241 -241
  21. package/src/components/BaseDropdown.test.ts +87 -87
  22. package/src/components/BaseDropdown.vue +172 -172
  23. package/src/components/BaseEntityPickerModal.vue +330 -330
  24. package/src/components/BaseFileDropzone.vue +94 -94
  25. package/src/components/BaseFilterChip.test.ts +44 -44
  26. package/src/components/BaseFilterChip.vue +67 -67
  27. package/src/components/BaseModalShell.vue +209 -209
  28. package/src/components/BasePageHeader.vue +120 -103
  29. package/src/components/BasePillPickerModal.test.ts +95 -95
  30. package/src/components/BaseRemoveButton.vue +22 -22
  31. package/src/components/BaseSpecFields.test.ts +39 -0
  32. package/src/components/BaseSpecFields.vue +27 -3
  33. package/src/components/BaseStatBreakdown.test.ts +56 -56
  34. package/src/components/BaseStatBreakdown.vue +49 -49
  35. package/src/components/BaseStatusPill.vue +54 -54
  36. package/src/components/BaseTextInputModal.vue +140 -140
  37. package/src/components/BaseToolbarButton.test.ts +48 -48
  38. package/src/components/BaseToolbarButton.vue +94 -94
  39. package/src/composables/useFieldClasses.test.ts +55 -55
  40. package/src/composables/useFieldClasses.ts +56 -56
  41. package/src/composables/usePolling.test.ts +112 -112
  42. package/src/composables/usePolling.ts +102 -90
  43. package/src/composables/useQueryParamSync.test.ts +56 -56
  44. package/src/composables/useQueryParamSync.ts +42 -42
  45. package/src/composables/useThemeClasses.test.ts +34 -34
  46. package/src/composables/useThemeClasses.ts +137 -137
  47. package/src/index.ts +138 -136
  48. package/src/style.css +32 -32
  49. package/src/types/distribution.ts +7 -0
  50. package/src/types/pillPicker.ts +14 -14
  51. package/src/types/specField.ts +30 -30
  52. package/src/types/statBreakdown.ts +6 -6
  53. package/src/utils/format.test.ts +264 -264
  54. package/src/utils/format.ts +216 -216
  55. package/src/utils/httpColors.test.ts +100 -100
  56. package/src/utils/httpColors.ts +99 -99
  57. package/src/utils/kvRows.test.ts +47 -47
  58. package/src/utils/kvRows.ts +34 -34
  59. package/src/utils/specForm.test.ts +64 -53
  60. package/src/utils/specForm.ts +49 -48
  61. package/src/utils/validate.test.ts +82 -82
  62. 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>