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,140 +1,140 @@
1
- <template>
2
- <!--
3
- Generic "ask the user for a single string and confirm" dialog.
4
- Used for clone-name prompts, rename dialogs, etc.
5
-
6
- Slots:
7
- • icon — replace the default emerald-tinted document icon.
8
-
9
- Preserves typed input on stray backdrop clicks (only treats the
10
- click as cancel if the user hasn't modified the initial value).
11
- Escape is always treated as an explicit dismissal.
12
- -->
13
- <BaseModalShell
14
- :title="title"
15
- :icon="DocumentDuplicateIcon"
16
- manual-close
17
- @backdrop="handleBackdropClick"
18
- >
19
- <!-- Forward a caller-supplied icon override; otherwise the shell
20
- renders the default document icon in the standard emerald chip. -->
21
- <template v-if="$slots.icon" #icon>
22
- <slot name="icon" />
23
- </template>
24
-
25
- <p class="text-sm" :class="isDark ? 'text-gray-300' : 'text-gray-600'">
26
- {{ message }}
27
- </p>
28
-
29
- <div class="mt-4">
30
- <label
31
- class="block text-sm font-medium mb-1"
32
- :class="t.label"
33
- >
34
- {{ inputLabel }}
35
- </label>
36
- <input
37
- ref="nameInput"
38
- v-model="inputValue"
39
- type="text"
40
- :placeholder="placeholder"
41
- class="w-full px-3 py-2 rounded-md border text-sm transition-colors"
42
- :class="isDark
43
- ? 'bg-gray-800 border-gray-600 text-gray-100 placeholder-gray-500 focus:outline-none focus:border-emerald-500'
44
- : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400 focus:outline-none focus:border-emerald-500'"
45
- @keyup.enter="handleConfirm"
46
- />
47
- </div>
48
-
49
- <template #footer>
50
- <button
51
- type="button"
52
- class="inline-flex items-center justify-center px-4 py-2 border rounded-md shadow-sm text-sm font-medium transition-colors duration-150 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
53
- :class="t.ghostButton"
54
- @click="handleCancel"
55
- >
56
- {{ cancelText }}
57
- </button>
58
- <button
59
- type="button"
60
- :disabled="!inputValue.trim() || submitting"
61
- class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2"
62
- :class="[
63
- !inputValue.trim() || submitting
64
- ? 'bg-emerald-400 cursor-not-allowed'
65
- : 'bg-emerald-600 hover:bg-emerald-700 cursor-pointer'
66
- ]"
67
- @click="handleConfirm"
68
- >
69
- {{ submitting ? submittingText : confirmText }}
70
- </button>
71
- </template>
72
- </BaseModalShell>
73
- </template>
74
-
75
- <script setup lang="ts">
76
- import { ref, onMounted, nextTick } from 'vue'
77
- import { DocumentDuplicateIcon } from '@heroicons/vue/24/outline'
78
- import BaseModalShell from './BaseModalShell.vue'
79
- import { useTheme } from '../composables/useTheme'
80
- import { useThemeClasses } from '../composables/useThemeClasses'
81
- import { useEscapeKey } from '../composables/useEscapeKey'
82
-
83
- interface Props {
84
- title: string
85
- message: string
86
- initialValue?: string
87
- placeholder?: string
88
- inputLabel?: string
89
- confirmText?: string
90
- cancelText?: string
91
- submittingText?: string
92
- submitting?: boolean
93
- }
94
-
95
- const props = withDefaults(defineProps<Props>(), {
96
- initialValue: '',
97
- placeholder: 'Enter value...',
98
- inputLabel: 'Name',
99
- confirmText: 'Confirm',
100
- cancelText: 'Cancel',
101
- submittingText: 'Working...',
102
- submitting: false,
103
- })
104
-
105
- const emit = defineEmits<{
106
- confirm: [value: string]
107
- cancel: []
108
- }>()
109
-
110
- const { isDark } = useTheme()
111
- const t = useThemeClasses()
112
- const inputValue = ref(props.initialValue)
113
- const nameInput = ref<HTMLInputElement | null>(null)
114
-
115
- onMounted(async () => {
116
- await nextTick()
117
- nameInput.value?.focus()
118
- })
119
-
120
- function handleConfirm() {
121
- if (!inputValue.value.trim() || props.submitting) return
122
- emit('confirm', inputValue.value.trim())
123
- }
124
-
125
- function handleCancel() {
126
- emit('cancel')
127
- }
128
-
129
- // Backdrop click: only treat it as cancel if the user hasn't modified
130
- // the value. Otherwise a stray click outside the dialog would silently
131
- // throw away the typed input.
132
- function handleBackdropClick() {
133
- if (inputValue.value.trim() === (props.initialValue ?? '').trim()) {
134
- handleCancel()
135
- }
136
- }
137
-
138
- // Escape always cancels.
139
- useEscapeKey(handleCancel)
140
- </script>
1
+ <template>
2
+ <!--
3
+ Generic "ask the user for a single string and confirm" dialog.
4
+ Used for clone-name prompts, rename dialogs, etc.
5
+
6
+ Slots:
7
+ • icon — replace the default emerald-tinted document icon.
8
+
9
+ Preserves typed input on stray backdrop clicks (only treats the
10
+ click as cancel if the user hasn't modified the initial value).
11
+ Escape is always treated as an explicit dismissal.
12
+ -->
13
+ <BaseModalShell
14
+ :title="title"
15
+ :icon="DocumentDuplicateIcon"
16
+ manual-close
17
+ @backdrop="handleBackdropClick"
18
+ >
19
+ <!-- Forward a caller-supplied icon override; otherwise the shell
20
+ renders the default document icon in the standard emerald chip. -->
21
+ <template v-if="$slots.icon" #icon>
22
+ <slot name="icon" />
23
+ </template>
24
+
25
+ <p class="text-sm" :class="isDark ? 'text-gray-300' : 'text-gray-600'">
26
+ {{ message }}
27
+ </p>
28
+
29
+ <div class="mt-4">
30
+ <label
31
+ class="block text-sm font-medium mb-1"
32
+ :class="t.label"
33
+ >
34
+ {{ inputLabel }}
35
+ </label>
36
+ <input
37
+ ref="nameInput"
38
+ v-model="inputValue"
39
+ type="text"
40
+ :placeholder="placeholder"
41
+ class="w-full px-3 py-2 rounded-md border text-sm transition-colors"
42
+ :class="isDark
43
+ ? 'bg-gray-800 border-gray-600 text-gray-100 placeholder-gray-500 focus:outline-none focus:border-emerald-500'
44
+ : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400 focus:outline-none focus:border-emerald-500'"
45
+ @keyup.enter="handleConfirm"
46
+ />
47
+ </div>
48
+
49
+ <template #footer>
50
+ <button
51
+ type="button"
52
+ class="inline-flex items-center justify-center px-4 py-2 border rounded-md shadow-sm text-sm font-medium transition-colors duration-150 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
53
+ :class="t.ghostButton"
54
+ @click="handleCancel"
55
+ >
56
+ {{ cancelText }}
57
+ </button>
58
+ <button
59
+ type="button"
60
+ :disabled="!inputValue.trim() || submitting"
61
+ class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2"
62
+ :class="[
63
+ !inputValue.trim() || submitting
64
+ ? 'bg-emerald-400 cursor-not-allowed'
65
+ : 'bg-emerald-600 hover:bg-emerald-700 cursor-pointer'
66
+ ]"
67
+ @click="handleConfirm"
68
+ >
69
+ {{ submitting ? submittingText : confirmText }}
70
+ </button>
71
+ </template>
72
+ </BaseModalShell>
73
+ </template>
74
+
75
+ <script setup lang="ts">
76
+ import { ref, onMounted, nextTick } from 'vue'
77
+ import { DocumentDuplicateIcon } from '@heroicons/vue/24/outline'
78
+ import BaseModalShell from './BaseModalShell.vue'
79
+ import { useTheme } from '../composables/useTheme'
80
+ import { useThemeClasses } from '../composables/useThemeClasses'
81
+ import { useEscapeKey } from '../composables/useEscapeKey'
82
+
83
+ interface Props {
84
+ title: string
85
+ message: string
86
+ initialValue?: string
87
+ placeholder?: string
88
+ inputLabel?: string
89
+ confirmText?: string
90
+ cancelText?: string
91
+ submittingText?: string
92
+ submitting?: boolean
93
+ }
94
+
95
+ const props = withDefaults(defineProps<Props>(), {
96
+ initialValue: '',
97
+ placeholder: 'Enter value...',
98
+ inputLabel: 'Name',
99
+ confirmText: 'Confirm',
100
+ cancelText: 'Cancel',
101
+ submittingText: 'Working...',
102
+ submitting: false,
103
+ })
104
+
105
+ const emit = defineEmits<{
106
+ confirm: [value: string]
107
+ cancel: []
108
+ }>()
109
+
110
+ const { isDark } = useTheme()
111
+ const t = useThemeClasses()
112
+ const inputValue = ref(props.initialValue)
113
+ const nameInput = ref<HTMLInputElement | null>(null)
114
+
115
+ onMounted(async () => {
116
+ await nextTick()
117
+ nameInput.value?.focus()
118
+ })
119
+
120
+ function handleConfirm() {
121
+ if (!inputValue.value.trim() || props.submitting) return
122
+ emit('confirm', inputValue.value.trim())
123
+ }
124
+
125
+ function handleCancel() {
126
+ emit('cancel')
127
+ }
128
+
129
+ // Backdrop click: only treat it as cancel if the user hasn't modified
130
+ // the value. Otherwise a stray click outside the dialog would silently
131
+ // throw away the typed input.
132
+ function handleBackdropClick() {
133
+ if (inputValue.value.trim() === (props.initialValue ?? '').trim()) {
134
+ handleCancel()
135
+ }
136
+ }
137
+
138
+ // Escape always cancels.
139
+ useEscapeKey(handleCancel)
140
+ </script>
@@ -1,48 +1,48 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { mount } from '@vue/test-utils'
3
- import BaseToolbarButton from './BaseToolbarButton.vue'
4
-
5
- describe('BaseToolbarButton', () => {
6
- it('renders the neutral pill shape by default', () => {
7
- const wrapper = mount(BaseToolbarButton, { props: { label: 'Refresh' } })
8
- const cls = wrapper.find('button').classes()
9
- expect(cls).toContain('rounded-lg')
10
- expect(cls).toContain('py-2')
11
- expect(cls).toContain('font-medium')
12
- expect(cls).toContain('disabled:opacity-50')
13
- expect(wrapper.text()).toBe('Refresh')
14
- })
15
-
16
- it('keeps the danger variant on the pill shape with the red palette', () => {
17
- const wrapper = mount(BaseToolbarButton, {
18
- props: { label: 'Delete', variant: 'danger' },
19
- })
20
- const cls = wrapper.find('button').classes()
21
- expect(cls).toContain('rounded-lg')
22
- expect(cls).toContain('bg-red-600')
23
- })
24
-
25
- it('renders the ghost variant as the slate h-9 rounded-md outline button', () => {
26
- const wrapper = mount(BaseToolbarButton, {
27
- props: { label: 'Close', variant: 'ghost' },
28
- })
29
- const cls = wrapper.find('button').classes()
30
- expect(cls).toContain('h-9')
31
- expect(cls).toContain('rounded-md')
32
- expect(cls).toContain('px-4')
33
- expect(cls).toContain('font-semibold')
34
- expect(cls).toContain('border-slate-300')
35
- expect(cls).toContain('disabled:opacity-40')
36
- expect(cls).not.toContain('rounded-lg')
37
- expect(cls).not.toContain('py-2')
38
- })
39
-
40
- it('emits click and honours disabled', async () => {
41
- const wrapper = mount(BaseToolbarButton, { props: { label: 'Go' } })
42
- await wrapper.find('button').trigger('click')
43
- expect(wrapper.emitted('click')).toHaveLength(1)
44
-
45
- const disabled = mount(BaseToolbarButton, { props: { label: 'Go', disabled: true } })
46
- expect(disabled.find('button').attributes('disabled')).toBeDefined()
47
- })
48
- })
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import BaseToolbarButton from './BaseToolbarButton.vue'
4
+
5
+ describe('BaseToolbarButton', () => {
6
+ it('renders the neutral pill shape by default', () => {
7
+ const wrapper = mount(BaseToolbarButton, { props: { label: 'Refresh' } })
8
+ const cls = wrapper.find('button').classes()
9
+ expect(cls).toContain('rounded-lg')
10
+ expect(cls).toContain('py-2')
11
+ expect(cls).toContain('font-medium')
12
+ expect(cls).toContain('disabled:opacity-50')
13
+ expect(wrapper.text()).toBe('Refresh')
14
+ })
15
+
16
+ it('keeps the danger variant on the pill shape with the red palette', () => {
17
+ const wrapper = mount(BaseToolbarButton, {
18
+ props: { label: 'Delete', variant: 'danger' },
19
+ })
20
+ const cls = wrapper.find('button').classes()
21
+ expect(cls).toContain('rounded-lg')
22
+ expect(cls).toContain('bg-red-600')
23
+ })
24
+
25
+ it('renders the ghost variant as the slate h-9 rounded-md outline button', () => {
26
+ const wrapper = mount(BaseToolbarButton, {
27
+ props: { label: 'Close', variant: 'ghost' },
28
+ })
29
+ const cls = wrapper.find('button').classes()
30
+ expect(cls).toContain('h-9')
31
+ expect(cls).toContain('rounded-md')
32
+ expect(cls).toContain('px-4')
33
+ expect(cls).toContain('font-semibold')
34
+ expect(cls).toContain('border-slate-300')
35
+ expect(cls).toContain('disabled:opacity-40')
36
+ expect(cls).not.toContain('rounded-lg')
37
+ expect(cls).not.toContain('py-2')
38
+ })
39
+
40
+ it('emits click and honours disabled', async () => {
41
+ const wrapper = mount(BaseToolbarButton, { props: { label: 'Go' } })
42
+ await wrapper.find('button').trigger('click')
43
+ expect(wrapper.emitted('click')).toHaveLength(1)
44
+
45
+ const disabled = mount(BaseToolbarButton, { props: { label: 'Go', disabled: true } })
46
+ expect(disabled.find('button').attributes('disabled')).toBeDefined()
47
+ })
48
+ })
@@ -1,94 +1,94 @@
1
- <template>
2
- <!--
3
- Bordered toolbar button — the "Refresh / Delete All" row that sits
4
- under a page header. Pairs an optional leading icon with a label.
5
-
6
- Slots:
7
- • icon — leading Heroicon. Receives an `iconClass` slot prop
8
- ('w-4 h-4') so callers can spread it onto their icon and still
9
- add their own state classes (e.g. `:class="['animate-spin']"`).
10
-
11
- Props:
12
- • label — button text (rendered in a <span>). Optional so an
13
- icon-only toolbar button is possible.
14
- • variant — 'neutral' (default grey), 'danger' (solid red) or
15
- 'ghost' (slate outline — TradeAutomation's toolbar/footer idiom).
16
- • disabled — greys out + blocks the click.
17
- • title — native tooltip / accessibility text.
18
- • type — native button type. Defaults to 'button' so the
19
- button never accidentally submits a surrounding form.
20
- -->
21
- <button
22
- :type="type"
23
- :disabled="disabled"
24
- :title="title"
25
- class="inline-flex shrink-0 items-center gap-2 text-sm border transition-colors cursor-pointer disabled:cursor-not-allowed whitespace-nowrap focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
26
- :class="variantClass"
27
- @click="$emit('click', $event)"
28
- >
29
- <slot name="icon" :icon-class="'w-4 h-4'" />
30
- <span v-if="label">{{ label }}</span>
31
- </button>
32
- </template>
33
-
34
- <script setup lang="ts">
35
- import { computed } from 'vue'
36
- import { useTheme } from '../composables/useTheme'
37
-
38
- type ToolbarVariant = 'neutral' | 'danger' | 'ghost'
39
-
40
- const props = withDefaults(
41
- defineProps<{
42
- label?: string
43
- variant?: ToolbarVariant
44
- disabled?: boolean
45
- title?: string
46
- type?: 'button' | 'submit' | 'reset'
47
- }>(),
48
- {
49
- label: '',
50
- variant: 'neutral',
51
- disabled: false,
52
- title: undefined,
53
- type: 'button',
54
- },
55
- )
56
-
57
- defineEmits<{
58
- click: [event: MouseEvent]
59
- }>()
60
-
61
- const { isDark } = useTheme()
62
-
63
- // Resolves the variant + theme into shape and colour classes. The danger
64
- // style is normalised here so every destructive toolbar button reads
65
- // identically (red-500 hover in dark, red-700 hover in light). Shape lives
66
- // per-variant because 'ghost' — the slate outline button extracted from
67
- // TradeAutomation's toolbars and modal footers — is a fixed-height
68
- // rounded-md button, not the neutral/danger pill; it themes via dark:
69
- // utilities so it renders the same markup in both themes.
70
- const SHAPE: Record<ToolbarVariant, string> = {
71
- neutral: 'px-3 py-2 rounded-lg font-medium disabled:opacity-50',
72
- danger: 'px-3 py-2 rounded-lg font-medium disabled:opacity-50',
73
- ghost: 'h-9 px-4 rounded-md font-semibold disabled:opacity-40 disabled:hover:bg-slate-50 dark:disabled:hover:bg-slate-900',
74
- }
75
-
76
- const variantClass = computed(() => {
77
- const palette: Record<ToolbarVariant, { dark: string; light: string }> = {
78
- neutral: {
79
- dark: 'bg-gray-800 text-gray-100 border-gray-700 hover:bg-gray-700',
80
- light: 'bg-white text-gray-700 border-gray-200 hover:bg-gray-100',
81
- },
82
- danger: {
83
- dark: 'bg-red-600 text-white border-red-600 hover:bg-red-500',
84
- light: 'bg-red-600 text-white border-red-600 hover:bg-red-700',
85
- },
86
- ghost: {
87
- dark: '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',
88
- light: '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',
89
- },
90
- }
91
- const entry = palette[props.variant]
92
- return [SHAPE[props.variant], isDark.value ? entry.dark : entry.light]
93
- })
94
- </script>
1
+ <template>
2
+ <!--
3
+ Bordered toolbar button — the "Refresh / Delete All" row that sits
4
+ under a page header. Pairs an optional leading icon with a label.
5
+
6
+ Slots:
7
+ • icon — leading Heroicon. Receives an `iconClass` slot prop
8
+ ('w-4 h-4') so callers can spread it onto their icon and still
9
+ add their own state classes (e.g. `:class="['animate-spin']"`).
10
+
11
+ Props:
12
+ • label — button text (rendered in a <span>). Optional so an
13
+ icon-only toolbar button is possible.
14
+ • variant — 'neutral' (default grey), 'danger' (solid red) or
15
+ 'ghost' (slate outline — TradeAutomation's toolbar/footer idiom).
16
+ • disabled — greys out + blocks the click.
17
+ • title — native tooltip / accessibility text.
18
+ • type — native button type. Defaults to 'button' so the
19
+ button never accidentally submits a surrounding form.
20
+ -->
21
+ <button
22
+ :type="type"
23
+ :disabled="disabled"
24
+ :title="title"
25
+ class="inline-flex shrink-0 items-center gap-2 text-sm border transition-colors cursor-pointer disabled:cursor-not-allowed whitespace-nowrap focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500"
26
+ :class="variantClass"
27
+ @click="$emit('click', $event)"
28
+ >
29
+ <slot name="icon" :icon-class="'w-4 h-4'" />
30
+ <span v-if="label">{{ label }}</span>
31
+ </button>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import { computed } from 'vue'
36
+ import { useTheme } from '../composables/useTheme'
37
+
38
+ type ToolbarVariant = 'neutral' | 'danger' | 'ghost'
39
+
40
+ const props = withDefaults(
41
+ defineProps<{
42
+ label?: string
43
+ variant?: ToolbarVariant
44
+ disabled?: boolean
45
+ title?: string
46
+ type?: 'button' | 'submit' | 'reset'
47
+ }>(),
48
+ {
49
+ label: '',
50
+ variant: 'neutral',
51
+ disabled: false,
52
+ title: undefined,
53
+ type: 'button',
54
+ },
55
+ )
56
+
57
+ defineEmits<{
58
+ click: [event: MouseEvent]
59
+ }>()
60
+
61
+ const { isDark } = useTheme()
62
+
63
+ // Resolves the variant + theme into shape and colour classes. The danger
64
+ // style is normalised here so every destructive toolbar button reads
65
+ // identically (red-500 hover in dark, red-700 hover in light). Shape lives
66
+ // per-variant because 'ghost' — the slate outline button extracted from
67
+ // TradeAutomation's toolbars and modal footers — is a fixed-height
68
+ // rounded-md button, not the neutral/danger pill; it themes via dark:
69
+ // utilities so it renders the same markup in both themes.
70
+ const SHAPE: Record<ToolbarVariant, string> = {
71
+ neutral: 'px-3 py-2 rounded-lg font-medium disabled:opacity-50',
72
+ danger: 'px-3 py-2 rounded-lg font-medium disabled:opacity-50',
73
+ ghost: 'h-9 px-4 rounded-md font-semibold disabled:opacity-40 disabled:hover:bg-slate-50 dark:disabled:hover:bg-slate-900',
74
+ }
75
+
76
+ const variantClass = computed(() => {
77
+ const palette: Record<ToolbarVariant, { dark: string; light: string }> = {
78
+ neutral: {
79
+ dark: 'bg-gray-800 text-gray-100 border-gray-700 hover:bg-gray-700',
80
+ light: 'bg-white text-gray-700 border-gray-200 hover:bg-gray-100',
81
+ },
82
+ danger: {
83
+ dark: 'bg-red-600 text-white border-red-600 hover:bg-red-500',
84
+ light: 'bg-red-600 text-white border-red-600 hover:bg-red-700',
85
+ },
86
+ ghost: {
87
+ dark: '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',
88
+ light: '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',
89
+ },
90
+ }
91
+ const entry = palette[props.variant]
92
+ return [SHAPE[props.variant], isDark.value ? entry.dark : entry.light]
93
+ })
94
+ </script>