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