@datametria/vue-components 2.2.0 → 2.3.1

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 (78) hide show
  1. package/README.md +25 -7
  2. package/dist/index.es.js +3378 -2148
  3. package/dist/index.umd.js +9 -9
  4. package/dist/src/components/DatametriaAutocomplete.vue.d.ts +14 -17
  5. package/dist/src/components/DatametriaBreadcrumb.vue.d.ts +39 -7
  6. package/dist/src/components/DatametriaCheckbox.vue.d.ts +35 -6
  7. package/dist/src/components/DatametriaCheckboxGroup.vue.d.ts +30 -0
  8. package/dist/src/components/DatametriaDataTable.vue.d.ts +64 -0
  9. package/dist/src/components/DatametriaDatePicker.vue.d.ts +15 -37
  10. package/dist/src/components/DatametriaDialog.vue.d.ts +71 -0
  11. package/dist/src/components/DatametriaEmpty.vue.d.ts +30 -0
  12. package/dist/src/components/DatametriaFloatingBar.vue.d.ts +2 -2
  13. package/dist/src/components/DatametriaForm.vue.d.ts +40 -0
  14. package/dist/src/components/DatametriaFormItem.vue.d.ts +28 -0
  15. package/dist/src/components/DatametriaGrid.vue.d.ts +1 -1
  16. package/dist/src/components/DatametriaInput.vue.d.ts +69 -10
  17. package/dist/src/components/DatametriaMenu.vue.d.ts +3 -3
  18. package/dist/src/components/DatametriaNavbar.vue.d.ts +2 -2
  19. package/dist/src/components/DatametriaPagination.vue.d.ts +29 -0
  20. package/dist/src/components/DatametriaPopconfirm.vue.d.ts +43 -0
  21. package/dist/src/components/DatametriaProgress.vue.d.ts +33 -8
  22. package/dist/src/components/DatametriaRadio.vue.d.ts +25 -6
  23. package/dist/src/components/DatametriaRadioGroup.vue.d.ts +29 -0
  24. package/dist/src/components/DatametriaResult.vue.d.ts +30 -0
  25. package/dist/src/components/DatametriaSelect.vue.d.ts +16 -11
  26. package/dist/src/components/DatametriaSidebar.vue.d.ts +3 -3
  27. package/dist/src/components/DatametriaSlider.vue.d.ts +3 -3
  28. package/dist/src/components/DatametriaSortableTable.vue.d.ts +1 -1
  29. package/dist/src/components/DatametriaSteps.vue.d.ts +45 -0
  30. package/dist/src/components/DatametriaSwitch.vue.d.ts +9 -4
  31. package/dist/src/components/DatametriaTabPane.vue.d.ts +28 -0
  32. package/dist/src/components/DatametriaTextarea.vue.d.ts +27 -8
  33. package/dist/src/components/DatametriaTimePicker.vue.d.ts +17 -25
  34. package/dist/src/components/DatametriaToast.vue.d.ts +1 -1
  35. package/dist/src/components/DatametriaTooltip.vue.d.ts +1 -1
  36. package/dist/src/components/DatametriaTree.vue.d.ts +31 -0
  37. package/dist/src/components/DatametriaTreeNode.vue.d.ts +17 -0
  38. package/dist/src/components/DatametriaUpload.vue.d.ts +64 -0
  39. package/dist/src/index.d.ts +14 -0
  40. package/dist/vue-components.css +1 -1
  41. package/package.json +8 -10
  42. package/src/components/DatametriaAutocomplete.vue +155 -260
  43. package/src/components/DatametriaBreadcrumb.vue +66 -80
  44. package/src/components/DatametriaCheckbox.vue +150 -37
  45. package/src/components/DatametriaCheckboxGroup.vue +43 -0
  46. package/src/components/DatametriaDataTable.vue +304 -0
  47. package/src/components/DatametriaDatePicker.vue +238 -614
  48. package/src/components/DatametriaDialog.vue +295 -0
  49. package/src/components/DatametriaDropdown.vue +352 -0
  50. package/src/components/DatametriaEmpty.vue +153 -0
  51. package/src/components/DatametriaForm.vue +160 -0
  52. package/src/components/DatametriaFormItem.vue +181 -0
  53. package/src/components/DatametriaInput.vue +226 -63
  54. package/src/components/DatametriaPagination.vue +373 -0
  55. package/src/components/DatametriaPopconfirm.vue +236 -0
  56. package/src/components/DatametriaProgress.vue +176 -63
  57. package/src/components/DatametriaRadio.vue +83 -72
  58. package/src/components/DatametriaRadioGroup.vue +42 -0
  59. package/src/components/DatametriaResult.vue +133 -0
  60. package/src/components/DatametriaSelect.vue +172 -67
  61. package/src/components/DatametriaSortableTable.vue +35 -4
  62. package/src/components/DatametriaSteps.vue +314 -0
  63. package/src/components/DatametriaSwitch.vue +86 -80
  64. package/src/components/DatametriaTabPane.vue +82 -0
  65. package/src/components/DatametriaTextarea.vue +140 -100
  66. package/src/components/DatametriaTimePicker.vue +231 -214
  67. package/src/components/DatametriaTree.vue +124 -0
  68. package/src/components/DatametriaTreeNode.vue +174 -0
  69. package/src/components/DatametriaUpload.vue +365 -0
  70. package/src/index.ts +25 -11
  71. package/src/components/__tests__/DatametriaAutocomplete.test.ts +0 -180
  72. package/src/components/__tests__/DatametriaBreadcrumb.test.ts +0 -75
  73. package/src/components/__tests__/DatametriaCheckbox.test.ts +0 -47
  74. package/src/components/__tests__/DatametriaDatePicker.test.ts +0 -234
  75. package/src/components/__tests__/DatametriaProgress.test.ts +0 -90
  76. package/src/components/__tests__/DatametriaRadio.test.ts +0 -77
  77. package/src/components/__tests__/DatametriaSwitch.test.ts +0 -64
  78. package/src/components/__tests__/DatametriaTextarea.test.ts +0 -66
