@tak-ps/vue-tabler 4.8.0 → 4.9.0
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/CHANGELOG.md +4 -0
- package/components/Badge.vue +136 -0
- package/lib.ts +1 -0
- package/package.json +1 -1
- package/test/Badge.spec.ts +52 -0
- package/test/Button.spec.ts +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
class='badge tabler-badge'
|
|
4
|
+
:style='badgeStyle'
|
|
5
|
+
>
|
|
6
|
+
<slot />
|
|
7
|
+
</span>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang='ts'>
|
|
11
|
+
import { computed } from 'vue';
|
|
12
|
+
|
|
13
|
+
export interface BadgeProps {
|
|
14
|
+
backgroundColor?: string;
|
|
15
|
+
borderColor?: string;
|
|
16
|
+
textColor?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<BadgeProps>(), {
|
|
20
|
+
backgroundColor: 'rgba(34, 197, 94, 0.2)',
|
|
21
|
+
borderColor: undefined,
|
|
22
|
+
textColor: '#ffffff',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
type ParsedColor = {
|
|
26
|
+
red: number;
|
|
27
|
+
green: number;
|
|
28
|
+
blue: number;
|
|
29
|
+
alpha?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const HEX_COLOR = /^#([\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i;
|
|
33
|
+
const RGB_COLOR = /^rgba?\(([^)]+)\)$/i;
|
|
34
|
+
|
|
35
|
+
function clampChannel(value: number): number {
|
|
36
|
+
return Math.max(0, Math.min(255, Math.round(value)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clampAlpha(value: number): number {
|
|
40
|
+
return Math.max(0, Math.min(1, value));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseHexColor(value: string): ParsedColor | null {
|
|
44
|
+
const match = value.match(HEX_COLOR);
|
|
45
|
+
if (!match) return null;
|
|
46
|
+
|
|
47
|
+
const hex = match[1];
|
|
48
|
+
|
|
49
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
50
|
+
const red = Number.parseInt(`${hex[0]}${hex[0]}`, 16);
|
|
51
|
+
const green = Number.parseInt(`${hex[1]}${hex[1]}`, 16);
|
|
52
|
+
const blue = Number.parseInt(`${hex[2]}${hex[2]}`, 16);
|
|
53
|
+
const alpha = hex.length === 4
|
|
54
|
+
? clampAlpha(Number.parseInt(`${hex[3]}${hex[3]}`, 16) / 255)
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
return { red, green, blue, alpha };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const red = Number.parseInt(hex.slice(0, 2), 16);
|
|
61
|
+
const green = Number.parseInt(hex.slice(2, 4), 16);
|
|
62
|
+
const blue = Number.parseInt(hex.slice(4, 6), 16);
|
|
63
|
+
const alpha = hex.length === 8
|
|
64
|
+
? clampAlpha(Number.parseInt(hex.slice(6, 8), 16) / 255)
|
|
65
|
+
: undefined;
|
|
66
|
+
|
|
67
|
+
return { red, green, blue, alpha };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseRgbColor(value: string): ParsedColor | null {
|
|
71
|
+
const match = value.match(RGB_COLOR);
|
|
72
|
+
if (!match) return null;
|
|
73
|
+
|
|
74
|
+
const parts = match[1].split(',').map((part) => part.trim());
|
|
75
|
+
if (parts.length < 3 || parts.length > 4) return null;
|
|
76
|
+
|
|
77
|
+
const red = Number(parts[0]);
|
|
78
|
+
const green = Number(parts[1]);
|
|
79
|
+
const blue = Number(parts[2]);
|
|
80
|
+
const alpha = parts[3] === undefined ? undefined : Number(parts[3]);
|
|
81
|
+
|
|
82
|
+
if ([red, green, blue, alpha].some((part) => part !== undefined && Number.isNaN(part))) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
red: clampChannel(red),
|
|
88
|
+
green: clampChannel(green),
|
|
89
|
+
blue: clampChannel(blue),
|
|
90
|
+
alpha: alpha === undefined ? undefined : clampAlpha(alpha),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseColor(value: string): ParsedColor | null {
|
|
95
|
+
return parseHexColor(value) || parseRgbColor(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function darkenColor(value: string, amount = 0.2): string {
|
|
99
|
+
const parsed = parseColor(value);
|
|
100
|
+
if (!parsed) return value;
|
|
101
|
+
|
|
102
|
+
const factor = 1 - amount;
|
|
103
|
+
const next = {
|
|
104
|
+
red: clampChannel(parsed.red * factor),
|
|
105
|
+
green: clampChannel(parsed.green * factor),
|
|
106
|
+
blue: clampChannel(parsed.blue * factor),
|
|
107
|
+
alpha: parsed.alpha,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (next.alpha === undefined) {
|
|
111
|
+
return `rgb(${next.red}, ${next.green}, ${next.blue})`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return `rgba(${next.red}, ${next.green}, ${next.blue}, ${next.alpha})`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const resolvedBorderColor = computed(() => {
|
|
118
|
+
if (props.borderColor) return props.borderColor;
|
|
119
|
+
return darkenColor(props.backgroundColor, 0.2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const badgeStyle = computed(() => {
|
|
123
|
+
return {
|
|
124
|
+
backgroundColor: props.backgroundColor,
|
|
125
|
+
borderColor: resolvedBorderColor.value,
|
|
126
|
+
color: props.textColor,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<style scoped>
|
|
132
|
+
.tabler-badge {
|
|
133
|
+
border-style: solid;
|
|
134
|
+
border-width: 1px;
|
|
135
|
+
}
|
|
136
|
+
</style>
|
package/lib.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { default as TablerEnum } from './components/input/Enum.vue';
|
|
|
10
10
|
|
|
11
11
|
export { default as TablerAlert } from './components/Alert.vue'
|
|
12
12
|
export { default as TablerInlineAlert } from './components/InlineAlert.vue'
|
|
13
|
+
export { default as TablerBadge } from './components/Badge.vue'
|
|
13
14
|
export { default as TablerButton } from './components/Button.vue'
|
|
14
15
|
export { default as TablerRefreshButton } from './components/RefreshButton.vue'
|
|
15
16
|
export { default as TablerIconButton } from './components/IconButton.vue'
|
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import Badge from '../components/Badge.vue'
|
|
4
|
+
|
|
5
|
+
describe('TablerBadge', () => {
|
|
6
|
+
it('renders the default slot content', () => {
|
|
7
|
+
const wrapper = mount(Badge, {
|
|
8
|
+
slots: {
|
|
9
|
+
default: 'Allowed',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
expect(wrapper.get('span').text().trim()).toBe('Allowed')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('applies the default badge colors', () => {
|
|
17
|
+
const wrapper = mount(Badge)
|
|
18
|
+
const badge = wrapper.get('span').element as HTMLSpanElement
|
|
19
|
+
|
|
20
|
+
expect(badge.style.backgroundColor).toBe('rgba(34, 197, 94, 0.2)')
|
|
21
|
+
expect(badge.style.borderColor).toBe('rgba(27, 158, 75, 0.2)')
|
|
22
|
+
expect(badge.style.color).toBe('rgb(255, 255, 255)')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('derives a darker border color from the provided background color', () => {
|
|
26
|
+
const wrapper = mount(Badge, {
|
|
27
|
+
props: {
|
|
28
|
+
backgroundColor: '#336699',
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const badge = wrapper.get('span').element as HTMLSpanElement
|
|
33
|
+
|
|
34
|
+
expect(badge.style.backgroundColor).toBe('rgb(51, 102, 153)')
|
|
35
|
+
expect(badge.style.borderColor).toBe('rgb(41, 82, 122)')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('prefers explicit border and text colors when provided', () => {
|
|
39
|
+
const wrapper = mount(Badge, {
|
|
40
|
+
props: {
|
|
41
|
+
backgroundColor: 'rgba(1, 2, 3, 0.4)',
|
|
42
|
+
borderColor: '#123456',
|
|
43
|
+
textColor: '#abcdef',
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const badge = wrapper.get('span').element as HTMLSpanElement
|
|
48
|
+
|
|
49
|
+
expect(badge.style.borderColor).toBe('rgb(18, 52, 86)')
|
|
50
|
+
expect(badge.style.color).toBe('rgb(171, 205, 239)')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import Button from '../components/Button.vue'
|
|
4
|
+
|
|
5
|
+
describe('TablerButton', () => {
|
|
6
|
+
it('renders the default slot content', () => {
|
|
7
|
+
const wrapper = mount(Button, {
|
|
8
|
+
slots: {
|
|
9
|
+
default: 'Click Me',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
expect(wrapper.get('button').text().trim()).toBe('Click Me')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('sets the disabled attribute when disabled is true', () => {
|
|
17
|
+
const wrapper = mount(Button, {
|
|
18
|
+
props: {
|
|
19
|
+
disabled: true,
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(wrapper.get('button').attributes('disabled')).toBeDefined()
|
|
24
|
+
expect((wrapper.get('button').element as HTMLButtonElement).disabled).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('applies the provided background color', () => {
|
|
28
|
+
const wrapper = mount(Button, {
|
|
29
|
+
props: {
|
|
30
|
+
color: 'red',
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect((wrapper.get('button').element as HTMLButtonElement).style.backgroundColor).toBe('red')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('does not apply a background color when color is omitted', () => {
|
|
38
|
+
const wrapper = mount(Button)
|
|
39
|
+
|
|
40
|
+
expect((wrapper.get('button').element as HTMLButtonElement).style.backgroundColor).toBe('')
|
|
41
|
+
})
|
|
42
|
+
})
|