@datametria/vue-components 2.2.0 → 2.3.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.
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 +4 -3
  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
@@ -1,286 +1,303 @@
1
1
  <template>
2
- <div class="dm-time-picker" :class="{ 'dm-time-picker--disabled': disabled }">
3
- <label v-if="label" :for="inputId" class="dm-time-picker__label">
4
- {{ label }}
5
- <span v-if="required" class="dm-time-picker__required" aria-label="obrigatório">*</span>
6
- </label>
7
-
8
- <div class="dm-time-picker__wrapper">
9
- <input
10
- :id="inputId"
11
- ref="inputRef"
12
- v-model="displayValue"
13
- type="time"
14
- class="dm-time-picker__input"
15
- :class="{
16
- 'dm-time-picker__input--error': hasError,
17
- 'dm-time-picker__input--success': hasSuccess
18
- }"
19
- :disabled="disabled"
20
- :required="required"
21
- :min="min"
22
- :max="max"
23
- :step="step"
24
- :aria-describedby="ariaDescribedBy"
25
- :aria-invalid="hasError"
26
- @input="handleInput"
27
- @blur="handleBlur"
28
- @focus="handleFocus"
29
- />
30
-
31
- <div v-if="hasError || hasSuccess" class="dm-time-picker__icon">
32
- <svg v-if="hasError" class="dm-time-picker__icon--error" viewBox="0 0 20 20" fill="currentColor">
33
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
34
- </svg>
35
- <svg v-else-if="hasSuccess" class="dm-time-picker__icon--success" viewBox="0 0 20 20" fill="currentColor">
36
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
37
- </svg>
2
+ <div class="datametria-timepicker" :class="{ 'datametria-timepicker--disabled': disabled }">
3
+ <input
4
+ ref="inputRef"
5
+ v-model="displayValue"
6
+ type="text"
7
+ class="datametria-timepicker__input"
8
+ :placeholder="placeholder"
9
+ :disabled="disabled"
10
+ :readonly="readonly"
11
+ @focus="handleFocus"
12
+ @blur="handleBlur"
13
+ />
14
+ <Teleport to="body">
15
+ <div
16
+ v-if="isOpen"
17
+ ref="dropdownRef"
18
+ class="datametria-timepicker__dropdown"
19
+ :style="dropdownStyle"
20
+ >
21
+ <div class="datametria-timepicker__spinners">
22
+ <div class="datametria-timepicker__spinner">
23
+ <div class="datametria-timepicker__spinner-title">Hora</div>
24
+ <div class="datametria-timepicker__spinner-list">
25
+ <button
26
+ v-for="hour in hours"
27
+ :key="hour"
28
+ class="datametria-timepicker__spinner-item"
29
+ :class="{ 'datametria-timepicker__spinner-item--active': hour === selectedHour }"
30
+ @click="selectHour(hour)"
31
+ >
32
+ {{ formatNumber(hour) }}
33
+ </button>
34
+ </div>
35
+ </div>
36
+ <div class="datametria-timepicker__spinner">
37
+ <div class="datametria-timepicker__spinner-title">Minuto</div>
38
+ <div class="datametria-timepicker__spinner-list">
39
+ <button
40
+ v-for="minute in minutes"
41
+ :key="minute"
42
+ class="datametria-timepicker__spinner-item"
43
+ :class="{ 'datametria-timepicker__spinner-item--active': minute === selectedMinute }"
44
+ @click="selectMinute(minute)"
45
+ >
46
+ {{ formatNumber(minute) }}
47
+ </button>
48
+ </div>
49
+ </div>
50
+ <div v-if="showSeconds" class="datametria-timepicker__spinner">
51
+ <div class="datametria-timepicker__spinner-title">Segundo</div>
52
+ <div class="datametria-timepicker__spinner-list">
53
+ <button
54
+ v-for="second in seconds"
55
+ :key="second"
56
+ class="datametria-timepicker__spinner-item"
57
+ :class="{ 'datametria-timepicker__spinner-item--active': second === selectedSecond }"
58
+ @click="selectSecond(second)"
59
+ >
60
+ {{ formatNumber(second) }}
61
+ </button>
62
+ </div>
63
+ </div>
64
+ </div>
38
65
  </div>