@@ -377,12 +377,43 @@ const filteredData = computed(() => {
377
377
  // Sorting
378
378
  if (sortKey.value) {
379
379
  result.sort((a, b) => {
380
- const aVal = a[sortKey.value]
381
- const bVal = b[sortKey.value]
380
+ let aVal = a[sortKey.value]
381
+ let bVal = b[sortKey.value]
382
+
383
+ // Handle null/undefined
384
+ if (aVal == null && bVal == null) return 0
385
+ if (aVal == null) return 1
386
+ if (bVal == null) return -1
387
+
388
+ // Convert dates to timestamps for proper comparison
389
+ if (aVal instanceof Date) aVal = aVal.getTime()
390
+ if (bVal instanceof Date) bVal = bVal.getTime()
391
+
392
+ // Try to parse as date if string looks like a date
393
+ if (typeof aVal === 'string' && !isNaN(Date.parse(aVal))) {
394
+ const datePattern = /^\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/
395
+ if (datePattern.test(aVal)) {
396
+ aVal = new Date(aVal).getTime()
397
+ }
398
+ }
399
+ if (typeof bVal === 'string' && !isNaN(Date.parse(bVal))) {
400
+ const datePattern = /^\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/
401
+ if (datePattern.test(bVal)) {
402
+ bVal = new Date(bVal).getTime()
403
+ }
404
+ }
382
405
 
383
- if (aVal === bVal) return 0
406
+ // Numeric comparison
407
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
408
+ return sortOrder.value === 'asc' ? aVal - bVal : bVal - aVal
409
+ }
384
410
 
385
- const comparison = aVal > bVal ? 1 : -1
411
+ // String comparison (case-insensitive)
412
+ const aStr = String(aVal).toLowerCase()
413
+ const bStr = String(bVal).toLowerCase()
414
+
415
+ if (aStr === bStr) return 0
416
+ const comparison = aStr > bStr ? 1 : -1
386
417
  return sortOrder.value === 'asc' ? comparison : -comparison
387
418
  })
388
419
  }
@@ -0,0 +1,314 @@
1
+ <template>
2
+ <div
3
+ class="datametria-steps"
4
+ :class="[
5
+ `datametria-steps--${direction}`,
6
+ { 'datametria-steps--simple': simple }
7
+ ]"
8
+ >
9
+ <div
10
+ v-for="(item, index) in items"
11
+ :key="index"
12
+ class="datametria-steps__item"
13
+ :class="[
14
+ `is-${getStepStatus(index)}`,
15
+ { 'is-last': index === items.length - 1 }
16
+ ]"
17
+ >
18
+ <div class="datametria-steps__head">
19
+ <div class="datametria-steps__line" v-if="index > 0">
20
+ <div class="datametria-steps__line-inner"></div>
21
+ </div>
22
+ <div class="datametria-steps__icon">
23
+ <span v-if="getStepStatus(index) === 'finish'" class="icon-check">✓</span>
24
+ <span v-else-if="getStepStatus(index) === 'error'" class="icon-error">✕</span>
25
+ <span v-else class="icon-number">{{ index + 1 }}</span>
26
+ </div>
27
+ </div>
28
+ <div class="datametria-steps__main">
29
+ <div class="datametria-steps__title">{{ item.title }}</div>
30
+ <div v-if="item.description" class="datametria-steps__description">
31
+ {{ item.description }}
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import { computed } from 'vue'
40
+
41
+ /**
42
+ * DatametriaSteps - Componente de passos/wizard
43
+ *
44
+ * @component
45
+ * @example
46
+ * <DatametriaSteps
47
+ * :active="1"
48
+ * :items="[
49
+ * { title: 'Passo 1', description: 'Descrição' },
50
+ * { title: 'Passo 2' },
51
+ * { title: 'Passo 3' }
52
+ * ]"
53
+ * direction="horizontal"
54
+ * />
55
+ */
56
+
57
+ export interface StepItem {
58
+ /** Título do passo */
59
+ title: string
60
+ /** Descrição do passo (opcional) */
61
+ description?: string
62
+ /** Status customizado do passo */
63
+ status?: 'wait' | 'process' | 'finish' | 'error'
64
+ }
65
+
66
+ interface Props {
67
+ /** Índice do passo ativo (0-based) */
68
+ active?: number
69
+ /** Array de itens dos passos */
70
+ items: StepItem[]
71
+ /** Direção dos passos */
72
+ direction?: 'horizontal' | 'vertical'
73
+ /** Modo simples (sem descrição) */
74
+ simple?: boolean
75
+ /** Status global (sobrescreve status individual) */
76
+ status?: 'wait' | 'process' | 'finish' | 'error'
77
+ }
78
+
79
+ const props = withDefaults(defineProps<Props>(), {
80
+ active: 0,
81
+ direction: 'horizontal',
82
+ simple: false
83
+ })
84
+
85
+ const emit = defineEmits<{
86
+ change: [index: number]
87
+ }>()
88
+
89
+ /**
90
+ * Determina o status de um passo baseado no índice ativo
91
+ */
92
+ const getStepStatus = (index: number): 'wait' | 'process' | 'finish' | 'error' => {
93
+ // Status customizado do item tem prioridade
94
+ if (props.items[index]?.status) {
95
+ return props.items[index].status!
96
+ }
97
+
98
+ // Status global tem segunda prioridade
99
+ if (props.status) {
100
+ return props.status
101
+ }
102
+
103
+ // Status baseado no índice ativo
104
+ if (index < props.active) {
105
+ return 'finish'
106
+ } else if (index === props.active) {
107
+ return 'process'
108
+ } else {
109
+ return 'wait'
110
+ }
111
+ }
112
+ </script>
113
+
114
+ <style scoped>
115
+ .datametria-steps {
116
+ display: flex;
117
+ font-size: 14px;
118
+ }
119
+
120
+ .datametria-steps--horizontal {
121
+ flex-direction: row;
122
+ }
123
+
124
+ .datametria-steps--vertical {
125
+ flex-direction: column;
126
+ }
127
+
128
+ .datametria-steps__item {
129
+ position: relative;
130
+ flex: 1;
131
+ display: flex;
132
+ overflow: hidden;
133
+ }
134
+
135
+ .datametria-steps--horizontal .datametria-steps__item {
136
+ flex-direction: column;
137
+ }
138
+
139
+ .datametria-steps--vertical .datametria-steps__item {
140
+ flex-direction: row;
141
+ padding-bottom: 24px;
142
+ }
143
+
144
+ .datametria-steps--vertical .datametria-steps__item.is-last {
145
+ padding-bottom: 0;
146
+ }
147
+
148
+ .datametria-steps__head {
149
+ position: relative;
150
+ display: flex;
151
+ align-items: center;
152
+ }
153
+
154
+ .datametria-steps--horizontal .datametria-steps__head {
155
+ flex-direction: column;
156
+ margin-bottom: 8px;
157
+ }
158
+
159
+ .datametria-steps--vertical .datametria-steps__head {
160
+ flex-direction: column;
161
+ margin-right: 16px;
162
+ flex-shrink: 0;
163
+ }
164
+
165
+ .datametria-steps__icon {
166
+ width: 32px;
167
+ height: 32px;
168
+ border-radius: 50%;
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ font-size: 16px;
173
+ font-weight: 500;
174
+ border: 2px solid var(--datametria-border, #dcdfe6);
175
+ background: var(--datametria-bg, #fff);
176
+ color: var(--datametria-text-secondary, #909399);
177
+ transition: all 0.3s;
178
+ z-index: 1;
179
+ }
180
+
181
+ .datametria-steps__item.is-process .datametria-steps__icon {
182
+ border-color: var(--datametria-primary, #0072ce);
183
+ background: var(--datametria-primary, #0072ce);
184
+ color: #fff;
185
+ }
186
+
187
+ .datametria-steps__item.is-finish .datametria-steps__icon {
188
+ border-color: var(--datametria-success, #67c23a);
189
+ background: var(--datametria-success, #67c23a);
190
+ color: #fff;
191
+ }
192
+
193
+ .datametria-steps__item.is-error .datametria-steps__icon {
194
+ border-color: var(--datametria-danger, #f56c6c);
195
+ background: var(--datametria-danger, #f56c6c);
196
+ color: #fff;
197
+ }
198
+
199
+ .datametria-steps__line {
200
+ position: absolute;
201
+ background: var(--datametria-border, #dcdfe6);
202
+ }
203
+
204
+ .datametria-steps--horizontal .datametria-steps__line {
205
+ height: 2px;
206
+ top: 15px;
207
+ left: -50%;
208
+ right: 50%;
209
+ }
210
+
211
+ .datametria-steps--vertical .datametria-steps__line {
212
+ width: 2px;
213
+ top: 32px;
214
+ bottom: 0;
215
+ left: 15px;
216
+ }
217
+
218
+ .datametria-steps__line-inner {
219
+ width: 0;
220
+ height: 100%;
221
+ background: var(--datametria-primary, #0072ce);
222
+ transition: width 0.3s, height 0.3s;
223
+ }
224
+
225
+ .datametria-steps__item.is-finish .datametria-steps__line-inner,
226
+ .datametria-steps__item.is-process .datametria-steps__line-inner {
227
+ width: 100%;
228
+ height: 100%;
229
+ background: var(--datametria-success, #67c23a);
230
+ }
231
+
232
+ .datametria-steps__item.is-error .datametria-steps__line-inner {
233
+ width: 100%;
234
+ height: 100%;
235
+ background: var(--datametria-danger, #f56c6c);
236
+ }
237
+
238
+ .datametria-steps__main {
239
+ text-align: center;
240
+ }
241
+
242
+ .datametria-steps--vertical .datametria-steps__main {
243
+ text-align: left;
244
+ flex: 1;
245
+ }
246
+
247
+ .datametria-steps__title {
248
+ font-size: 14px;
249
+ font-weight: 500;
250
+ color: var(--datametria-text-primary, #303133);
251
+ line-height: 1.5;
252
+ }
253
+
254
+ .datametria-steps__item.is-wait .datametria-steps__title {
255
+ color: var(--datametria-text-secondary, #909399);
256
+ }
257
+
258
+ .datametria-steps__item.is-process .datametria-steps__title {
259
+ color: var(--datametria-primary, #0072ce);
260
+ }
261
+
262
+ .datametria-steps__item.is-finish .datametria-steps__title {
263
+ color: var(--datametria-success, #67c23a);
264
+ }
265
+
266
+ .datametria-steps__item.is-error .datametria-steps__title {
267
+ color: var(--datametria-danger, #f56c6c);
268
+ }
269
+
270
+ .datametria-steps__description {
271
+ font-size: 12px;
272
+ color: var(--datametria-text-secondary, #909399);
273
+ margin-top: 4px;
274
+ line-height: 1.5;
275
+ }
276
+
277
+ .datametria-steps--simple .datametria-steps__description {
278
+ display: none;
279
+ }
280
+
281
+ /* Dark mode */
282
+ @media (prefers-color-scheme: dark) {
283
+ .datametria-steps__icon {
284
+ border-color: var(--datametria-border-dark, #4c4d4f);
285
+ background: var(--datametria-bg-dark, #1d1e1f);
286
+ color: var(--datametria-text-secondary-dark, #909399);
287
+ }
288
+
289
+ .datametria-steps__item.is-process .datametria-steps__icon {
290
+ border-color: var(--datametria-primary-dark, #409eff);
291
+ background: var(--datametria-primary-dark, #409eff);
292
+ }
293
+
294
+ .datametria-steps__line {
295
+ background: var(--datametria-border-dark, #4c4d4f);
296
+ }
297
+
298
+ .datametria-steps__title {
299
+ color: var(--datametria-text-primary-dark, #e5eaf3);
300
+ }
301
+
302
+ .datametria-steps__item.is-wait .datametria-steps__title {
303
+ color: var(--datametria-text-secondary-dark, #909399);
304
+ }
305
+
306
+ .datametria-steps__item.is-process .datametria-steps__title {
307
+ color: var(--datametria-primary-dark, #409eff);
308
+ }
309
+
310
+ .datametria-steps__description {
311
+ color: var(--datametria-text-secondary-dark, #909399);
312
+ }
313
+ }
314
+ </style>
@@ -1,137 +1,143 @@
1
1
  <template>
2
- <div class="dm-switch">
3
- <label class="dm-switch__label">
4
- <input
5
- type="checkbox"
6
- class="dm-switch__input"
7
- :checked="modelValue"
8
- :disabled="disabled"
9
- :aria-label="ariaLabel"
10
- :aria-checked="modelValue"
11
- role="switch"
12
- @change="handleChange"
13
- />
14
- <span class="dm-switch__track">
15
- <span class="dm-switch__thumb"></span>
2
+ <button
3
+ type="button"
4
+ role="switch"
5
+ class="datametria-switch"
6
+ :class="{
7
+ 'is-checked': modelValue,
8
+ 'is-disabled': disabled,
9
+ 'is-loading': loading
10
+ }"
11
+ :aria-checked="modelValue"
12
+ :disabled="disabled || loading"
13
+ @click="handleClick"
14
+ >
15
+ <span class="datametria-switch__core" :style="coreStyle">
16
+ <span v-if="loading" class="datametria-switch__loading">
17
+ <span class="spinner"></span>
16
18
  </span>
17
- <span v-if="label" class="dm-switch__text">{{ label }}</span>
18
- </label>
19
- <p v-if="error" class="dm-switch__error">{{ error }}</p>
20
- </div>
19
+ </span>
20
+ </button>
21
21
  </template>
22
22
 
23
23
  <script setup lang="ts">
24
+ import { computed } from 'vue'
25
+
24
26
  interface Props {
25
27
  modelValue?: boolean
26
- label?: string
27
28
  disabled?: boolean
28
- error?: string
29
- ariaLabel?: string
29
+ loading?: boolean
30
+ activeColor?: string
31
+ inactiveColor?: string
30
32
  }
31
33
 
32
34
  const props = withDefaults(defineProps<Props>(), {
33
35
  modelValue: false,
34
- disabled: false
36
+ disabled: false,
37
+ loading: false,
38
+ activeColor: '#0072ce',
39
+ inactiveColor: '#dcdfe6'
35
40
  })
36
41
 
37
42
  const emit = defineEmits<{
38
43
  'update:modelValue': [value: boolean]
44
+ change: [value: boolean]
39
45
  }>()
40
46
 
41
- const handleChange = (event: Event) => {
42
- if (!props.disabled) {
43
- emit('update:modelValue', (event.target as HTMLInputElement).checked)
44
- }
47
+ const coreStyle = computed(() => ({
48
+ backgroundColor: props.modelValue ? props.activeColor : props.inactiveColor
49
+ }))
50
+
51
+ const handleClick = () => {
52
+ if (props.disabled || props.loading) return
53
+ const newValue = !props.modelValue
54
+ emit('update:modelValue', newValue)
55
+ emit('change', newValue)
45
56
  }
46
57
  </script>
47
58
 
48
59
  <style scoped>
49
- .dm-switch {
50
- display: flex;
51
- flex-direction: column;
52
- gap: var(--dm-spacing-1, 0.25rem);
53
- }
54
-
55
- .dm-switch__label {
60
+ .datametria-switch {
56
61
  display: inline-flex;
57
62
  align-items: center;
58
- gap: var(--dm-spacing-3, 0.75rem);
63
+ position: relative;
64
+ font-size: 14px;
65
+ line-height: 20px;
66
+ height: 20px;
67
+ vertical-align: middle;
68
+ border: none;
69
+ background: transparent;
59
70
  cursor: pointer;
60
- min-height: 44px;
61
- padding: var(--dm-spacing-2, 0.5rem);
62
- user-select: none;
71
+ padding: 0;
63
72
  }
64
73
 
65
- .dm-switch__label:has(.dm-switch__input:disabled) {
74
+ .datametria-switch.is-disabled {
66
75
  cursor: not-allowed;
67
76
  opacity: 0.5;
68
77
  }
69
78
 
70
- .dm-switch__input {
71
- position: absolute;
72
- opacity: 0;
73
- width: 0;
74
- height: 0;
79
+ .datametria-switch.is-loading {
80
+ cursor: not-allowed;
75
81
  }
76
82
 
77
- .dm-switch__track {
83
+ .datametria-switch__core {
84
+ display: inline-block;
78
85
  position: relative;
79
- width: 44px;
80
- height: 24px;
81
- background: var(--dm-neutral-300, #d1d5db);
82
- border-radius: 12px;
83
- transition: all 0.2s;
84
- flex-shrink: 0;
86
+ width: 40px;
87
+ height: 20px;
88
+ border-radius: 10px;
89
+ transition: background-color 0.3s;
85
90
  }
86
91
 
87
- .dm-switch__thumb {
92
+ .datametria-switch__core::after {
93
+ content: '';
88
94
  position: absolute;
89
95
  top: 2px;
90
96
  left: 2px;
91
- width: 20px;
92
- height: 20px;
93
- background: white;
97
+ width: 16px;
98
+ height: 16px;
94
99
  border-radius: 50%;
95
- transition: all 0.2s;
96
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
97
- }
98
-
99
- .dm-switch__input:checked + .dm-switch__track {
100
- background: var(--dm-primary, #0072CE);
100
+ background-color: #fff;
101
+ transition: transform 0.3s;
101
102
  }
102
103
 
103
- .dm-switch__input:checked + .dm-switch__track .dm-switch__thumb {
104
+ .datametria-switch.is-checked .datametria-switch__core::after {
104
105
  transform: translateX(20px);
105
106
  }
106
107
 
107
- .dm-switch__input:focus-visible + .dm-switch__track {
108
- outline: 2px solid var(--dm-primary, #0072CE);
109
- outline-offset: 2px;
108
+ .datametria-switch__loading {
109
+ position: absolute;
110
+ top: 50%;
111
+ left: 50%;
112
+ transform: translate(-50%, -50%);
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: center;
110
116
  }
111
117
 
112
- .dm-switch__input:disabled + .dm-switch__track {
113
- background: var(--dm-neutral-200, #e5e7eb);
118
+ .spinner {
119
+ width: 12px;
120
+ height: 12px;
121
+ border: 2px solid #fff;
122
+ border-top-color: transparent;
123
+ border-radius: 50%;
124
+ animation: spin 0.6s linear infinite;
114
125
  }
115
126
 
116
- .dm-switch__text {
117
- color: var(--dm-neutral-900, #111827);
118
- font-size: var(--dm-font-size-base, 1rem);
119
- line-height: 1.5;
127
+ @keyframes spin {
128
+ to { transform: rotate(360deg); }
120
129
  }
121
130
 
122
- .dm-switch__error {
123
- color: var(--dm-error, #ef4444);
124
- font-size: var(--dm-font-size-sm, 0.875rem);
125
- margin: 0;
131
+ .datametria-switch:focus-visible {
132
+ outline: 2px solid var(--dm-color-primary, #0072ce);
133
+ outline-offset: 2px;
134
+ border-radius: 10px;
126
135
  }
127
136
 
137
+ /* Dark mode */
128
138
  @media (prefers-color-scheme: dark) {
129
- .dm-switch__track {
130
- background: var(--dm-neutral-700, #374151);
131
- }
132
-
133
- .dm-switch__input:disabled + .dm-switch__track {
134
- background: var(--dm-neutral-800, #1f2937);
139
+ .datametria-switch__core {
140
+ background-color: var(--dm-bg-color-dark, #1e1e1e);
135
141
  }
136
142
  }
137
143
  </style>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <div
3
+ :id="id"
4
+ class="datametria-tab-pane"
5
+ :class="{ 'is-active': active }"
6
+ role="tabpanel"
7
+ :aria-labelledby="ariaLabelledby"
8
+ :hidden="!active"
9
+ >
10
+ <slot v-if="lazy ? (active || wasActive) : true"></slot>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { ref, watch, inject, onMounted, onBeforeUnmount, computed } from 'vue'
16
+
17
+ interface Props {
18
+ label: string
19
+ name?: string
20
+ disabled?: boolean
21
+ lazy?: boolean
22
+ icon?: string
23
+ badge?: string | number
24
+ }
25
+
26
+ const props = withDefaults(defineProps<Props>(), {
27
+ disabled: false,
28
+ lazy: false
29
+ })
30
+
31
+ const tabsContext = inject<any>('datametriaTabs', null)
32
+ const wasActive = ref(false)
33
+
34
+ const active = computed(() => {
35
+ if (!tabsContext) return false
36
+ return tabsContext.activePane.value === (props.name || props.label)
37
+ })
38
+
39
+ const id = computed(() => {
40
+ const name = props.name || props.label
41
+ return `pane-${name.replace(/\s+/g, '-').toLowerCase()}`
42
+ })
43
+
44
+ const ariaLabelledby = computed(() => {
45
+ const name = props.name || props.label
46
+ return `tab-${name.replace(/\s+/g, '-').toLowerCase()}`
47
+ })
48
+
49
+ watch(active, (isActive) => {
50
+ if (isActive) {
51
+ wasActive.value = true
52
+ }
53
+ })
54
+
55
+ onMounted(() => {
56
+ if (tabsContext) {
57
+ tabsContext.registerPane({
58
+ label: props.label,
59
+ name: props.name || props.label,
60
+ disabled: props.disabled,
61
+ icon: props.icon,
62
+ badge: props.badge
63
+ })
64
+ }
65
+ })
66
+
67
+ onBeforeUnmount(() => {
68
+ if (tabsContext) {
69
+ tabsContext.unregisterPane(props.name || props.label)
70
+ }
71
+ })
72
+ </script>
73
+
74
+ <style scoped>
75
+ .datametria-tab-pane {
76
+ display: none;
77
+ }
78
+
79
+ .datametria-tab-pane.is-active {
80
+ display: block;
81
+ }
82
+ </style>