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,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>
|