39
- </div>
40
-
41
- <div v-if="errorMessage || successMessage || helperText" class="dm-time-picker__messages">
42
- <p v-if="errorMessage" :id="`${inputId}-error`" class="dm-time-picker__error" role="alert">
43
- {{ errorMessage }}
44
- </p>
45
- <p v-else-if="successMessage" :id="`${inputId}-success`" class="dm-time-picker__success">
46
- {{ successMessage }}
47
- </p>
48
- <p v-else-if="helperText" :id="`${inputId}-helper`" class="dm-time-picker__helper">
49
- {{ helperText }}
50
- </p>
51
- </div>
66
+ </Teleport>
52
67
  </div>
53
68
  </template>
54
69
 
55
70
  <script setup lang="ts">
56
- import { ref, computed, watch, nextTick } from 'vue'
71
+ import { ref, computed, watch } from 'vue'
57
72
 
58
73
  interface Props {
59
- modelValue?: string
60
- label?: string
74
+ modelValue?: string | null
75
+ format?: string
76
+ step?: number
61
77
  placeholder?: string
62
78
  disabled?: boolean
63
- required?: boolean
64
- errorMessage?: string
65
- successMessage?: string
66
- helperText?: string
67
- min?: string
68
- max?: string
69
- step?: number
70
- format24h?: boolean
71
- }
72
-
73
- interface Emits {
74
- (e: 'update:modelValue', value: string): void
75
- (e: 'blur', event: FocusEvent): void
76
- (e: 'focus', event: FocusEvent): void
77
- (e: 'change', value: string): void
79
+ readonly?: boolean
80
+ range?: boolean
78
81
  }
79
82
 
80
83
  const props = withDefaults(defineProps<Props>(), {
81
- modelValue: '',
82
- step: 60,
83
- format24h: true
84
+ modelValue: null,
85
+ format: 'HH:mm:ss',
86
+ step: 1,
87
+ placeholder: 'Selecione um horário',
88
+ disabled: false,
89
+ readonly: false,
90
+ range: false
84
91
  })
85
92
 
86
- const emit = defineEmits<Emits>()
93
+ const emit = defineEmits<{
94
+ 'update:modelValue': [value: string | null]
95
+ change: [value: string | null]
96
+ }>()
87
97
 
88
- // Refs
89
98
  const inputRef = ref<HTMLInputElement>()
