@m3ui-vue/m3ui-vue 0.1.11 → 0.2.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.
- package/dist/{MMenuItem-_n5OG5MT.js → MMenuItem-DpoEsH91.js} +215 -122
- package/dist/MMenuItem-DpoEsH91.js.map +1 -0
- package/dist/components/MAlert.vue.d.ts +1 -0
- package/dist/components/MButton.vue.d.ts +1 -1
- package/dist/components/MCalendar.vue.d.ts +3 -0
- package/dist/components/MCarousel.vue.d.ts +36 -0
- package/dist/components/MChip.vue.d.ts +3 -3
- package/dist/components/MCommandPalette.vue.d.ts +3 -2
- package/dist/components/MConfirmDialog.vue.d.ts +0 -2
- package/dist/components/MDataTable.vue.d.ts +25 -2
- package/dist/components/MDatePicker.vue.d.ts +2 -0
- package/dist/components/MDateRangePicker.vue.d.ts +5 -0
- package/dist/components/MDialog.vue.d.ts +1 -0
- package/dist/components/MFab.vue.d.ts +1 -1
- package/dist/components/MFileUpload.vue.d.ts +15 -1
- package/dist/components/MFlex.vue.d.ts +1 -1
- package/dist/components/MInfiniteScroll.vue.d.ts +7 -5
- package/dist/components/MMaskField.vue.d.ts +24 -0
- package/dist/components/MMultiSelect.vue.d.ts +20 -6
- package/dist/components/MNavigationDrawer.vue.d.ts +9 -3
- package/dist/components/MNumberField.vue.d.ts +27 -0
- package/dist/components/MRichTextEditor.vue.d.ts +28 -1
- package/dist/components/MScheduler.vue.d.ts +5 -0
- package/dist/components/MSelect.vue.d.ts +12 -9
- package/dist/components/MSnackbar.vue.d.ts +4 -1
- package/dist/components/MSpinner.vue.d.ts +1 -0
- package/dist/components/MSplitter.vue.d.ts +1 -1
- package/dist/components/MSpotlightSearch.vue.d.ts +3 -2
- package/dist/components/MStack.vue.d.ts +1 -1
- package/dist/components/MTable.vue.d.ts +3 -2
- package/dist/components/MTextField.vue.d.ts +2 -5
- package/dist/components/MTimePicker.vue.d.ts +1 -0
- package/dist/components/MTour.vue.d.ts +3 -0
- package/dist/components/MTransferList.vue.d.ts +19 -3
- package/dist/components/MTree.vue.d.ts +1 -1
- package/dist/components/_MDrawerItemList.vue.d.ts +57 -0
- package/dist/composables/useLocale.d.ts +74 -0
- package/dist/index.d.ts +10 -1
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/es.d.ts +2 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/index.d.ts +8 -0
- package/dist/locales/ja.d.ts +2 -0
- package/dist/locales/ko.d.ts +2 -0
- package/dist/locales/pt.d.ts +2 -0
- package/dist/locales/zh.d.ts +2 -0
- package/dist/m3ui-vue.css +1 -1
- package/dist/m3ui.js +3278 -2489
- package/dist/m3ui.js.map +1 -1
- package/dist/plugin.d.ts +3 -1
- package/dist/rich-text-editor.d.ts +1 -0
- package/dist/rich-text-editor.js +141 -113
- package/dist/rich-text-editor.js.map +1 -1
- package/dist/styles/palettes.css +660 -0
- package/dist/styles.css +1 -1
- package/package.json +6 -2
- package/src/components/MAlert.vue +5 -1
- package/src/components/MCalendar.vue +14 -5
- package/src/components/MCarousel.vue +203 -0
- package/src/components/MCommandPalette.vue +11 -7
- package/src/components/MConfirmDialog.vue +6 -5
- package/src/components/MDataTable.vue +78 -14
- package/src/components/MDatePicker.vue +11 -4
- package/src/components/MDateRangePicker.vue +15 -5
- package/src/components/MDialog.vue +5 -1
- package/src/components/MFileUpload.vue +22 -10
- package/src/components/MInfiniteScroll.vue +14 -9
- package/src/components/MMaskField.vue +198 -0
- package/src/components/MMultiSelect.vue +53 -24
- package/src/components/MNavigationDrawer.vue +282 -107
- package/src/components/MNumberField.vue +176 -0
- package/src/components/MRichTextEditor.vue +95 -34
- package/src/components/MScheduler.vue +12 -4
- package/src/components/MSelect.vue +41 -14
- package/src/components/MSnackbar.vue +8 -1
- package/src/components/MSpinner.vue +5 -1
- package/src/components/MSpotlightSearch.vue +11 -7
- package/src/components/MTable.vue +8 -4
- package/src/components/MTextField.vue +19 -10
- package/src/components/MTimePicker.vue +5 -1
- package/src/components/MTour.vue +12 -9
- package/src/components/MTransferList.vue +30 -15
- package/src/components/_MDrawerItemList.vue +105 -0
- package/src/composables/useColorPalette.ts +30 -20
- package/src/composables/useLocale.ts +181 -0
- package/src/index.ts +10 -1
- package/src/locales/de.ts +93 -0
- package/src/locales/es.ts +93 -0
- package/src/locales/fr.ts +93 -0
- package/src/locales/index.ts +8 -0
- package/src/locales/ja.ts +93 -0
- package/src/locales/ko.ts +93 -0
- package/src/locales/pt.ts +93 -0
- package/src/locales/zh.ts +93 -0
- package/src/plugin.ts +7 -1
- package/src/rich-text-editor.ts +1 -0
- package/src/styles/palettes.css +660 -0
- package/dist/MMenuItem-_n5OG5MT.js.map +0 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref, useSlots, watch } from 'vue'
|
|
2
|
+
import { computed, ref, useSlots, watch, onMounted } from 'vue'
|
|
3
3
|
import MCheckbox from './MCheckbox.vue'
|
|
4
4
|
import MIcon from './MIcon.vue'
|
|
5
5
|
import MIconButton from './MIconButton.vue'
|
|
6
6
|
import MPagination from './MPagination.vue'
|
|
7
7
|
import MChip from './MChip.vue'
|
|
8
|
+
import { useLocale } from '../composables/useLocale'
|
|
9
|
+
|
|
10
|
+
const locale = useLocale()
|
|
8
11
|
|
|
9
12
|
export interface DataTableColumn {
|
|
10
13
|
key: string
|
|
@@ -43,9 +46,18 @@ const props = withDefaults(defineProps<{
|
|
|
43
46
|
groupBy?: string
|
|
44
47
|
columnToggle?: boolean
|
|
45
48
|
exportable?: boolean
|
|
49
|
+
serverSide?: boolean
|
|
50
|
+
total?: number
|
|
51
|
+
page?: number
|
|
52
|
+
searchPlaceholder?: string
|
|
53
|
+
selectedText?: string
|
|
54
|
+
recordsText?: string
|
|
55
|
+
expandLabel?: string
|
|
56
|
+
columnsLabel?: string
|
|
57
|
+
exportLabel?: string
|
|
58
|
+
noGroupText?: string
|
|
46
59
|
}>(), {
|
|
47
60
|
loading: false,
|
|
48
|
-
emptyText: 'Sin resultados',
|
|
49
61
|
rowKey: 'id',
|
|
50
62
|
selectable: false,
|
|
51
63
|
modelValue: () => [],
|
|
@@ -57,11 +69,24 @@ const props = withDefaults(defineProps<{
|
|
|
57
69
|
stickyHeader: false,
|
|
58
70
|
columnToggle: false,
|
|
59
71
|
exportable: false,
|
|
72
|
+
serverSide: false,
|
|
73
|
+
total: 0,
|
|
74
|
+
page: 1,
|
|
60
75
|
})
|
|
61
76
|
|
|
77
|
+
export interface DataTableFetchParams {
|
|
78
|
+
page: number
|
|
79
|
+
perPage: number
|
|
80
|
+
search: string
|
|
81
|
+
sortKey: string
|
|
82
|
+
sortDir: 'asc' | 'desc' | ''
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
const emit = defineEmits<{
|
|
63
86
|
'update:modelValue': [Record<string, any>[]]
|
|
87
|
+
'update:page': [number]
|
|
64
88
|
rowClick: [Record<string, any>]
|
|
89
|
+
fetch: [DataTableFetchParams]
|
|
65
90
|
}>()
|
|
66
91
|
|
|
67
92
|
const slots = useSlots()
|
|
@@ -87,7 +112,16 @@ function toggleSort(key: string) {
|
|
|
87
112
|
else { sortKey.value = ''; sortDir.value = '' }
|
|
88
113
|
}
|
|
89
114
|
|
|
115
|
+
const currentPage = computed({
|
|
116
|
+
get: () => props.serverSide ? (props.page ?? 1) : internalPage.value,
|
|
117
|
+
set: (val: number) => {
|
|
118
|
+
if (props.serverSide) emit('update:page', val)
|
|
119
|
+
else internalPage.value = val
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
90
123
|
const processedRows = computed(() => {
|
|
124
|
+
if (props.serverSide) return props.rows
|
|
91
125
|
let result = props.rows
|
|
92
126
|
if (search.value.trim()) {
|
|
93
127
|
const q = search.value.toLowerCase()
|
|
@@ -112,20 +146,50 @@ const groupedRows = computed(() => {
|
|
|
112
146
|
if (!props.groupBy) return null
|
|
113
147
|
const map = new Map<string, Record<string, any>[]>()
|
|
114
148
|
for (const row of processedRows.value) {
|
|
115
|
-
const key = String(row[props.groupBy] ??
|
|
149
|
+
const key = String(row[props.groupBy] ?? (props.noGroupText ?? locale.noGroup))
|
|
116
150
|
if (!map.has(key)) map.set(key, [])
|
|
117
151
|
map.get(key)!.push(row)
|
|
118
152
|
}
|
|
119
153
|
return map
|
|
120
154
|
})
|
|
121
155
|
|
|
122
|
-
const totalCount = computed(() => processedRows.value.length)
|
|
156
|
+
const totalCount = computed(() => props.serverSide ? (props.total ?? 0) : processedRows.value.length)
|
|
123
157
|
const visibleRows = computed(() => {
|
|
124
|
-
|
|
158
|
+
if (props.serverSide) return props.rows
|
|
159
|
+
const start = (currentPage.value - 1) * props.perPage
|
|
125
160
|
return processedRows.value.slice(start, start + props.perPage)
|
|
126
161
|
})
|
|
127
162
|
|
|
128
|
-
watch([search, sortKey, sortDir], () => {
|
|
163
|
+
watch([search, sortKey, sortDir], () => {
|
|
164
|
+
if (!props.serverSide) internalPage.value = 1
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
function emitFetch() {
|
|
168
|
+
emit('fetch', {
|
|
169
|
+
page: currentPage.value,
|
|
170
|
+
perPage: props.perPage,
|
|
171
|
+
search: search.value,
|
|
172
|
+
sortKey: sortKey.value,
|
|
173
|
+
sortDir: sortDir.value,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const mounted = ref(false)
|
|
178
|
+
onMounted(() => {
|
|
179
|
+
mounted.value = true
|
|
180
|
+
if (props.serverSide) emitFetch()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
watch([search, sortKey, sortDir], () => {
|
|
184
|
+
if (!props.serverSide || !mounted.value) return
|
|
185
|
+
internalPage.value = 1
|
|
186
|
+
emitFetch()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
watch(currentPage, () => {
|
|
190
|
+
if (!props.serverSide || !mounted.value) return
|
|
191
|
+
emitFetch()
|
|
192
|
+
})
|
|
129
193
|
|
|
130
194
|
const selected = computed({
|
|
131
195
|
get: () => props.modelValue ?? [],
|
|
@@ -214,7 +278,7 @@ function colStyle(col: DataTableColumn) {
|
|
|
214
278
|
>
|
|
215
279
|
<div v-if="searchable" class="flex min-w-48 flex-1 items-center gap-2 rounded-full border border-outline-variant bg-surface-container px-3 py-1.5 transition-[border-color,box-shadow] duration-150 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30">
|
|
216
280
|
<MIcon name="search" :size="16" class="shrink-0 text-on-surface-variant" />
|
|
217
|
-
<input v-model="search" type="text" placeholder="
|
|
281
|
+
<input v-model="search" type="text" :placeholder="searchPlaceholder ?? locale.search" class="w-full bg-transparent text-body-medium text-on-surface outline-none placeholder:text-on-surface-variant" />
|
|
218
282
|
<button v-if="search" class="text-on-surface-variant transition-colors hover:text-on-surface" @click="search = ''">
|
|
219
283
|
<MIcon name="close" :size="14" />
|
|
220
284
|
</button>
|
|
@@ -224,13 +288,13 @@ function colStyle(col: DataTableColumn) {
|
|
|
224
288
|
|
|
225
289
|
<Transition enter-active-class="transition-[opacity,transform] duration-150" enter-from-class="opacity-0 scale-90" leave-active-class="transition-[opacity,transform] duration-100" leave-to-class="opacity-0 scale-90">
|
|
226
290
|
<span v-if="selectable && selected.length > 0" class="rounded-full bg-primary/12 px-3 py-1 text-label-small font-medium text-primary">
|
|
227
|
-
{{ selected.length }}
|
|
291
|
+
{{ selected.length }} {{ selectedText ?? locale.selectedCount }}{{ selected.length !== 1 ? 's' : '' }}
|
|
228
292
|
</span>
|
|
229
293
|
</Transition>
|
|
230
294
|
|
|
231
295
|
<!-- Column toggle -->
|
|
232
296
|
<div v-if="columnToggle" class="relative">
|
|
233
|
-
<MIconButton icon="view_column" label="
|
|
297
|
+
<MIconButton icon="view_column" :label="columnsLabel ?? locale.columns" :size="36" @click="showColMenu = !showColMenu" />
|
|
234
298
|
<div v-if="showColMenu" class="absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg bg-surface-container py-2 shadow-elevation-3">
|
|
235
299
|
<label v-for="col in columns" :key="col.key" class="flex cursor-pointer items-center gap-2 px-3 py-1.5 hover:bg-on-surface/4">
|
|
236
300
|
<MCheckbox
|
|
@@ -242,7 +306,7 @@ function colStyle(col: DataTableColumn) {
|
|
|
242
306
|
</div>
|
|
243
307
|
</div>
|
|
244
308
|
|
|
245
|
-
<MIconButton v-if="exportable" icon="download" label="
|
|
309
|
+
<MIconButton v-if="exportable" icon="download" :label="exportLabel ?? locale.exportCsv" :size="36" @click="exportCSV" />
|
|
246
310
|
</div>
|
|
247
311
|
|
|
248
312
|
<!-- Table -->
|
|
@@ -308,7 +372,7 @@ function colStyle(col: DataTableColumn) {
|
|
|
308
372
|
<td :colspan="visibleColumns.length + extraCols" class="border-t border-outline-variant px-4 py-14 text-center">
|
|
309
373
|
<slot name="empty">
|
|
310
374
|
<MIcon name="search_off" :size="36" class="mb-2 text-on-surface-variant opacity-30" />
|
|
311
|
-
<p class="text-body-medium text-on-surface-variant">{{ emptyText }}</p>
|
|
375
|
+
<p class="text-body-medium text-on-surface-variant">{{ emptyText ?? locale.noResults }}</p>
|
|
312
376
|
</slot>
|
|
313
377
|
</td>
|
|
314
378
|
</tr>
|
|
@@ -330,7 +394,7 @@ function colStyle(col: DataTableColumn) {
|
|
|
330
394
|
<td v-if="hasExpand" class="px-2" :class="dense ? 'py-1' : 'py-2'" @click.stop>
|
|
331
395
|
<MIconButton
|
|
332
396
|
icon="expand_more"
|
|
333
|
-
label="
|
|
397
|
+
:label="expandLabel ?? locale.expand"
|
|
334
398
|
:size="28"
|
|
335
399
|
:class="isExpanded(row) ? 'rotate-180' : ''"
|
|
336
400
|
class="transition-transform duration-200"
|
|
@@ -374,9 +438,9 @@ function colStyle(col: DataTableColumn) {
|
|
|
374
438
|
<!-- Footer -->
|
|
375
439
|
<div class="flex items-center justify-between gap-4 border-t border-outline-variant bg-surface-container-lowest px-4 py-2">
|
|
376
440
|
<span class="text-label-small text-on-surface-variant">
|
|
377
|
-
{{ totalCount }}
|
|
441
|
+
{{ totalCount }} {{ recordsText ?? locale.recordCount }}{{ totalCount !== 1 ? 's' : '' }}
|
|
378
442
|
</span>
|
|
379
|
-
<MPagination :page="
|
|
443
|
+
<MPagination :page="currentPage" :per-page="perPage" :total="totalCount" @update:page="currentPage = $event" />
|
|
380
444
|
</div>
|
|
381
445
|
</div>
|
|
382
446
|
</template>
|
|
@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
|
3
3
|
import MIcon from './MIcon.vue'
|
|
4
4
|
import MIconButton from './MIconButton.vue'
|
|
5
5
|
import { useFieldBg } from '../composables/useFieldBg'
|
|
6
|
+
import { useLocale } from '../composables/useLocale'
|
|
6
7
|
|
|
7
8
|
const props = withDefaults(defineProps<{
|
|
8
9
|
modelValue: string | null
|
|
@@ -15,7 +16,13 @@ const props = withDefaults(defineProps<{
|
|
|
15
16
|
hint?: string
|
|
16
17
|
locale?: string
|
|
17
18
|
fieldBg?: string
|
|
18
|
-
|
|
19
|
+
prevMonthLabel?: string
|
|
20
|
+
nextMonthLabel?: string
|
|
21
|
+
}>(), {
|
|
22
|
+
locale: 'es-ES',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const localeStrings = useLocale()
|
|
19
26
|
|
|
20
27
|
const emit = defineEmits<{ 'update:modelValue': [string | null] }>()
|
|
21
28
|
|
|
@@ -173,7 +180,7 @@ onUnmounted(() => {
|
|
|
173
180
|
>
|
|
174
181
|
<MIcon name="calendar_today" :size="20" class="shrink-0 text-on-surface-variant" />
|
|
175
182
|
<span v-if="displayValue" class="flex-1 text-on-surface">{{ displayValue }}</span>
|
|
176
|
-
<span v-else class="flex-1 text-on-surface-variant">{{ placeholder || label ||
|
|
183
|
+
<span v-else class="flex-1 text-on-surface-variant">{{ placeholder || label || localeStrings.selectDate }}</span>
|
|
177
184
|
<MIcon
|
|
178
185
|
v-if="modelValue"
|
|
179
186
|
name="close"
|
|
@@ -210,9 +217,9 @@ onUnmounted(() => {
|
|
|
210
217
|
>
|
|
211
218
|
<!-- Header -->
|
|
212
219
|
<div class="mb-3 flex items-center justify-between">
|
|
213
|
-
<MIconButton icon="chevron_left" label="
|
|
220
|
+
<MIconButton icon="chevron_left" :label="prevMonthLabel ?? localeStrings.previousMonth" :size="36" @click="prevMonth" />
|
|
214
221
|
<span class="text-title-small font-medium capitalize text-on-surface">{{ monthLabel }}</span>
|
|
215
|
-
<MIconButton icon="chevron_right" label="
|
|
222
|
+
<MIconButton icon="chevron_right" :label="nextMonthLabel ?? localeStrings.nextMonth" :size="36" @click="nextMonth" />
|
|
216
223
|
</div>
|
|
217
224
|
|
|
218
225
|
<!-- Weekday headers -->
|
|
@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
|
3
3
|
import MIcon from './MIcon.vue'
|
|
4
4
|
import MIconButton from './MIconButton.vue'
|
|
5
5
|
import { useFieldBg } from '../composables/useFieldBg'
|
|
6
|
+
import { useLocale } from '../composables/useLocale'
|
|
6
7
|
|
|
7
8
|
export interface DateRange {
|
|
8
9
|
start: string | null
|
|
@@ -12,6 +13,7 @@ export interface DateRange {
|
|
|
12
13
|
const props = withDefaults(defineProps<{
|
|
13
14
|
modelValue: DateRange
|
|
14
15
|
label?: string
|
|
16
|
+
placeholder?: string
|
|
15
17
|
min?: string
|
|
16
18
|
max?: string
|
|
17
19
|
disabled?: boolean
|
|
@@ -19,7 +21,15 @@ const props = withDefaults(defineProps<{
|
|
|
19
21
|
hint?: string
|
|
20
22
|
locale?: string
|
|
21
23
|
fieldBg?: string
|
|
22
|
-
|
|
24
|
+
prevMonthLabel?: string
|
|
25
|
+
nextMonthLabel?: string
|
|
26
|
+
pickStartText?: string
|
|
27
|
+
pickEndText?: string
|
|
28
|
+
}>(), {
|
|
29
|
+
locale: 'es-ES',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const localeStrings = useLocale()
|
|
23
33
|
|
|
24
34
|
const emit = defineEmits<{ 'update:modelValue': [DateRange] }>()
|
|
25
35
|
|
|
@@ -184,7 +194,7 @@ onUnmounted(() => {
|
|
|
184
194
|
>
|
|
185
195
|
<MIcon name="date_range" :size="20" class="shrink-0 text-on-surface-variant" />
|
|
186
196
|
<span v-if="displayValue" class="flex-1 text-on-surface">{{ displayValue }}</span>
|
|
187
|
-
<span v-else class="flex-1 text-on-surface-variant">{{ label ||
|
|
197
|
+
<span v-else class="flex-1 text-on-surface-variant">{{ label || placeholder || localeStrings.selectRange }}</span>
|
|
188
198
|
<MIcon
|
|
189
199
|
v-if="modelValue.start || modelValue.end"
|
|
190
200
|
name="close"
|
|
@@ -219,13 +229,13 @@ onUnmounted(() => {
|
|
|
219
229
|
:style="dropPos"
|
|
220
230
|
>
|
|
221
231
|
<p class="mb-2 text-center text-label-medium text-on-surface-variant">
|
|
222
|
-
{{ picking === 'start' ?
|
|
232
|
+
{{ picking === 'start' ? (pickStartText ?? localeStrings.pickStart) : (pickEndText ?? localeStrings.pickEnd) }}
|
|
223
233
|
</p>
|
|
224
234
|
|
|
225
235
|
<div class="mb-3 flex items-center justify-between">
|
|
226
|
-
<MIconButton icon="chevron_left" label="
|
|
236
|
+
<MIconButton icon="chevron_left" :label="prevMonthLabel ?? localeStrings.previousMonth" :size="36" @click="prevMonth" />
|
|
227
237
|
<span class="text-title-small font-medium capitalize text-on-surface">{{ monthLabel }}</span>
|
|
228
|
-
<MIconButton icon="chevron_right" label="
|
|
238
|
+
<MIconButton icon="chevron_right" :label="nextMonthLabel ?? localeStrings.nextMonth" :size="36" @click="nextMonth" />
|
|
229
239
|
</div>
|
|
230
240
|
|
|
231
241
|
<div class="mb-1 grid grid-cols-7 gap-0.5 text-center">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { watch } from 'vue'
|
|
3
3
|
import MIconButton from './MIconButton.vue'
|
|
4
|
+
import { useLocale } from '../composables/useLocale'
|
|
4
5
|
|
|
5
6
|
const props = withDefaults(
|
|
6
7
|
defineProps<{
|
|
@@ -8,6 +9,7 @@ const props = withDefaults(
|
|
|
8
9
|
title?: string
|
|
9
10
|
maxWidth?: string
|
|
10
11
|
persistent?: boolean
|
|
12
|
+
closeLabel?: string
|
|
11
13
|
}>(),
|
|
12
14
|
{
|
|
13
15
|
maxWidth: 'max-w-md',
|
|
@@ -15,6 +17,8 @@ const props = withDefaults(
|
|
|
15
17
|
},
|
|
16
18
|
)
|
|
17
19
|
|
|
20
|
+
const locale = useLocale()
|
|
21
|
+
|
|
18
22
|
const emit = defineEmits<{ 'update:modelValue': [boolean] }>()
|
|
19
23
|
|
|
20
24
|
function close() {
|
|
@@ -56,7 +60,7 @@ watch(
|
|
|
56
60
|
<h2 class="text-headline-small text-on-surface">
|
|
57
61
|
<slot name="title">{{ title }}</slot>
|
|
58
62
|
</h2>
|
|
59
|
-
<MIconButton v-if="!persistent" icon="close" label="
|
|
63
|
+
<MIconButton v-if="!persistent" icon="close" :label="closeLabel ?? locale.close" @click="close" />
|
|
60
64
|
</div>
|
|
61
65
|
<div class="overflow-y-auto px-6 py-2 text-body-medium text-on-surface-variant">
|
|
62
66
|
<slot />
|
|
@@ -3,6 +3,9 @@ import { ref, computed } from 'vue'
|
|
|
3
3
|
import MIcon from './MIcon.vue'
|
|
4
4
|
import MIconButton from './MIconButton.vue'
|
|
5
5
|
import MSpinner from './MSpinner.vue'
|
|
6
|
+
import { useLocale } from '../composables/useLocale'
|
|
7
|
+
|
|
8
|
+
const locale = useLocale()
|
|
6
9
|
|
|
7
10
|
export interface UploadFile {
|
|
8
11
|
file: File
|
|
@@ -18,8 +21,15 @@ const props = withDefaults(
|
|
|
18
21
|
multiple?: boolean
|
|
19
22
|
maxSize?: number
|
|
20
23
|
disabled?: boolean
|
|
24
|
+
dropText?: string
|
|
25
|
+
selectText?: string
|
|
26
|
+
maxSizePrefix?: string
|
|
27
|
+
removeLabel?: string
|
|
21
28
|
}>(),
|
|
22
|
-
{
|
|
29
|
+
{
|
|
30
|
+
multiple: false,
|
|
31
|
+
disabled: false,
|
|
32
|
+
},
|
|
23
33
|
)
|
|
24
34
|
|
|
25
35
|
const emit = defineEmits<{
|
|
@@ -128,14 +138,16 @@ function openPicker() {
|
|
|
128
138
|
class="text-on-surface-variant"
|
|
129
139
|
/>
|
|
130
140
|
<div class="text-center">
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
<slot name="dropzone">
|
|
142
|
+
<p class="text-body-large text-on-surface">
|
|
143
|
+
{{ dropText ?? locale.dropText }} <span class="font-medium text-primary">{{ selectText ?? locale.selectText }}</span>
|
|
144
|
+
</p>
|
|
145
|
+
<p v-if="accept || maxSize" class="mt-1 text-body-small text-on-surface-variant">
|
|
146
|
+
<span v-if="accept">{{ accept }}</span>
|
|
147
|
+
<span v-if="accept && maxSize"> · </span>
|
|
148
|
+
<span v-if="maxSize">{{ maxSizePrefix ?? locale.maxSizePrefix }} {{ formatSize(maxSize) }}</span>
|
|
149
|
+
</p>
|
|
150
|
+
</slot>
|
|
139
151
|
</div>
|
|
140
152
|
</div>
|
|
141
153
|
|
|
@@ -187,7 +199,7 @@ function openPicker() {
|
|
|
187
199
|
<MIcon v-else-if="entry.status === 'done'" name="check_circle" :size="20" class="text-success" />
|
|
188
200
|
<MIcon v-else-if="entry.status === 'error'" name="error" :size="20" class="text-error" />
|
|
189
201
|
|
|
190
|
-
<MIconButton icon="close" label="
|
|
202
|
+
<MIconButton icon="close" :label="removeLabel ?? locale.remove" :size="32" @click="removeFile(entry)" />
|
|
191
203
|
</div>
|
|
192
204
|
</TransitionGroup>
|
|
193
205
|
</div>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
|
3
3
|
import MSpinner from './MSpinner.vue'
|
|
4
|
+
import { useLocale } from '../composables/useLocale'
|
|
5
|
+
|
|
6
|
+
const locale = useLocale()
|
|
4
7
|
|
|
5
8
|
const props = withDefaults(
|
|
6
9
|
defineProps<{
|
|
@@ -15,8 +18,6 @@ const props = withDefaults(
|
|
|
15
18
|
loading: false,
|
|
16
19
|
disabled: false,
|
|
17
20
|
threshold: 100,
|
|
18
|
-
loadingText: 'Cargando...',
|
|
19
|
-
endText: 'No hay más elementos',
|
|
20
21
|
ended: false,
|
|
21
22
|
},
|
|
22
23
|
)
|
|
@@ -55,13 +56,17 @@ onBeforeUnmount(() => observer?.disconnect())
|
|
|
55
56
|
<slot />
|
|
56
57
|
|
|
57
58
|
<div ref="sentinelRef" class="flex items-center justify-center py-4">
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
<slot v-if="loading" name="loading">
|
|
60
|
+
<div class="flex items-center gap-3">
|
|
61
|
+
<MSpinner :size="20" class="text-primary" />
|
|
62
|
+
<span class="text-body-medium text-on-surface-variant">{{ loadingText ?? locale.loadingMore }}</span>
|
|
63
|
+
</div>
|
|
64
|
+
</slot>
|
|
65
|
+
<slot v-else-if="ended" name="end">
|
|
66
|
+
<p class="text-body-small text-on-surface-variant">
|
|
67
|
+
{{ endText ?? locale.noMoreItems }}
|
|
68
|
+
</p>
|
|
69
|
+
</slot>
|
|
65
70
|
<slot v-else name="idle" />
|
|
66
71
|
</div>
|
|
67
72
|
</div>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, useId, watch } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
import { useFieldBg } from '../composables/useFieldBg'
|
|
5
|
+
|
|
6
|
+
export type MaskPreset = 'credit-card' | 'phone' | 'date' | 'time' | 'cpf' | 'cnpj' | 'zip'
|
|
7
|
+
|
|
8
|
+
const PRESETS: Record<MaskPreset, string> = {
|
|
9
|
+
'credit-card': '#### #### #### ####',
|
|
10
|
+
phone: '(###) ###-####',
|
|
11
|
+
date: '##/##/####',
|
|
12
|
+
time: '##:##',
|
|
13
|
+
cpf: '###.###.###-##',
|
|
14
|
+
cnpj: '##.###.###/####-##',
|
|
15
|
+
zip: '#####-####',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(
|
|
19
|
+
defineProps<{
|
|
20
|
+
modelValue: string
|
|
21
|
+
label: string
|
|
22
|
+
mask: string | MaskPreset
|
|
23
|
+
variant?: 'filled' | 'outlined'
|
|
24
|
+
error?: string
|
|
25
|
+
hint?: string
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
required?: boolean
|
|
28
|
+
leadingIcon?: string
|
|
29
|
+
clearable?: boolean
|
|
30
|
+
fieldBg?: string
|
|
31
|
+
}>(),
|
|
32
|
+
{
|
|
33
|
+
variant: 'filled',
|
|
34
|
+
clearable: false,
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const emit = defineEmits<{ 'update:modelValue': [string] }>()
|
|
39
|
+
|
|
40
|
+
const id = useId()
|
|
41
|
+
const fieldBgEl = ref<HTMLElement | null>(null)
|
|
42
|
+
const { resolvedFieldBg } = useFieldBg(fieldBgEl, () => props.fieldBg)
|
|
43
|
+
const inputEl = ref<HTMLInputElement | null>(null)
|
|
44
|
+
|
|
45
|
+
const resolvedMask = computed(() => PRESETS[props.mask as MaskPreset] ?? props.mask)
|
|
46
|
+
|
|
47
|
+
const showClear = computed(() => props.clearable && props.modelValue.length > 0 && !props.disabled)
|
|
48
|
+
|
|
49
|
+
function rawToMasked(raw: string, mask: string): string {
|
|
50
|
+
let result = ''
|
|
51
|
+
let ri = 0
|
|
52
|
+
for (let mi = 0; mi < mask.length && ri < raw.length; mi++) {
|
|
53
|
+
if (mask[mi] === '#') {
|
|
54
|
+
result += raw[ri++]
|
|
55
|
+
} else {
|
|
56
|
+
result += mask[mi]
|
|
57
|
+
if (raw[ri] === mask[mi]) ri++
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function maskedToRaw(masked: string): string {
|
|
64
|
+
const mask = resolvedMask.value
|
|
65
|
+
let raw = ''
|
|
66
|
+
for (let i = 0; i < masked.length && i < mask.length; i++) {
|
|
67
|
+
if (mask[i] === '#') raw += masked[i]
|
|
68
|
+
}
|
|
69
|
+
return raw
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onInput(e: Event) {
|
|
73
|
+
const input = e.target as HTMLInputElement
|
|
74
|
+
const raw = input.value.replace(/[^\d]/g, '')
|
|
75
|
+
const masked = rawToMasked(raw, resolvedMask.value)
|
|
76
|
+
emit('update:modelValue', masked)
|
|
77
|
+
|
|
78
|
+
requestAnimationFrame(() => {
|
|
79
|
+
if (inputEl.value) inputEl.value.value = masked
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function onKeydown(e: KeyboardEvent) {
|
|
84
|
+
if (e.key === 'Backspace' && props.modelValue.length > 0) {
|
|
85
|
+
const raw = maskedToRaw(props.modelValue)
|
|
86
|
+
if (raw.length > 0) {
|
|
87
|
+
e.preventDefault()
|
|
88
|
+
const newRaw = raw.slice(0, -1)
|
|
89
|
+
const masked = rawToMasked(newRaw, resolvedMask.value)
|
|
90
|
+
emit('update:modelValue', masked)
|
|
91
|
+
requestAnimationFrame(() => {
|
|
92
|
+
if (inputEl.value) inputEl.value.value = masked
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const hasTrailing = computed(() => props.clearable)
|
|
99
|
+
|
|
100
|
+
const inputClasses = computed(() => {
|
|
101
|
+
const pl = props.leadingIcon ? 'pl-12' : 'pl-4'
|
|
102
|
+
const pr = hasTrailing.value ? 'pr-12' : 'pr-4'
|
|
103
|
+
const base = [
|
|
104
|
+
'peer block w-full text-body-large text-on-surface outline-none placeholder:text-transparent',
|
|
105
|
+
'transition-[border-color,border-width] duration-150',
|
|
106
|
+
'disabled:cursor-not-allowed disabled:opacity-[0.38]',
|
|
107
|
+
'h-14', pl, pr,
|
|
108
|
+
]
|
|
109
|
+
if (props.variant === 'outlined') {
|
|
110
|
+
return [...base,
|
|
111
|
+
'rounded-sm border bg-transparent py-4',
|
|
112
|
+
props.error
|
|
113
|
+
? 'border-error focus:border-2 focus:border-error'
|
|
114
|
+
: 'border-outline hover:border-on-surface focus:border-2 focus:border-primary',
|
|
115
|
+
].join(' ')
|
|
116
|
+
}
|
|
117
|
+
return [...base,
|
|
118
|
+
'rounded-t-sm bg-surface-container-highest border-b pt-6 pb-2',
|
|
119
|
+
props.error
|
|
120
|
+
? 'border-error focus:border-b-2 focus:border-error'
|
|
121
|
+
: 'border-on-surface-variant hover:border-on-surface focus:border-b-2 focus:border-primary',
|
|
122
|
+
].join(' ')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const labelClasses = computed(() => {
|
|
126
|
+
const left = props.leadingIcon
|
|
127
|
+
? (props.variant === 'outlined' ? 'left-11' : 'left-12')
|
|
128
|
+
: (props.variant === 'outlined' ? 'left-3' : 'left-4')
|
|
129
|
+
const unfloatedTop = props.variant === 'filled' ? 'top-[53%]' : 'top-1/2'
|
|
130
|
+
const base = [
|
|
131
|
+
'pointer-events-none absolute truncate transition-all duration-200',
|
|
132
|
+
left, 'right-4', `${unfloatedTop} -translate-y-1/2 text-body-large`,
|
|
133
|
+
]
|
|
134
|
+
if (props.variant === 'outlined') {
|
|
135
|
+
return [...base,
|
|
136
|
+
'peer-focus:-top-2.5 peer-focus:translate-y-0 peer-focus:text-label-small peer-focus:right-auto peer-focus:max-w-[calc(100%-1.5rem)] peer-focus:bg-[var(--field-bg)] peer-focus:px-1',
|
|
137
|
+
'peer-[&:not(:placeholder-shown)]:-top-2.5 peer-[&:not(:placeholder-shown)]:translate-y-0 peer-[&:not(:placeholder-shown)]:right-auto peer-[&:not(:placeholder-shown)]:max-w-[calc(100%-1.5rem)]',
|
|
138
|
+
'peer-[&:not(:placeholder-shown)]:text-label-small peer-[&:not(:placeholder-shown)]:bg-[var(--field-bg)] peer-[&:not(:placeholder-shown)]:px-1',
|
|
139
|
+
props.error ? 'text-error peer-focus:text-error' : 'text-on-surface-variant peer-focus:text-primary',
|
|
140
|
+
].join(' ')
|
|
141
|
+
}
|
|
142
|
+
return [...base,
|
|
143
|
+
'peer-focus:top-2 peer-focus:translate-y-0 peer-focus:text-label-small',
|
|
144
|
+
'peer-[&:not(:placeholder-shown)]:top-2 peer-[&:not(:placeholder-shown)]:translate-y-0 peer-[&:not(:placeholder-shown)]:text-label-small',
|
|
145
|
+
props.error ? 'text-error peer-focus:text-error' : 'text-on-surface-variant peer-focus:text-primary',
|
|
146
|
+
].join(' ')
|
|
147
|
+
})
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<template>
|
|
151
|
+
<div class="flex flex-col gap-1">
|
|
152
|
+
<div
|
|
153
|
+
ref="fieldBgEl"
|
|
154
|
+
class="relative"
|
|
155
|
+
:class="variant === 'outlined' ? 'mt-2' : ''"
|
|
156
|
+
:style="variant === 'outlined' ? { '--field-bg': resolvedFieldBg } : undefined"
|
|
157
|
+
>
|
|
158
|
+
<div
|
|
159
|
+
v-if="leadingIcon"
|
|
160
|
+
class="pointer-events-none absolute left-3.5 text-on-surface-variant"
|
|
161
|
+
:class="variant === 'filled' ? 'top-[57%] -translate-y-1/2' : 'top-[55%] -translate-y-1/2'"
|
|
162
|
+
>
|
|
163
|
+
<MIcon :name="leadingIcon" :size="20" />
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<input
|
|
167
|
+
:id="id"
|
|
168
|
+
ref="inputEl"
|
|
169
|
+
type="text"
|
|
170
|
+
inputmode="numeric"
|
|
171
|
+
:value="modelValue"
|
|
172
|
+
:disabled="disabled"
|
|
173
|
+
:required="required"
|
|
174
|
+
placeholder=" "
|
|
175
|
+
:class="inputClasses"
|
|
176
|
+
@input="onInput"
|
|
177
|
+
@keydown="onKeydown"
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
<label :for="id" :class="labelClasses">
|
|
181
|
+
{{ label }}<span v-if="required" class="text-error"> *</span>
|
|
182
|
+
</label>
|
|
183
|
+
|
|
184
|
+
<button
|
|
185
|
+
v-if="showClear"
|
|
186
|
+
type="button"
|
|
187
|
+
class="absolute right-3 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-on-surface-variant transition-colors hover:bg-on-surface/8 hover:text-on-surface"
|
|
188
|
+
:class="variant === 'filled' ? 'top-[57%] -translate-y-1/2' : 'top-[55%] -translate-y-1/2'"
|
|
189
|
+
@click="emit('update:modelValue', '')"
|
|
190
|
+
>
|
|
191
|
+
<MIcon name="close" :size="18" />
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<p v-if="error" class="px-4 text-body-small text-error">{{ error }}</p>
|
|
196
|
+
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
197
|
+
</div>
|
|
198
|
+
</template>
|