@datametria/vue-components 2.0.1 → 2.1.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/README.md +31 -25
- package/dist/index.es.js +1687 -1539
- package/dist/index.umd.js +9 -9
- package/dist/vue-components.css +1 -1
- package/package.json +2 -2
- package/src/components/DatametriaFloatingBar.vue +126 -0
- package/src/components/DatametriaSidebar.vue +230 -0
- package/src/components/DatametriaTabs.vue +150 -29
- package/src/components/__tests__/DatametriaFloatingBar.test.ts +137 -0
- package/src/components/__tests__/DatametriaSidebar.test.ts +169 -0
- package/src/components/__tests__/DatametriaTabs.test.ts +232 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<aside
|
|
3
|
+
:class="sidebarClasses"
|
|
4
|
+
role="complementary"
|
|
5
|
+
:aria-label="ariaLabel"
|
|
6
|
+
:aria-expanded="isOpen"
|
|
7
|
+
>
|
|
8
|
+
<div class="dm-sidebar__header" v-if="$slots.header">
|
|
9
|
+
<slot name="header"></slot>
|
|
10
|
+
<button
|
|
11
|
+
v-if="collapsible"
|
|
12
|
+
class="dm-sidebar__toggle"
|
|
13
|
+
@click="toggle"
|
|
14
|
+
:aria-label="isOpen ? 'Fechar sidebar' : 'Abrir sidebar'"
|
|
15
|
+
>
|
|
16
|
+
<span class="dm-sidebar__toggle-icon" :class="{ 'dm-sidebar__toggle-icon--open': isOpen }"></span>
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<nav class="dm-sidebar__content">
|
|
21
|
+
<slot></slot>
|
|
22
|
+
</nav>
|
|
23
|
+
|
|
24
|
+
<div class="dm-sidebar__footer" v-if="$slots.footer">
|
|
25
|
+
<slot name="footer"></slot>
|
|
26
|
+
</div>
|
|
27
|
+
</aside>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { ref, computed, watch } from 'vue'
|
|
32
|
+
|
|
33
|
+
interface Props {
|
|
34
|
+
position?: 'left' | 'right'
|
|
35
|
+
variant?: 'light' | 'dark' | 'primary'
|
|
36
|
+
width?: string
|
|
37
|
+
collapsible?: boolean
|
|
38
|
+
defaultOpen?: boolean
|
|
39
|
+
ariaLabel?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
43
|
+
position: 'left',
|
|
44
|
+
variant: 'light',
|
|
45
|
+
width: '280px',
|
|
46
|
+
collapsible: false,
|
|
47
|
+
defaultOpen: true,
|
|
48
|
+
ariaLabel: 'Sidebar navigation'
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const emit = defineEmits<{
|
|
52
|
+
toggle: [isOpen: boolean]
|
|
53
|
+
open: []
|
|
54
|
+
close: []
|
|
55
|
+
}>()
|
|
56
|
+
|
|
57
|
+
const isOpen = ref(props.defaultOpen)
|
|
58
|
+
|
|
59
|
+
const sidebarClasses = computed(() => [
|
|
60
|
+
'dm-sidebar',
|
|
61
|
+
`dm-sidebar--${props.position}`,
|
|
62
|
+
`dm-sidebar--${props.variant}`,
|
|
63
|
+
{ 'dm-sidebar--collapsed': !isOpen.value && props.collapsible }
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
const toggle = () => {
|
|
67
|
+
isOpen.value = !isOpen.value
|
|
68
|
+
emit('toggle', isOpen.value)
|
|
69
|
+
if (isOpen.value) {
|
|
70
|
+
emit('open')
|
|
71
|
+
} else {
|
|
72
|
+
emit('close')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
watch(() => props.defaultOpen, (newVal) => {
|
|
77
|
+
isOpen.value = newVal
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
defineExpose({ isOpen, toggle })
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<style scoped>
|
|
84
|
+
.dm-sidebar {
|
|
85
|
+
height: 100vh;
|
|
86
|
+
background: var(--dm-neutral-50, #ffffff);
|
|
87
|
+
border-right: 1px solid var(--dm-neutral-200, #e5e7eb);
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
transition: width 0.3s ease;
|
|
91
|
+
width: v-bind(width);
|
|
92
|
+
position: fixed;
|
|
93
|
+
top: 0;
|
|
94
|
+
z-index: 90;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.dm-sidebar--left {
|
|
98
|
+
left: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.dm-sidebar--right {
|
|
102
|
+
right: 0;
|
|
103
|
+
border-right: none;
|
|
104
|
+
border-left: 1px solid var(--dm-neutral-200, #e5e7eb);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.dm-sidebar--dark {
|
|
108
|
+
background: var(--dm-neutral-900, #111827);
|
|
109
|
+
border-color: var(--dm-neutral-800, #1f2937);
|
|
110
|
+
color: var(--dm-neutral-50, #ffffff);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.dm-sidebar--primary {
|
|
114
|
+
background: var(--dm-primary, #0072CE);
|
|
115
|
+
border-color: color-mix(in srgb, var(--dm-primary, #0072CE) 90%, black);
|
|
116
|
+
color: var(--dm-neutral-50, #ffffff);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.dm-sidebar--collapsed {
|
|
120
|
+
width: 64px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.dm-sidebar__header {
|
|
124
|
+
padding: var(--dm-spacing-4, 1rem);
|
|
125
|
+
border-bottom: 1px solid var(--dm-neutral-200, #e5e7eb);
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: space-between;
|
|
129
|
+
min-height: 64px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.dm-sidebar--dark .dm-sidebar__header {
|
|
133
|
+
border-color: var(--dm-neutral-800, #1f2937);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.dm-sidebar--primary .dm-sidebar__header {
|
|
137
|
+
border-color: color-mix(in srgb, var(--dm-primary, #0072CE) 90%, black);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.dm-sidebar__toggle {
|
|
141
|
+
width: 32px;
|
|
142
|
+
height: 32px;
|
|
143
|
+
padding: 0;
|
|
144
|
+
border: none;
|
|
145
|
+
background: transparent;
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
border-radius: var(--dm-radius-md, 0.375rem);
|
|
151
|
+
transition: background 0.2s ease;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.dm-sidebar__toggle:hover {
|
|
155
|
+
background: var(--dm-neutral-100, #f3f4f6);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.dm-sidebar--dark .dm-sidebar__toggle:hover {
|
|
159
|
+
background: var(--dm-neutral-800, #1f2937);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.dm-sidebar__toggle-icon {
|
|
163
|
+
width: 16px;
|
|
164
|
+
height: 2px;
|
|
165
|
+
background: var(--dm-neutral-900, #111827);
|
|
166
|
+
position: relative;
|
|
167
|
+
transition: transform 0.3s ease;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.dm-sidebar--dark .dm-sidebar__toggle-icon,
|
|
171
|
+
.dm-sidebar--primary .dm-sidebar__toggle-icon {
|
|
172
|
+
background: var(--dm-neutral-50, #ffffff);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.dm-sidebar__toggle-icon::before,
|
|
176
|
+
.dm-sidebar__toggle-icon::after {
|
|
177
|
+
content: '';
|
|
178
|
+
position: absolute;
|
|
179
|
+
width: 16px;
|
|
180
|
+
height: 2px;
|
|
181
|
+
background: inherit;
|
|
182
|
+
transition: transform 0.3s ease;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.dm-sidebar__toggle-icon::before {
|
|
186
|
+
top: -5px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.dm-sidebar__toggle-icon::after {
|
|
190
|
+
bottom: -5px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.dm-sidebar__toggle-icon--open {
|
|
194
|
+
transform: rotate(180deg);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.dm-sidebar__content {
|
|
198
|
+
flex: 1;
|
|
199
|
+
overflow-y: auto;
|
|
200
|
+
padding: var(--dm-spacing-4, 1rem);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.dm-sidebar__footer {
|
|
204
|
+
padding: var(--dm-spacing-4, 1rem);
|
|
205
|
+
border-top: 1px solid var(--dm-neutral-200, #e5e7eb);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.dm-sidebar--dark .dm-sidebar__footer {
|
|
209
|
+
border-color: var(--dm-neutral-800, #1f2937);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.dm-sidebar--primary .dm-sidebar__footer {
|
|
213
|
+
border-color: color-mix(in srgb, var(--dm-primary, #0072CE) 90%, black);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@media (max-width: 768px) {
|
|
217
|
+
.dm-sidebar {
|
|
218
|
+
transform: translateX(0);
|
|
219
|
+
transition: transform 0.3s ease;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.dm-sidebar--left.dm-sidebar--collapsed {
|
|
223
|
+
transform: translateX(-100%);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.dm-sidebar--right.dm-sidebar--collapsed {
|
|
227
|
+
transform: translateX(100%);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
</style>
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="
|
|
3
|
-
<div class="dm-tabs__header" role="tablist" :aria-label="ariaLabel">
|
|
2
|
+
<div :class="tabsClasses">
|
|
3
|
+
<div class="dm-tabs__header" role="tablist" :aria-label="ariaLabel" :aria-orientation="orientation">
|
|
4
4
|
<button
|
|
5
5
|
v-for="(tab, index) in tabs"
|
|
6
6
|
:key="index"
|
|
7
7
|
:id="`tab-${index}`"
|
|
8
8
|
class="dm-tabs__tab"
|
|
9
|
-
:class="{ 'dm-tabs__tab--active': activeTab === index }"
|
|
9
|
+
:class="{ 'dm-tabs__tab--active': activeTab === index, 'dm-tabs__tab--disabled': typeof tab === 'object' && 'disabled' in tab && tab.disabled }"
|
|
10
10
|
role="tab"
|
|
11
11
|
:aria-selected="activeTab === index"
|
|
12
12
|
:aria-controls="`panel-${index}`"
|
|
13
|
+
:aria-disabled="typeof tab === 'object' && 'disabled' in tab ? tab.disabled : false"
|
|
13
14
|
:tabindex="activeTab === index ? 0 : -1"
|
|
15
|
+
:disabled="typeof tab === 'object' && 'disabled' in tab ? tab.disabled : false"
|
|
14
16
|
@click="selectTab(index)"
|
|
15
17
|
@keydown="handleKeydown($event, index)"
|
|
16
18
|
>
|
|
17
|
-
{{ tab }}
|
|
19
|
+
<span v-if="typeof tab === 'object' && 'icon' in tab && tab.icon" class="dm-tabs__icon">{{ tab.icon }}</span>
|
|
20
|
+
<span class="dm-tabs__label">{{ typeof tab === 'string' ? tab : tab.label }}</span>
|
|
21
|
+
<span v-if="typeof tab === 'object' && 'badge' in tab && tab.badge" class="dm-tabs__badge">{{ tab.badge }}</span>
|
|
18
22
|
</button>
|
|
19
23
|
<div
|
|
24
|
+
v-if="showIndicator"
|
|
20
25
|
class="dm-tabs__indicator"
|
|
21
|
-
:style="
|
|
26
|
+
:style="indicatorStyle"
|
|
22
27
|
></div>
|
|
23
28
|
</div>
|
|
24
29
|
<div class="dm-tabs__panels">
|
|
@@ -39,21 +44,35 @@
|
|
|
39
44
|
</template>
|
|
40
45
|
|
|
41
46
|
<script setup lang="ts">
|
|
42
|
-
import { ref, watch } from 'vue'
|
|
47
|
+
import { ref, watch, computed } from 'vue'
|
|
48
|
+
|
|
49
|
+
interface Tab {
|
|
50
|
+
label: string
|
|
51
|
+
icon?: string
|
|
52
|
+
badge?: string | number
|
|
53
|
+
disabled?: boolean
|
|
54
|
+
}
|
|
43
55
|
|
|
44
56
|
interface Props {
|
|
45
|
-
tabs: string[]
|
|
57
|
+
tabs: (string | Tab)[]
|
|
46
58
|
modelValue?: number
|
|
59
|
+
variant?: 'default' | 'pills' | 'underline'
|
|
60
|
+
orientation?: 'horizontal' | 'vertical'
|
|
61
|
+
showIndicator?: boolean
|
|
47
62
|
ariaLabel?: string
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
const props = withDefaults(defineProps<Props>(), {
|
|
51
66
|
modelValue: 0,
|
|
67
|
+
variant: 'default',
|
|
68
|
+
orientation: 'horizontal',
|
|
69
|
+
showIndicator: true,
|
|
52
70
|
ariaLabel: 'Tabs'
|
|
53
71
|
})
|
|
54
72
|
|
|
55
73
|
const emit = defineEmits<{
|
|
56
74
|
'update:modelValue': [index: number]
|
|
75
|
+
change: [index: number]
|
|
57
76
|
}>()
|
|
58
77
|
|
|
59
78
|
const activeTab = ref(props.modelValue)
|
|
@@ -62,20 +81,59 @@ watch(() => props.modelValue, (newValue) => {
|
|
|
62
81
|
activeTab.value = newValue
|
|
63
82
|
})
|
|
64
83
|
|
|
84
|
+
const tabsClasses = computed(() => [
|
|
85
|
+
'dm-tabs',
|
|
86
|
+
`dm-tabs--${props.variant}`,
|
|
87
|
+
`dm-tabs--${props.orientation}`
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
const indicatorStyle = computed(() => {
|
|
91
|
+
const count = props.tabs.length
|
|
92
|
+
const position = props.orientation === 'horizontal'
|
|
93
|
+
? `translateX(${activeTab.value * 100}%)`
|
|
94
|
+
: `translateY(${activeTab.value * 100}%)`
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
transform: position,
|
|
98
|
+
width: props.orientation === 'horizontal' ? `${100 / count}%` : '100%',
|
|
99
|
+
height: props.orientation === 'vertical' ? `${100 / count}%` : '2px'
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
65
103
|
const selectTab = (index: number) => {
|
|
104
|
+
const tab = props.tabs[index]
|
|
105
|
+
const isDisabled = typeof tab === 'object' && 'disabled' in tab && tab.disabled
|
|
106
|
+
|
|
107
|
+
if (isDisabled) return
|
|
108
|
+
|
|
66
109
|
activeTab.value = index
|
|
67
110
|
emit('update:modelValue', index)
|
|
111
|
+
emit('change', index)
|
|
68
112
|
}
|
|
69
113
|
|
|
70
114
|
const handleKeydown = (event: KeyboardEvent, index: number) => {
|
|
71
115
|
let newIndex = index
|
|
72
116
|
|
|
117
|
+
const isHorizontal = props.orientation === 'horizontal'
|
|
118
|
+
|
|
73
119
|
switch (event.key) {
|
|
74
120
|
case 'ArrowLeft':
|
|
121
|
+
if (!isHorizontal) return
|
|
75
122
|
event.preventDefault()
|
|
76
123
|
newIndex = index > 0 ? index - 1 : props.tabs.length - 1
|
|
77
124
|
break
|
|
78
125
|
case 'ArrowRight':
|
|
126
|
+
if (!isHorizontal) return
|
|
127
|
+
event.preventDefault()
|
|
128
|
+
newIndex = index < props.tabs.length - 1 ? index + 1 : 0
|
|
129
|
+
break
|
|
130
|
+
case 'ArrowUp':
|
|
131
|
+
if (isHorizontal) return
|
|
132
|
+
event.preventDefault()
|
|
133
|
+
newIndex = index > 0 ? index - 1 : props.tabs.length - 1
|
|
134
|
+
break
|
|
135
|
+
case 'ArrowDown':
|
|
136
|
+
if (isHorizontal) return
|
|
79
137
|
event.preventDefault()
|
|
80
138
|
newIndex = index < props.tabs.length - 1 ? index + 1 : 0
|
|
81
139
|
break
|
|
@@ -102,10 +160,24 @@ const handleKeydown = (event: KeyboardEvent, index: number) => {
|
|
|
102
160
|
flex-direction: column;
|
|
103
161
|
}
|
|
104
162
|
|
|
163
|
+
.dm-tabs--vertical {
|
|
164
|
+
flex-direction: row;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.dm-tabs--vertical .dm-tabs__header {
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
border-bottom: none;
|
|
170
|
+
border-right: 2px solid var(--dm-neutral-200, #e5e7eb);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.dm-tabs--vertical .dm-tabs__tab {
|
|
174
|
+
text-align: left;
|
|
175
|
+
}
|
|
176
|
+
|
|
105
177
|
.dm-tabs__header {
|
|
106
178
|
display: flex;
|
|
107
179
|
position: relative;
|
|
108
|
-
border-bottom: 2px solid var(--dm-
|
|
180
|
+
border-bottom: 2px solid var(--dm-neutral-200, #e5e7eb);
|
|
109
181
|
overflow-x: auto;
|
|
110
182
|
scrollbar-width: none;
|
|
111
183
|
}
|
|
@@ -117,43 +189,95 @@ const handleKeydown = (event: KeyboardEvent, index: number) => {
|
|
|
117
189
|
.dm-tabs__tab {
|
|
118
190
|
flex: 1;
|
|
119
191
|
min-width: max-content;
|
|
120
|
-
padding: var(--dm-
|
|
192
|
+
padding: var(--dm-spacing-3, 0.75rem) var(--dm-spacing-4, 1rem);
|
|
121
193
|
border: none;
|
|
122
194
|
background: transparent;
|
|
123
|
-
color: var(--dm-
|
|
124
|
-
font-size:
|
|
195
|
+
color: var(--dm-neutral-600, #6b7280);
|
|
196
|
+
font-size: 1rem;
|
|
125
197
|
font-weight: 500;
|
|
126
198
|
cursor: pointer;
|
|
127
|
-
transition:
|
|
199
|
+
transition: all 0.2s ease;
|
|
128
200
|
position: relative;
|
|
129
201
|
white-space: nowrap;
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
gap: var(--dm-spacing-2, 0.5rem);
|
|
130
205
|
}
|
|
131
206
|
|
|
132
|
-
.dm-tabs__tab
|
|
133
|
-
|
|
207
|
+
.dm-tabs__tab--disabled {
|
|
208
|
+
opacity: 0.5;
|
|
209
|
+
cursor: not-allowed;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.dm-tabs__icon {
|
|
213
|
+
font-size: 1.25rem;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.dm-tabs__badge {
|
|
217
|
+
display: inline-flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
justify-content: center;
|
|
220
|
+
min-width: 20px;
|
|
221
|
+
height: 20px;
|
|
222
|
+
padding: 0 6px;
|
|
223
|
+
background: var(--dm-primary, #0072CE);
|
|
224
|
+
color: white;
|
|
225
|
+
font-size: 0.75rem;
|
|
226
|
+
font-weight: 600;
|
|
227
|
+
border-radius: 10px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.dm-tabs__tab:hover:not(:disabled) {
|
|
231
|
+
color: var(--dm-neutral-900, #111827);
|
|
134
232
|
}
|
|
135
233
|
|
|
136
234
|
.dm-tabs__tab:focus-visible {
|
|
137
|
-
outline: var(--dm-
|
|
235
|
+
outline: 2px solid var(--dm-primary, #0072CE);
|
|
138
236
|
outline-offset: -2px;
|
|
237
|
+
border-radius: 4px;
|
|
139
238
|
}
|
|
140
239
|
|
|
141
240
|
.dm-tabs__tab--active {
|
|
142
|
-
color: var(--dm-primary);
|
|
241
|
+
color: var(--dm-primary, #0072CE);
|
|
143
242
|
}
|
|
144
243
|
|
|
145
244
|
.dm-tabs__indicator {
|
|
146
245
|
position: absolute;
|
|
147
246
|
bottom: -2px;
|
|
148
247
|
left: 0;
|
|
149
|
-
|
|
150
|
-
height: 2px;
|
|
151
|
-
background: var(--dm-primary);
|
|
248
|
+
background: var(--dm-primary, #0072CE);
|
|
152
249
|
transition: transform 0.3s ease;
|
|
153
250
|
}
|
|
154
251
|
|
|
252
|
+
.dm-tabs--vertical .dm-tabs__indicator {
|
|
253
|
+
bottom: auto;
|
|
254
|
+
left: auto;
|
|
255
|
+
right: -2px;
|
|
256
|
+
top: 0;
|
|
257
|
+
width: 2px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.dm-tabs--pills .dm-tabs__header {
|
|
261
|
+
border-bottom: none;
|
|
262
|
+
gap: var(--dm-spacing-2, 0.5rem);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.dm-tabs--pills .dm-tabs__tab {
|
|
266
|
+
border-radius: var(--dm-radius-md, 0.375rem);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.dm-tabs--pills .dm-tabs__tab--active {
|
|
270
|
+
background: var(--dm-primary, #0072CE);
|
|
271
|
+
color: white;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.dm-tabs--pills .dm-tabs__indicator {
|
|
275
|
+
display: none;
|
|
276
|
+
}
|
|
277
|
+
|
|
155
278
|
.dm-tabs__panels {
|
|
156
|
-
padding: var(--dm-
|
|
279
|
+
padding: var(--dm-spacing-4, 1rem);
|
|
280
|
+
flex: 1;
|
|
157
281
|
}
|
|
158
282
|
|
|
159
283
|
.dm-tabs__panel {
|
|
@@ -164,17 +288,14 @@ const handleKeydown = (event: KeyboardEvent, index: number) => {
|
|
|
164
288
|
display: block;
|
|
165
289
|
}
|
|
166
290
|
|
|
167
|
-
@media (
|
|
168
|
-
.dm-tabs__header {
|
|
169
|
-
border-bottom-color: var(--dm-gray-700);
|
|
170
|
-
}
|
|
171
|
-
|
|
291
|
+
@media (max-width: 768px) {
|
|
172
292
|
.dm-tabs__tab {
|
|
173
|
-
|
|
293
|
+
padding: var(--dm-spacing-2, 0.5rem) var(--dm-spacing-3, 0.75rem);
|
|
294
|
+
font-size: 0.875rem;
|
|
174
295
|
}
|
|
175
|
-
|
|
176
|
-
.dm-
|
|
177
|
-
|
|
296
|
+
|
|
297
|
+
.dm-tabs__icon {
|
|
298
|
+
font-size: 1rem;
|
|
178
299
|
}
|
|
179
300
|
}
|
|
180
301
|
</style>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import DatametriaFloatingBar from '../DatametriaFloatingBar.vue'
|
|
4
|
+
|
|
5
|
+
describe('DatametriaFloatingBar', () => {
|
|
6
|
+
it('renders correctly', () => {
|
|
7
|
+
const wrapper = mount(DatametriaFloatingBar)
|
|
8
|
+
expect(wrapper.find('.dm-floating-bar').exists()).toBe(true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('applies position classes', () => {
|
|
12
|
+
const positions = ['top', 'bottom', 'left', 'right'] as const
|
|
13
|
+
positions.forEach(position => {
|
|
14
|
+
const wrapper = mount(DatametriaFloatingBar, { props: { position } })
|
|
15
|
+
expect(wrapper.find(`.dm-floating-bar--${position}`).exists()).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('applies variant classes', () => {
|
|
20
|
+
const variants = ['light', 'dark', 'primary'] as const
|
|
21
|
+
variants.forEach(variant => {
|
|
22
|
+
const wrapper = mount(DatametriaFloatingBar, { props: { variant } })
|
|
23
|
+
expect(wrapper.find(`.dm-floating-bar--${variant}`).exists()).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('applies shadow class when shadow is true', () => {
|
|
28
|
+
const wrapper = mount(DatametriaFloatingBar, { props: { shadow: true } })
|
|
29
|
+
expect(wrapper.find('.dm-floating-bar--shadow').exists()).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('does not apply shadow class when shadow is false', () => {
|
|
33
|
+
const wrapper = mount(DatametriaFloatingBar, { props: { shadow: false } })
|
|
34
|
+
expect(wrapper.find('.dm-floating-bar--shadow').exists()).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('applies rounded class when rounded is true', () => {
|
|
38
|
+
const wrapper = mount(DatametriaFloatingBar, { props: { rounded: true } })
|
|
39
|
+
expect(wrapper.find('.dm-floating-bar--rounded').exists()).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('does not apply rounded class when rounded is false', () => {
|
|
43
|
+
const wrapper = mount(DatametriaFloatingBar, { props: { rounded: false } })
|
|
44
|
+
expect(wrapper.find('.dm-floating-bar--rounded').exists()).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('renders slot content', () => {
|
|
48
|
+
const wrapper = mount(DatametriaFloatingBar, {
|
|
49
|
+
slots: { default: '<button class="test-button">Action</button>' }
|
|
50
|
+
})
|
|
51
|
+
expect(wrapper.find('.test-button').exists()).toBe(true)
|
|
52
|
+
expect(wrapper.text()).toContain('Action')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('applies correct position style for top', () => {
|
|
56
|
+
const wrapper = mount(DatametriaFloatingBar, {
|
|
57
|
+
props: { position: 'top', offset: '20px' }
|
|
58
|
+
})
|
|
59
|
+
const style = wrapper.find('.dm-floating-bar').attributes('style')
|
|
60
|
+
expect(style).toContain('top: 20px')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('applies correct position style for bottom', () => {
|
|
64
|
+
const wrapper = mount(DatametriaFloatingBar, {
|
|
65
|
+
props: { position: 'bottom', offset: '24px' }
|
|
66
|
+
})
|
|
67
|
+
const style = wrapper.find('.dm-floating-bar').attributes('style')
|
|
68
|
+
expect(style).toContain('bottom: 24px')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('applies correct position style for left', () => {
|
|
72
|
+
const wrapper = mount(DatametriaFloatingBar, {
|
|
73
|
+
props: { position: 'left', offset: '32px' }
|
|
74
|
+
})
|
|
75
|
+
const style = wrapper.find('.dm-floating-bar').attributes('style')
|
|
76
|
+
expect(style).toContain('left: 32px')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('applies correct position style for right', () => {
|
|
80
|
+
const wrapper = mount(DatametriaFloatingBar, {
|
|
81
|
+
props: { position: 'right', offset: '40px' }
|
|
82
|
+
})
|
|
83
|
+
const style = wrapper.find('.dm-floating-bar').attributes('style')
|
|
84
|
+
expect(style).toContain('right: 40px')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('uses default offset when not provided', () => {
|
|
88
|
+
const wrapper = mount(DatametriaFloatingBar, { props: { position: 'bottom' } })
|
|
89
|
+
const style = wrapper.find('.dm-floating-bar').attributes('style')
|
|
90
|
+
expect(style).toContain('bottom: 16px')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('has correct aria attributes', () => {
|
|
94
|
+
const wrapper = mount(DatametriaFloatingBar, {
|
|
95
|
+
props: { ariaLabel: 'Floating toolbar' }
|
|
96
|
+
})
|
|
97
|
+
const bar = wrapper.find('.dm-floating-bar')
|
|
98
|
+
expect(bar.attributes('role')).toBe('toolbar')
|
|
99
|
+
expect(bar.attributes('aria-label')).toBe('Floating toolbar')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('renders content wrapper', () => {
|
|
103
|
+
const wrapper = mount(DatametriaFloatingBar)
|
|
104
|
+
expect(wrapper.find('.dm-floating-bar__content').exists()).toBe(true)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('applies all classes correctly', () => {
|
|
108
|
+
const wrapper = mount(DatametriaFloatingBar, {
|
|
109
|
+
props: {
|
|
110
|
+
position: 'top',
|
|
111
|
+
variant: 'primary',
|
|
112
|
+
shadow: true,
|
|
113
|
+
rounded: true
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
const bar = wrapper.find('.dm-floating-bar')
|
|
117
|
+
expect(bar.classes()).toContain('dm-floating-bar--top')
|
|
118
|
+
expect(bar.classes()).toContain('dm-floating-bar--primary')
|
|
119
|
+
expect(bar.classes()).toContain('dm-floating-bar--shadow')
|
|
120
|
+
expect(bar.classes()).toContain('dm-floating-bar--rounded')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('renders multiple action buttons', () => {
|
|
124
|
+
const wrapper = mount(DatametriaFloatingBar, {
|
|
125
|
+
slots: {
|
|
126
|
+
default: `
|
|
127
|
+
<button class="btn-1">Action 1</button>
|
|
128
|
+
<button class="btn-2">Action 2</button>
|
|
129
|
+
<button class="btn-3">Action 3</button>
|
|
130
|
+
`
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
expect(wrapper.find('.btn-1').exists()).toBe(true)
|
|
134
|
+
expect(wrapper.find('.btn-2').exists()).toBe(true)
|
|
135
|
+
expect(wrapper.find('.btn-3').exists()).toBe(true)
|
|
136
|
+
})
|
|
137
|
+
})
|