90
-
91
- // Computed
92
- const inputId = computed(() => `dm-time-picker-${Math.random().toString(36).substr(2, 9)}`)
93
-
94
- const hasError = computed(() => !!props.errorMessage)
95
- const hasSuccess = computed(() => !!props.successMessage && !hasError.value)
96
-
97
- const ariaDescribedBy = computed(() => {
98
- const ids = []
99
- if (props.errorMessage) ids.push(`${inputId.value}-error`)
100
- else if (props.successMessage) ids.push(`${inputId.value}-success`)
101
- else if (props.helperText) ids.push(`${inputId.value}-helper`)
102
- return ids.length > 0 ? ids.join(' ') : undefined
99
+ const dropdownRef = ref<HTMLDivElement>()
100
+ const isOpen = ref(false)
101
+ const selectedHour = ref(0)
102
+ const selectedMinute = ref(0)
103
+ const selectedSecond = ref(0)
104
+
105
+ const showSeconds = computed(() => props.format.includes('ss'))
106
+
107
+ const hours = computed(() => {
108
+ const result: number[] = []
109
+ for (let i = 0; i < 24; i += props.step) {
110
+ result.push(i)
111
+ }
112
+ return result
103
113
  })
104
114
 
105
- const displayValue = computed({
106
- get: () => props.modelValue,
107
- set: (value: string) => {
108
- emit('update:modelValue', value)
109
- emit('change', value)
115
+ const minutes = computed(() => {
116
+ const result: number[] = []
117
+ for (let i = 0; i < 60; i += props.step) {
118
+ result.push(i)
110
119
  }
120
+ return result
111
121
  })
112
122
 
113
- // Methods
114
- const handleInput = (event: Event) => {
115
- const target = event.target as HTMLInputElement
116
- displayValue.value = target.value
117
- }
118
-
119
- const handleBlur = (event: FocusEvent) => {
120
- emit('blur', event)
121
- }
122
-
123
- const handleFocus = (event: FocusEvent) => {
124
- emit('focus', event)
125
- }
126
-
127
- const focus = () => {
128
- nextTick(() => {
129
- inputRef.value?.focus()
130
- })
131
- }
132
-
133
- const blur = () => {
134
- inputRef.value?.blur()
135
- }
136
-
137
- // Watch for external changes
138
- watch(() => props.modelValue, (newValue) => {
139
- if (inputRef.value && inputRef.value.value !== newValue) {
140
- inputRef.value.value = newValue
123
+ const seconds = computed(() => {
124
+ const result: number[] = []
125
+ for (let i = 0; i < 60; i += props.step) {
126
+ result.push(i)
141
127
  }
128
+ return result
142
129
  })
143
130
 
144
- // Expose methods
145
- defineExpose({
146
- focus,
147
- blur,
148
- inputRef
131
+ const displayValue = computed(() => {
132
+ if (!props.modelValue) return ''
133
+ return props.modelValue
149
134
  })
150
- </script>
151
-
152
- <style scoped>
153
- .dm-time-picker {
154
- @apply w-full;
155
- }
156
135
 
157
- .dm-time-picker--disabled {
158
- @apply opacity-60 cursor-not-allowed;
159
- }
136
+ const dropdownStyle = computed(() => {
137
+ if (!inputRef.value) return {}
138
+ const rect = inputRef.value.getBoundingClientRect()
139
+ return {
140
+ position: 'fixed',
141
+ top: `${rect.bottom + 4}px`,
142
+ left: `${rect.left}px`,
143
+ zIndex: 9999
144
+ }
145
+ })
160
146
 
161
- .dm-time-picker__label {
162
- @apply block text-sm font-medium text-gray-700 mb-1;
163
- color: var(--dm-gray-700);
147
+ const formatNumber = (num: number): string => {
148
+ return String(num).padStart(2, '0')
164
149
  }
165
150
 
166
- [data-theme="dark"] .dm-time-picker__label {
167
- color: var(--dm-text-secondary);
151
+ const formatTime = (): string => {
152
+ const h = formatNumber(selectedHour.value)
153
+ const m = formatNumber(selectedMinute.value)
154
+ const s = formatNumber(selectedSecond.value)
155
+
156
+ if (showSeconds.value) {
157
+ return `${h}:${m}:${s}`
158
+ }
159
+ return `${h}:${m}`
168
160
  }
169
161
 
170
- .dm-time-picker__required {
171
- @apply text-red-500 ml-1;
172
- color: var(--dm-error);
162
+ const parseTime = (time: string) => {
163
+ if (!time) return
164
+
165
+ const parts = time.split(':')
166
+ if (parts.length >= 2) {
167
+ selectedHour.value = parseInt(parts[0]) || 0
168
+ selectedMinute.value = parseInt(parts[1]) || 0
169
+ if (parts.length >= 3) {
170
+ selectedSecond.value = parseInt(parts[2]) || 0
171
+ }
172
+ }
173
173
  }
174
174
 
175
- .dm-time-picker__wrapper {
176
- @apply relative;
175
+ const selectHour = (hour: number) => {
176
+ selectedHour.value = hour
177
+ updateValue()
177
178
  }
178
179
 
179
- .dm-time-picker__input {
180
- @apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm;
181
- @apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500;
182
- @apply disabled:bg-gray-50 disabled:cursor-not-allowed;
183
- @apply transition-colors duration-200;
184
-
185
- border-color: var(--dm-gray-300, #d1d5db);
186
- background-color: var(--dm-bg-primary, #ffffff);
187
- color: var(--dm-text-primary);
188
- border-radius: var(--dm-radius);
189
- transition: var(--dm-transition);
180
+ const selectMinute = (minute: number) => {
181
+ selectedMinute.value = minute
182
+ updateValue()
190
183
  }
191
184
 
192
- .dm-time-picker__input:focus {
193
- box-shadow: var(--dm-focus-ring);
194
- border-color: var(--dm-primary);
185
+ const selectSecond = (second: number) => {
186
+ selectedSecond.value = second
187
+ updateValue()
195
188
  }
196
189
 
197
- .dm-time-picker__input--error {
198
- @apply border-red-500 focus:ring-red-500 focus:border-red-500;
199
- border-color: var(--dm-error);
190
+ const updateValue = () => {
191
+ const time = formatTime()
192
+ emit('update:modelValue', time)
193
+ emit('change', time)
200
194
  }
201
195
 
202
- .dm-time-picker__input--error:focus {
203
- box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
196
+ const handleFocus = () => {
197
+ if (!props.disabled && !props.readonly) {
198
+ isOpen.value = true
199
+ }
204
200
  }
205
201
 
206
- .dm-time-picker__input--success {
207
- @apply border-green-500 focus:ring-green-500 focus:border-green-500;
208
- border-color: var(--dm-success);
202
+ const handleBlur = (event: FocusEvent) => {
203
+ setTimeout(() => {
204
+ if (!dropdownRef.value?.contains(event.relatedTarget as Node)) {
205
+ isOpen.value = false
206
+ }
207
+ }, 200)
209
208
  }
210
209
 
211
- .dm-time-picker__input--success:focus {
212
- box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
213
- }
210
+ watch(() => props.modelValue, (newValue) => {
211
+ if (newValue) {
212
+ parseTime(newValue)
213
+ }
214
+ }, { immediate: true })
215
+ </script>
214
216
 
215
- [data-theme="dark"] .dm-time-picker__input {
216
- background-color: var(--dm-bg-secondary);
217
- border-color: var(--dm-gray-600, #4b5563);
218
- color: var(--dm-text-primary);
217
+ <style scoped>
218
+ .datametria-timepicker {
219
+ position: relative;
220
+ display: inline-block;
221
+ width: 100%;
219
222
  }
220
223
 
221
- [data-theme="dark"] .dm-time-picker__input:disabled {
222
- background-color: var(--dm-gray-700, #374151);
224
+ .datametria-timepicker__input {
225
+ width: 100%;
226
+ padding: 8px 12px;
227
+ border: 1px solid var(--datametria-border-color, #dcdfe6);
228
+ border-radius: 4px;
229
+ font-size: 14px;
230
+ color: var(--datametria-text-color, #303133);
231
+ background-color: var(--datametria-bg-color, #ffffff);
232
+ cursor: pointer;
233
+ transition: border-color 0.2s;
223
234
  }
224
235
 
225
- .dm-time-picker__icon {
226
- @apply absolute right-3 top-1/2 transform -translate-y-1/2 pointer-events-none;
236
+ .datametria-timepicker__input:hover {
237
+ border-color: var(--datametria-border-color-hover, #c0c4cc);
227
238
  }
228
239
 
229
- .dm-time-picker__icon--error {
230
- @apply w-5 h-5 text-red-500;
231
- color: var(--dm-error);
240
+ .datametria-timepicker__input:focus {
241
+ outline: none;
242
+ border-color: var(--datametria-primary-color, #0072ce);
232
243
  }
233
244
 
234
- .dm-time-picker__icon--success {
235
- @apply w-5 h-5 text-green-500;
236
- color: var(--dm-success);
245
+ .datametria-timepicker--disabled .datametria-timepicker__input {
246
+ background-color: var(--datametria-disabled-bg-color, #f5f7fa);
247
+ cursor: not-allowed;
237
248
  }
238
249
 
239
- .dm-time-picker__messages {
240
- @apply mt-1;
250
+ .datametria-timepicker__dropdown {
251
+ background: var(--datametria-bg-color, #ffffff);
252
+ border: 1px solid var(--datametria-border-color, #dcdfe6);
253
+ border-radius: 4px;
254
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
255
+ padding: 12px;
241
256
  }
242
257
 
243
- .dm-time-picker__error {
244
- @apply text-sm text-red-600;
245
- color: var(--dm-error);
258
+ .datametria-timepicker__spinners {
259
+ display: flex;
260
+ gap: 12px;
246
261
  }
247
262
 
248
- .dm-time-picker__success {
249
- @apply text-sm text-green-600;
250
- color: var(--dm-success);
263
+ .datametria-timepicker__spinner {
264
+ display: flex;
265
+ flex-direction: column;
251
266
  }
252
267
 
253
- .dm-time-picker__helper {
254
- @apply text-sm text-gray-500;
255
- color: var(--dm-gray-500, #6b7280);
268
+ .datametria-timepicker__spinner-title {
269
+ font-size: 12px;
270
+ color: var(--datametria-info-color, #909399);
271
+ text-align: center;
272
+ margin-bottom: 8px;
256
273
  }
257
274
 
258
- [data-theme="dark"] .dm-time-picker__helper {
259
- color: var(--dm-text-secondary);
275
+ .datametria-timepicker__spinner-list {
276
+ display: flex;
277
+ flex-direction: column;
278
+ max-height: 200px;
279
+ overflow-y: auto;
280
+ border: 1px solid var(--datametria-border-color, #dcdfe6);
281
+ border-radius: 4px;
260
282
  }
261
283
 
262
- /* Responsive adjustments */
263
- @media (max-width: 640px) {
264
- .dm-time-picker__input {
265
- @apply text-base; /* Prevent zoom on iOS */
266
- }
284
+ .datametria-timepicker__spinner-item {
285
+ padding: 8px 16px;
286
+ border: none;
287
+ background: none;
288
+ cursor: pointer;
289
+ text-align: center;
290
+ font-size: 14px;
291
+ color: var(--datametria-text-color, #303133);
292
+ transition: background-color 0.2s;
267
293
  }
268
294
 
269
- /* High contrast mode support */
270
- @media (prefers-contrast: high) {
271
- .dm-time-picker__input {
272
- @apply border-2;
273
- }
274
-
275
- .dm-time-picker__input:focus {
276
- @apply border-4;
277
- }
295
+ .datametria-timepicker__spinner-item:hover {
296
+ background-color: var(--datametria-hover-bg-color, #f5f7fa);
278
297
  }
279
298
 
280
- /* Reduced motion support */
281
- @media (prefers-reduced-motion: reduce) {
282
- .dm-time-picker__input {
283
- @apply transition-none;
284
- }
299
+ .datametria-timepicker__spinner-item--active {
300
+ background-color: var(--datametria-primary-color, #0072ce);
301
+ color: white;
285
302
  }
286
- </style>
303
+ </style>
@@ -0,0 +1,124 @@
1
+ <template>
2
+ <div class="datametria-tree" role="tree">
3
+ <DatametriaTreeNode
4
+ v-for="node in data"
5
+ :key="getNodeKey(node)"
6
+ :node="node"
7
+ :node-key="nodeKey"
8
+ :checkable="checkable"
9
+ :default-expand-all="defaultExpandAll"
10
+ :expanded-keys="expandedKeysSet"
11
+ :checked-keys="checkedKeysSet"
12
+ @toggle="handleToggle"
13
+ @check="handleCheck"
14
+ />
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { ref, computed } from 'vue'
20
+ import DatametriaTreeNode from './DatametriaTreeNode.vue'
21
+
22
+ export interface TreeNode {
23
+ [key: string]: any
24
+ children?: TreeNode[]
25
+ }
26
+
27
+ interface Props {
28
+ data: TreeNode[]
29
+ nodeKey?: string
30
+ defaultExpandAll?: boolean
31
+ checkable?: boolean
32
+ defaultExpandedKeys?: (string | number)[]
33
+ defaultCheckedKeys?: (string | number)[]
34
+ }
35
+
36
+ const props = withDefaults(defineProps<Props>(), {
37
+ nodeKey: 'id',
38
+ defaultExpandAll: false,
39
+ checkable: false,
40
+ defaultExpandedKeys: () => [],
41
+ defaultCheckedKeys: () => []
42
+ })
43
+
44
+ const emit = defineEmits<{
45
+ nodeExpand: [node: TreeNode]
46
+ nodeCollapse: [node: TreeNode]
47
+ check: [checkedKeys: (string | number)[], node: TreeNode]
48
+ }>()
49
+
50
+ const getAllKeys = (nodes: TreeNode[]): (string | number)[] => {
51
+ const keys: (string | number)[] = []
52
+ nodes.forEach(node => {
53
+ keys.push(node[props.nodeKey])
54
+ if (node.children) {
55
+ keys.push(...getAllKeys(node.children))
56
+ }
57
+ })
58
+ return keys
59
+ }
60
+
61
+ const initialKeys = props.defaultExpandAll
62
+ ? getAllKeys(props.data)
63
+ : props.defaultExpandedKeys
64
+
65
+ const expandedKeys = ref<Set<string | number>>(new Set(initialKeys))
66
+ const checkedKeys = ref<Set<string | number>>(new Set(props.defaultCheckedKeys))
67
+
68
+ const expandedKeysSet = computed(() => expandedKeys.value)
69
+ const checkedKeysSet = computed(() => checkedKeys.value)
70
+
71
+ const getNodeKey = (node: TreeNode): string | number => {
72
+ return node[props.nodeKey]
73
+ }
74
+
75
+ const handleToggle = (node: TreeNode) => {
76
+ const key = getNodeKey(node)
77
+
78
+ if (expandedKeys.value.has(key)) {
79
+ expandedKeys.value.delete(key)
80
+ emit('nodeCollapse', node)
81
+ } else {
82
+ expandedKeys.value.add(key)
83
+ emit('nodeExpand', node)
84
+ }
85
+ }
86
+
87
+ const handleCheck = (node: TreeNode, checked: boolean) => {
88
+ const key = getNodeKey(node)
89
+
90
+ if (checked) {
91
+ checkedKeys.value.add(key)
92
+ } else {
93
+ checkedKeys.value.delete(key)
94
+ }
95
+
96
+ emit('check', Array.from(checkedKeys.value), node)
97
+ }
98
+
99
+ const expandAll = () => {
100
+ expandedKeys.value = new Set(getAllKeys(props.data))
101
+ }
102
+
103
+ const collapseAll = () => {
104
+ expandedKeys.value.clear()
105
+ }
106
+
107
+ defineExpose({
108
+ expandAll,
109
+ collapseAll
110
+ })
111
+ </script>
112
+
113
+ <style scoped>
114
+ .datametria-tree {
115
+ font-size: var(--dm-font-size-base, 14px);
116
+ color: var(--dm-color-text, #333);
117
+ }
118
+
119
+ @media (prefers-color-scheme: dark) {
120
+ .datametria-tree {
121
+ color: var(--dm-color-text-dark, #e0e0e0);
122
+ }
123
+ }
124
+ </style>