@m3ui-vue/m3ui-vue 0.2.0 → 0.2.2

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 (92) hide show
  1. package/dist/{MMenuItem-CIDblhtb.js → MMenuItem-DpoEsH91.js} +195 -114
  2. package/dist/MMenuItem-DpoEsH91.js.map +1 -0
  3. package/dist/components/MAlert.vue.d.ts +1 -0
  4. package/dist/components/MButton.vue.d.ts +1 -1
  5. package/dist/components/MCalendar.vue.d.ts +3 -0
  6. package/dist/components/MChip.vue.d.ts +3 -3
  7. package/dist/components/MCommandPalette.vue.d.ts +3 -2
  8. package/dist/components/MConfirmDialog.vue.d.ts +0 -2
  9. package/dist/components/MDataTable.vue.d.ts +8 -2
  10. package/dist/components/MDatePicker.vue.d.ts +2 -0
  11. package/dist/components/MDateRangePicker.vue.d.ts +5 -0
  12. package/dist/components/MDialog.vue.d.ts +1 -0
  13. package/dist/components/MFileUpload.vue.d.ts +15 -1
  14. package/dist/components/MInfiniteScroll.vue.d.ts +7 -5
  15. package/dist/components/MMultiSelect.vue.d.ts +13 -1
  16. package/dist/components/MRichTextEditor.vue.d.ts +28 -1
  17. package/dist/components/MScheduler.vue.d.ts +5 -0
  18. package/dist/components/MSnackbar.vue.d.ts +4 -1
  19. package/dist/components/MSpinner.vue.d.ts +1 -0
  20. package/dist/components/MSpotlightSearch.vue.d.ts +3 -2
  21. package/dist/components/MTable.vue.d.ts +2 -1
  22. package/dist/components/MTimePicker.vue.d.ts +1 -0
  23. package/dist/components/MTour.vue.d.ts +3 -0
  24. package/dist/components/MTransferList.vue.d.ts +19 -3
  25. package/dist/components/MTree.vue.d.ts +1 -1
  26. package/dist/composables/useLocale.d.ts +74 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/locales/de.d.ts +2 -0
  29. package/dist/locales/es.d.ts +2 -0
  30. package/dist/locales/fr.d.ts +2 -0
  31. package/dist/locales/index.d.ts +8 -0
  32. package/dist/locales/index.js +489 -0
  33. package/dist/locales/index.js.map +1 -0
  34. package/dist/locales/ja.d.ts +2 -0
  35. package/dist/locales/ko.d.ts +2 -0
  36. package/dist/locales/pt.d.ts +2 -0
  37. package/dist/locales/zh.d.ts +2 -0
  38. package/dist/m3ui-vue.css +1 -1
  39. package/dist/m3ui.js +2734 -2647
  40. package/dist/m3ui.js.map +1 -1
  41. package/dist/plugin.d.ts +3 -1
  42. package/dist/rich-text-editor.d.ts +1 -0
  43. package/dist/rich-text-editor.js +141 -113
  44. package/dist/rich-text-editor.js.map +1 -1
  45. package/dist/styles/palettes.css +660 -0
  46. package/dist/styles.css +1 -1
  47. package/package.json +11 -2
  48. package/src/__tests__/MAlert.test.ts +59 -0
  49. package/src/__tests__/MConfirmDialog.test.ts +51 -0
  50. package/src/__tests__/MDialog.test.ts +58 -0
  51. package/src/__tests__/MSpinner.test.ts +37 -0
  52. package/src/__tests__/localePresets.test.ts +45 -0
  53. package/src/__tests__/plugin.test.ts +27 -0
  54. package/src/__tests__/setup.ts +11 -0
  55. package/src/__tests__/useColorPalette.test.ts +71 -0
  56. package/src/__tests__/useLocale.test.ts +50 -0
  57. package/src/__tests__/useTheme.test.ts +61 -0
  58. package/src/components/MAlert.vue +5 -1
  59. package/src/components/MCalendar.vue +14 -5
  60. package/src/components/MCommandPalette.vue +11 -7
  61. package/src/components/MConfirmDialog.vue +6 -5
  62. package/src/components/MDataTable.vue +18 -9
  63. package/src/components/MDatePicker.vue +11 -4
  64. package/src/components/MDateRangePicker.vue +15 -5
  65. package/src/components/MDialog.vue +5 -1
  66. package/src/components/MFileUpload.vue +22 -10
  67. package/src/components/MInfiniteScroll.vue +14 -9
  68. package/src/components/MMultiSelect.vue +11 -7
  69. package/src/components/MRichTextEditor.vue +95 -34
  70. package/src/components/MScheduler.vue +12 -4
  71. package/src/components/MSnackbar.vue +8 -1
  72. package/src/components/MSpinner.vue +5 -1
  73. package/src/components/MSpotlightSearch.vue +11 -7
  74. package/src/components/MTable.vue +8 -4
  75. package/src/components/MTimePicker.vue +5 -1
  76. package/src/components/MTour.vue +12 -9
  77. package/src/components/MTransferList.vue +30 -15
  78. package/src/composables/useColorPalette.ts +30 -20
  79. package/src/composables/useLocale.ts +181 -0
  80. package/src/index.ts +2 -0
  81. package/src/locales/de.ts +93 -0
  82. package/src/locales/es.ts +93 -0
  83. package/src/locales/fr.ts +93 -0
  84. package/src/locales/index.ts +8 -0
  85. package/src/locales/ja.ts +93 -0
  86. package/src/locales/ko.ts +93 -0
  87. package/src/locales/pt.ts +93 -0
  88. package/src/locales/zh.ts +93 -0
  89. package/src/plugin.ts +7 -1
  90. package/src/rich-text-editor.ts +1 -0
  91. package/src/styles/palettes.css +660 -0
  92. package/dist/MMenuItem-CIDblhtb.js.map +0 -1
@@ -6,6 +6,37 @@ import MMenuItem from './MMenuItem.vue'
6
6
  import MDialog from './MDialog.vue'
7
7
  import MTextField from './MTextField.vue'
8
8
  import MButton from './MButton.vue'
9
+ import { useLocale } from '../composables/useLocale'
10
+
11
+ export interface RichTextEditorLabels {
12
+ bold?: string
13
+ italic?: string
14
+ underline?: string
15
+ strikethrough?: string
16
+ highlight?: string
17
+ bulletList?: string
18
+ orderedList?: string
19
+ blockquote?: string
20
+ code?: string
21
+ alignLeft?: string
22
+ alignCenter?: string
23
+ alignRight?: string
24
+ undo?: string
25
+ redo?: string
26
+ link?: string
27
+ image?: string
28
+ paragraph?: string
29
+ heading1?: string
30
+ heading2?: string
31
+ heading3?: string
32
+ insertLink?: string
33
+ insertImage?: string
34
+ imageUrlLabel?: string
35
+ cancel?: string
36
+ insert?: string
37
+ }
38
+
39
+ const locale = useLocale()
9
40
 
10
41
  const props = withDefaults(
11
42
  defineProps<{
@@ -13,10 +44,40 @@ const props = withDefaults(
13
44
  placeholder?: string
14
45
  disabled?: boolean
15
46
  minHeight?: string
47
+ labels?: RichTextEditorLabels
16
48
  }>(),
17
- { placeholder: 'Escribe aquí...', disabled: false, minHeight: '200px' },
49
+ { disabled: false, minHeight: '200px' },
18
50
  )
19
51
 
52
+ const l = computed<Required<RichTextEditorLabels>>(() => ({
53
+ bold: locale.bold,
54
+ italic: locale.italic,
55
+ underline: locale.underline,
56
+ strikethrough: locale.strikethrough,
57
+ highlight: locale.highlight,
58
+ bulletList: locale.bulletList,
59
+ orderedList: locale.orderedList,
60
+ blockquote: locale.blockquote,
61
+ code: locale.code,
62
+ alignLeft: locale.alignLeft,
63
+ alignCenter: locale.alignCenter,
64
+ alignRight: locale.alignRight,
65
+ undo: locale.undo,
66
+ redo: locale.redo,
67
+ link: locale.link,
68
+ image: locale.image,
69
+ paragraph: locale.paragraph,
70
+ heading1: locale.heading1,
71
+ heading2: locale.heading2,
72
+ heading3: locale.heading3,
73
+ insertLink: locale.insertLink,
74
+ insertImage: locale.insertImage,
75
+ imageUrlLabel: locale.imageUrlLabel,
76
+ cancel: locale.cancel,
77
+ insert: locale.insert,
78
+ ...props.labels,
79
+ }))
80
+
20
81
  const emit = defineEmits<{ 'update:modelValue': [string] }>()
21
82
 
22
83
  const ready = ref(false)
@@ -68,7 +129,7 @@ onMounted(async () => {
68
129
  Link.configure({ openOnClick: false }),
69
130
  Image,
70
131
  Highlight.configure({ multicolor: true }),
71
- Placeholder.configure({ placeholder: props.placeholder }),
132
+ Placeholder.configure({ placeholder: props.placeholder ?? 'Start writing...' }),
72
133
  TextStyle,
73
134
  Color,
74
135
  ],
@@ -79,26 +140,26 @@ onMounted(async () => {
79
140
 
80
141
  toolGroups.value = [
81
142
  [
82
- { icon: 'format_bold', label: 'Negrita', action: () => editor.chain().focus().toggleBold().run(), active: () => !!editor.isActive('bold') },
83
- { icon: 'format_italic', label: 'Cursiva', action: () => editor.chain().focus().toggleItalic().run(), active: () => !!editor.isActive('italic') },
84
- { icon: 'format_underlined', label: 'Subrayado', action: () => editor.chain().focus().toggleUnderline().run(), active: () => !!editor.isActive('underline') },
85
- { icon: 'format_strikethrough', label: 'Tachado', action: () => editor.chain().focus().toggleStrike().run(), active: () => !!editor.isActive('strike') },
86
- { icon: 'ink_highlighter', label: 'Resaltar', action: () => editor.chain().focus().toggleHighlight().run(), active: () => !!editor.isActive('highlight') },
143
+ { icon: 'format_bold', label: l.value.bold, action: () => editor.chain().focus().toggleBold().run(), active: () => !!editor.isActive('bold') },
144
+ { icon: 'format_italic', label: l.value.italic, action: () => editor.chain().focus().toggleItalic().run(), active: () => !!editor.isActive('italic') },
145
+ { icon: 'format_underlined', label: l.value.underline, action: () => editor.chain().focus().toggleUnderline().run(), active: () => !!editor.isActive('underline') },
146
+ { icon: 'format_strikethrough', label: l.value.strikethrough, action: () => editor.chain().focus().toggleStrike().run(), active: () => !!editor.isActive('strike') },
147
+ { icon: 'ink_highlighter', label: l.value.highlight, action: () => editor.chain().focus().toggleHighlight().run(), active: () => !!editor.isActive('highlight') },
87
148
  ],
88
149
  [
89
- { icon: 'format_list_bulleted', label: 'Lista', action: () => editor.chain().focus().toggleBulletList().run(), active: () => !!editor.isActive('bulletList') },
90
- { icon: 'format_list_numbered', label: 'Lista numerada', action: () => editor.chain().focus().toggleOrderedList().run(), active: () => !!editor.isActive('orderedList') },
91
- { icon: 'format_quote', label: 'Cita', action: () => editor.chain().focus().toggleBlockquote().run(), active: () => !!editor.isActive('blockquote') },
92
- { icon: 'code', label: 'Código', action: () => editor.chain().focus().toggleCode().run(), active: () => !!editor.isActive('code') },
150
+ { icon: 'format_list_bulleted', label: l.value.bulletList, action: () => editor.chain().focus().toggleBulletList().run(), active: () => !!editor.isActive('bulletList') },
151
+ { icon: 'format_list_numbered', label: l.value.orderedList, action: () => editor.chain().focus().toggleOrderedList().run(), active: () => !!editor.isActive('orderedList') },
152
+ { icon: 'format_quote', label: l.value.blockquote, action: () => editor.chain().focus().toggleBlockquote().run(), active: () => !!editor.isActive('blockquote') },
153
+ { icon: 'code', label: l.value.code, action: () => editor.chain().focus().toggleCode().run(), active: () => !!editor.isActive('code') },
93
154
  ],
94
155
  [
95
- { icon: 'format_align_left', label: 'Izquierda', action: () => editor.chain().focus().setTextAlign('left').run(), active: () => !!editor.isActive({ textAlign: 'left' }) },
96
- { icon: 'format_align_center', label: 'Centro', action: () => editor.chain().focus().setTextAlign('center').run(), active: () => !!editor.isActive({ textAlign: 'center' }) },
97
- { icon: 'format_align_right', label: 'Derecha', action: () => editor.chain().focus().setTextAlign('right').run(), active: () => !!editor.isActive({ textAlign: 'right' }) },
156
+ { icon: 'format_align_left', label: l.value.alignLeft, action: () => editor.chain().focus().setTextAlign('left').run(), active: () => !!editor.isActive({ textAlign: 'left' }) },
157
+ { icon: 'format_align_center', label: l.value.alignCenter, action: () => editor.chain().focus().setTextAlign('center').run(), active: () => !!editor.isActive({ textAlign: 'center' }) },
158
+ { icon: 'format_align_right', label: l.value.alignRight, action: () => editor.chain().focus().setTextAlign('right').run(), active: () => !!editor.isActive({ textAlign: 'right' }) },
98
159
  ],
99
160
  [
100
- { icon: 'undo', label: 'Deshacer', action: () => editor.chain().focus().undo().run() },
101
- { icon: 'redo', label: 'Rehacer', action: () => editor.chain().focus().redo().run() },
161
+ { icon: 'undo', label: l.value.undo, action: () => editor.chain().focus().undo().run() },
162
+ { icon: 'redo', label: l.value.redo, action: () => editor.chain().focus().redo().run() },
102
163
  ],
103
164
  ]
104
165
 
@@ -120,11 +181,11 @@ const imageUrl = ref('')
120
181
 
121
182
  const headingLabel = computed(() => {
122
183
  const e = editorRef.value
123
- if (!e) return 'Párrafo'
124
- if (e.isActive('heading', { level: 1 })) return 'Título 1'
125
- if (e.isActive('heading', { level: 2 })) return 'Título 2'
126
- if (e.isActive('heading', { level: 3 })) return 'Título 3'
127
- return 'Párrafo'
184
+ if (!e) return l.value.paragraph
185
+ if (e.isActive('heading', { level: 1 })) return l.value.heading1
186
+ if (e.isActive('heading', { level: 2 })) return l.value.heading2
187
+ if (e.isActive('heading', { level: 3 })) return l.value.heading3
188
+ return l.value.paragraph
128
189
  })
129
190
 
130
191
  function setHeading(level: 0 | 1 | 2 | 3) {
@@ -171,10 +232,10 @@ function confirmImage() {
171
232
  <MIcon name="arrow_drop_down" :size="20" />
172
233
  </button>
173
234
  </template>
174
- <MMenuItem @click="setHeading(0)">Párrafo</MMenuItem>
175
- <MMenuItem @click="setHeading(1)">Título 1</MMenuItem>
176
- <MMenuItem @click="setHeading(2)">Título 2</MMenuItem>
177
- <MMenuItem @click="setHeading(3)">Título 3</MMenuItem>
235
+ <MMenuItem @click="setHeading(0)">{{ l.paragraph }}</MMenuItem>
236
+ <MMenuItem @click="setHeading(1)">{{ l.heading1 }}</MMenuItem>
237
+ <MMenuItem @click="setHeading(2)">{{ l.heading2 }}</MMenuItem>
238
+ <MMenuItem @click="setHeading(3)">{{ l.heading3 }}</MMenuItem>
178
239
  </MMenu>
179
240
 
180
241
  <div class="mx-1 h-6 w-px bg-outline-variant" />
@@ -198,7 +259,7 @@ function confirmImage() {
198
259
 
199
260
  <button
200
261
  type="button"
201
- title="Enlace"
262
+ :title="l.link"
202
263
  class="flex h-8 w-8 cursor-pointer items-center justify-center rounded text-on-surface-variant transition-colors hover:bg-on-surface/8"
203
264
  @click="openLinkDialog"
204
265
  >
@@ -206,7 +267,7 @@ function confirmImage() {
206
267
  </button>
207
268
  <button
208
269
  type="button"
209
- title="Imagen"
270
+ :title="l.image"
210
271
  class="flex h-8 w-8 cursor-pointer items-center justify-center rounded text-on-surface-variant transition-colors hover:bg-on-surface/8"
211
272
  @click="openImageDialog"
212
273
  >
@@ -222,20 +283,20 @@ function confirmImage() {
222
283
  />
223
284
 
224
285
  <!-- Link dialog -->
225
- <MDialog v-model="linkDialogOpen" title="Insertar enlace">
286
+ <MDialog v-model="linkDialogOpen" :title="l.insertLink">
226
287
  <MTextField v-model="linkUrl" label="URL" placeholder=" " @keydown.enter="confirmLink" />
227
288
  <template #actions>
228
- <MButton variant="text" @click="linkDialogOpen = false">Cancelar</MButton>
229
- <MButton @click="confirmLink">Insertar</MButton>
289
+ <MButton variant="text" @click="linkDialogOpen = false">{{ l.cancel }}</MButton>
290
+ <MButton @click="confirmLink">{{ l.insert }}</MButton>
230
291
  </template>
231
292
  </MDialog>
232
293
 
233
294
  <!-- Image dialog -->
234
- <MDialog v-model="imageDialogOpen" title="Insertar imagen">
235
- <MTextField v-model="imageUrl" label="URL de la imagen" placeholder=" " @keydown.enter="confirmImage" />
295
+ <MDialog v-model="imageDialogOpen" :title="l.insertImage">
296
+ <MTextField v-model="imageUrl" :label="l.imageUrlLabel" placeholder=" " @keydown.enter="confirmImage" />
236
297
  <template #actions>
237
- <MButton variant="text" @click="imageDialogOpen = false">Cancelar</MButton>
238
- <MButton @click="confirmImage">Insertar</MButton>
298
+ <MButton variant="text" @click="imageDialogOpen = false">{{ l.cancel }}</MButton>
299
+ <MButton @click="confirmImage">{{ l.insert }}</MButton>
239
300
  </template>
240
301
  </MDialog>
241
302
  </div>
@@ -1,6 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed } from 'vue'
3
3
  import MIconButton from './MIconButton.vue'
4
+ import { useLocale } from '../composables/useLocale'
5
+
6
+ const locale = useLocale()
4
7
 
5
8
  export interface SchedulerEvent {
6
9
  id: string | number
@@ -17,6 +20,11 @@ const props = withDefaults(
17
20
  startHour?: number
18
21
  endHour?: number
19
22
  locale?: string
23
+ prevLabel?: string
24
+ nextLabel?: string
25
+ todayLabel?: string
26
+ dayViewLabel?: string
27
+ weekViewLabel?: string
20
28
  }>(),
21
29
  {
22
30
  events: () => [],
@@ -135,14 +143,14 @@ const eventColors: Record<string, string> = {
135
143
  <!-- Header -->
136
144
  <div class="flex items-center justify-between border-b border-outline-variant bg-surface-container px-4 py-3">
137
145
  <div class="flex items-center gap-1">
138
- <MIconButton icon="chevron_left" label="Anterior" :size="36" @click="navigate(-1)" />
139
- <MIconButton icon="chevron_right" label="Siguiente" :size="36" @click="navigate(1)" />
146
+ <MIconButton icon="chevron_left" :label="prevLabel ?? locale.previous" :size="36" @click="navigate(-1)" />
147
+ <MIconButton icon="chevron_right" :label="nextLabel ?? locale.next" :size="36" @click="navigate(1)" />
140
148
  <button
141
149
  type="button"
142
150
  class="ml-2 cursor-pointer rounded-full border border-outline px-3 py-1 text-label-medium text-on-surface transition-colors hover:bg-on-surface/8"
143
151
  @click="goToday"
144
152
  >
145
- Hoy
153
+ {{ todayLabel ?? locale.today }}
146
154
  </button>
147
155
  </div>
148
156
 
@@ -157,7 +165,7 @@ const eventColors: Record<string, string> = {
157
165
  :class="currentView === v ? 'bg-secondary-container text-on-secondary-container shadow-elevation-1' : 'text-on-surface-variant hover:bg-on-surface/8'"
158
166
  @click="currentView = v"
159
167
  >
160
- {{ v === 'day' ? 'Día' : 'Semana' }}
168
+ {{ v === 'day' ? (dayViewLabel ?? locale.dayView) : (weekViewLabel ?? locale.weekView) }}
161
169
  </button>
162
170
  </div>
163
171
  </div>
@@ -2,6 +2,13 @@
2
2
  import { computed } from "vue";
3
3
  import { useToast } from "../composables/useToast";
4
4
  import MIcon from "./MIcon.vue";
5
+ import { useLocale } from '../composables/useLocale';
6
+
7
+ const props = defineProps<{
8
+ closeAriaLabel?: string
9
+ }>()
10
+
11
+ const locale = useLocale()
5
12
 
6
13
  const { toasts, position, dismiss } = useToast();
7
14
 
@@ -117,7 +124,7 @@ const getVariantStyle = (variant: string): VariantStyle =>
117
124
  type="button"
118
125
  class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full transition-colors"
119
126
  :class="t.color ? 'text-white/60 hover:bg-white/15' : getVariantStyle(t.variant).close"
120
- aria-label="Cerrar"
127
+ :aria-label="closeAriaLabel ?? locale.close"
121
128
  @click="dismiss(t.id)"
122
129
  >
123
130
  <MIcon name="close" :size="18" />
@@ -1,14 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
+ import { useLocale } from '../composables/useLocale';
3
4
 
4
5
  const props = withDefaults(
5
6
  defineProps<{
6
7
  size?: number;
7
8
  wavy?: boolean;
9
+ label?: string;
8
10
  }>(),
9
11
  { size: 20, wavy: false },
10
12
  );
11
13
 
14
+ const locale = useLocale();
15
+
12
16
  const STROKE = 3;
13
17
  const BUMPS = 9;
14
18
 
@@ -56,7 +60,7 @@ const wavyData = computed(() => {
56
60
  class="inline-flex shrink-0 items-center justify-center"
57
61
  :style="{ width: `${size}px`, height: `${size}px` }"
58
62
  role="status"
59
- aria-label="Cargando"
63
+ :aria-label="label ?? locale.loading"
60
64
  >
61
65
  <!-- Standard circular spinner -->
62
66
  <span
@@ -2,6 +2,9 @@
2
2
  import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
3
3
  import MIcon from './MIcon.vue'
4
4
  import MSpinner from './MSpinner.vue'
5
+ import { useLocale } from '../composables/useLocale'
6
+
7
+ const locale = useLocale()
5
8
 
6
9
  export interface SpotlightResult {
7
10
  id: string | number
@@ -20,12 +23,13 @@ const props = withDefaults(
20
23
  noResultsText?: string
21
24
  hotkey?: string
22
25
  debounce?: number
26
+ navigateHint?: string
27
+ openHint?: string
28
+ closeHint?: string
23
29
  }>(),
24
30
  {
25
31
  results: () => [],
26
- placeholder: 'Buscar...',
27
32
  loading: false,
28
- noResultsText: 'No se encontraron resultados',
29
33
  hotkey: '/',
30
34
  debounce: 0,
31
35
  },
@@ -148,7 +152,7 @@ onBeforeUnmount(() => {
148
152
  ref="inputRef"
149
153
  v-model="query"
150
154
  type="text"
151
- :placeholder="placeholder"
155
+ :placeholder="placeholder ?? locale.search"
152
156
  class="h-14 flex-1 bg-transparent text-title-medium text-on-surface outline-none placeholder:text-on-surface-variant/50"
153
157
  @keydown="onKeydown"
154
158
  />
@@ -198,20 +202,20 @@ onBeforeUnmount(() => {
198
202
  </template>
199
203
  <div v-else-if="!loading" class="flex flex-col items-center gap-2 py-10">
200
204
  <MIcon name="search_off" :size="40" class="text-on-surface-variant/40" />
201
- <p class="text-body-medium text-on-surface-variant">{{ noResultsText }}</p>
205
+ <p class="text-body-medium text-on-surface-variant">{{ noResultsText ?? locale.noResults }}</p>
202
206
  </div>
203
207
  </div>
204
208
 
205
209
  <!-- Hints -->
206
210
  <div class="flex items-center gap-4 border-t border-outline-variant px-5 py-2">
207
211
  <span class="flex items-center gap-1 text-label-small text-on-surface-variant">
208
- <kbd class="rounded bg-surface-container px-1 py-0.5">↑↓</kbd> navegar
212
+ <kbd class="rounded bg-surface-container px-1 py-0.5">↑↓</kbd> {{ navigateHint ?? locale.navigateHint }}
209
213
  </span>
210
214
  <span class="flex items-center gap-1 text-label-small text-on-surface-variant">
211
- <kbd class="rounded bg-surface-container px-1 py-0.5">↵</kbd> abrir
215
+ <kbd class="rounded bg-surface-container px-1 py-0.5">↵</kbd> {{ openHint ?? locale.openHint }}
212
216
  </span>
213
217
  <span class="flex items-center gap-1 text-label-small text-on-surface-variant">
214
- <kbd class="rounded bg-surface-container px-1 py-0.5">esc</kbd> cerrar
218
+ <kbd class="rounded bg-surface-container px-1 py-0.5">esc</kbd> {{ closeHint ?? locale.closeHint }}
215
219
  </span>
216
220
  </div>
217
221
  </div>
@@ -3,6 +3,9 @@ import { computed, onMounted, ref, watch } from 'vue'
3
3
  import MCheckbox from './MCheckbox.vue'
4
4
  import MIcon from './MIcon.vue'
5
5
  import MPagination from './MPagination.vue'
6
+ import { useLocale } from '../composables/useLocale'
7
+
8
+ const locale = useLocale()
6
9
 
7
10
  export interface TableColumn {
8
11
  key: string
@@ -38,10 +41,11 @@ const props = withDefaults(
38
41
  serverSide?: boolean
39
42
  total?: number
40
43
  page?: number
44
+ searchPlaceholder?: string
45
+ selectedText?: string
41
46
  }>(),
42
47
  {
43
48
  loading: false,
44
- emptyText: 'Sin resultados',
45
49
  rowKey: 'id',
46
50
  selectable: false,
47
51
  modelValue: () => [],
@@ -228,7 +232,7 @@ const hasActions = computed(() => !!slots['row-actions'])
228
232
  <input
229
233
  v-model="search"
230
234
  type="text"
231
- placeholder="Buscar..."
235
+ :placeholder="searchPlaceholder ?? locale.search"
232
236
  class="w-full bg-transparent text-body-medium text-on-surface outline-none placeholder:text-on-surface-variant"
233
237
  />
234
238
  <button
@@ -254,7 +258,7 @@ const hasActions = computed(() => !!slots['row-actions'])
254
258
  v-if="selectable && selected.length > 0"
255
259
  class="rounded-full bg-primary/12 px-3 py-1 text-label-small font-medium text-primary"
256
260
  >
257
- {{ selected.length }} seleccionado{{ selected.length !== 1 ? 's' : '' }}
261
+ {{ selected.length }} {{ selectedText ?? locale.selectedCount }}{{ selected.length !== 1 ? 's' : '' }}
258
262
  </span>
259
263
  </Transition>
260
264
  </div>
@@ -346,7 +350,7 @@ const hasActions = computed(() => !!slots['row-actions'])
346
350
  >
347
351
  <slot name="empty">
348
352
  <MIcon name="search_off" :size="36" class="mb-2 text-on-surface-variant opacity-30" />
349
- <p class="text-body-medium text-on-surface-variant">{{ emptyText }}</p>
353
+ <p class="text-body-medium text-on-surface-variant">{{ emptyText ?? locale.noResults }}</p>
350
354
  </slot>
351
355
  </td>
352
356
  </tr>
@@ -2,10 +2,12 @@
2
2
  import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
3
3
  import MIcon from './MIcon.vue'
4
4
  import { useFieldBg } from '../composables/useFieldBg'
5
+ import { useLocale } from '../composables/useLocale'
5
6
 
6
7
  const props = withDefaults(defineProps<{
7
8
  modelValue: string | null
8
9
  label?: string
10
+ placeholder?: string
9
11
  disabled?: boolean
10
12
  error?: string
11
13
  hint?: string
@@ -14,6 +16,8 @@ const props = withDefaults(defineProps<{
14
16
  fieldBg?: string
15
17
  }>(), { minuteStep: 5, use24h: true })
16
18
 
19
+ const locale = useLocale()
20
+
17
21
  const emit = defineEmits<{ 'update:modelValue': [string | null] }>()
18
22
 
19
23
  const open = ref(false)
@@ -129,7 +133,7 @@ onUnmounted(() => {
129
133
  >
130
134
  <MIcon name="schedule" :size="20" class="shrink-0 text-on-surface-variant" />
131
135
  <span v-if="displayValue" class="flex-1 font-mono text-on-surface">{{ displayValue }}</span>
132
- <span v-else class="flex-1 text-on-surface-variant">{{ label || 'Seleccionar hora' }}</span>
136
+ <span v-else class="flex-1 text-on-surface-variant">{{ label || placeholder || locale.selectTime }}</span>
133
137
  <MIcon
134
138
  v-if="modelValue"
135
139
  name="close"
@@ -2,6 +2,7 @@
2
2
  import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
3
3
  import MButton from './MButton.vue'
4
4
  import MIcon from './MIcon.vue'
5
+ import { useLocale } from '../composables/useLocale'
5
6
 
6
7
  export interface TourStep {
7
8
  target: string
@@ -10,13 +11,15 @@ export interface TourStep {
10
11
  placement?: 'top' | 'bottom' | 'left' | 'right'
11
12
  }
12
13
 
13
- const props = withDefaults(
14
- defineProps<{
15
- modelValue: boolean
16
- steps: TourStep[]
17
- }>(),
18
- {},
19
- )
14
+ const props = defineProps<{
15
+ modelValue: boolean
16
+ steps: TourStep[]
17
+ prevLabel?: string
18
+ nextLabel?: string
19
+ finishLabel?: string
20
+ }>()
21
+
22
+ const locale = useLocale()
20
23
 
21
24
  const emit = defineEmits<{
22
25
  'update:modelValue': [boolean]
@@ -237,11 +240,11 @@ onBeforeUnmount(() => { stopListeners(); clearHighlight() })
237
240
  variant="text"
238
241
  @click="goPrev"
239
242
  >
240
- Anterior
243
+ {{ prevLabel ?? locale.previous }}
241
244
  </MButton>
242
245
  <span v-else />
243
246
  <MButton @click="goNext">
244
- {{ isLast ? 'Finalizar' : 'Siguiente' }}
247
+ {{ isLast ? (finishLabel ?? locale.finish) : (nextLabel ?? locale.next) }}
245
248
  </MButton>
246
249
  </div>
247
250
  </div>
@@ -3,6 +3,9 @@ import { computed, ref } from 'vue'
3
3
  import MIcon from './MIcon.vue'
4
4
  import MIconButton from './MIconButton.vue'
5
5
  import MCheckbox from './MCheckbox.vue'
6
+ import { useLocale } from '../composables/useLocale'
7
+
8
+ const locale = useLocale()
6
9
 
7
10
  export interface TransferItem {
8
11
  value: string | number
@@ -16,7 +19,15 @@ const props = withDefaults(defineProps<{
16
19
  sourceTitle?: string
17
20
  targetTitle?: string
18
21
  filterable?: boolean
19
- }>(), { sourceTitle: 'Disponibles', targetTitle: 'Seleccionados', filterable: false })
22
+ searchPlaceholder?: string
23
+ emptyText?: string
24
+ moveAllRightLabel?: string
25
+ moveRightLabel?: string
26
+ moveLeftLabel?: string
27
+ moveAllLeftLabel?: string
28
+ }>(), {
29
+ filterable: false,
30
+ })
20
31
 
21
32
  const emit = defineEmits<{ 'update:modelValue': [(string | number)[]] }>()
22
33
 
@@ -83,14 +94,14 @@ function moveAllLeft() {
83
94
  <!-- Source list -->
84
95
  <div class="flex min-w-0 flex-1 flex-col overflow-hidden rounded-lg border border-outline-variant">
85
96
  <div class="flex items-center justify-between border-b border-outline-variant bg-surface-container px-3 py-2">
86
- <span class="text-label-large font-medium text-on-surface">{{ sourceTitle }}</span>
97
+ <span class="text-label-large font-medium text-on-surface">{{ sourceTitle ?? locale.available }}</span>
87
98
  <span class="text-label-small text-on-surface-variant">{{ sourceItems.length }}</span>
88
99
  </div>
89
100
  <div v-if="filterable" class="border-b border-outline-variant px-3 py-2">
90
101
  <input
91
102
  v-model="sourceSearch"
92
103
  type="text"
93
- placeholder="Buscar..."
104
+ :placeholder="searchPlaceholder ?? locale.search"
94
105
  class="w-full bg-transparent text-body-medium text-on-surface outline-none placeholder:text-on-surface-variant/50"
95
106
  />
96
107
  </div>
@@ -106,9 +117,11 @@ function moveAllLeft() {
106
117
  <MIcon v-if="item.icon" :name="item.icon" :size="18" class="shrink-0 text-on-surface-variant" />
107
118
  <span class="flex-1 truncate text-body-medium text-on-surface">{{ item.label }}</span>
108
119
  </button>
109
- <p v-if="!sourceItems.length" class="px-3 py-4 text-center text-body-small text-on-surface-variant">
110
- Sin elementos
111
- </p>
120
+ <slot v-if="!sourceItems.length" name="source-empty">
121
+ <p class="px-3 py-4 text-center text-body-small text-on-surface-variant">
122
+ {{ emptyText ?? locale.noItems }}
123
+ </p>
124
+ </slot>
112
125
  </div>
113
126
  </div>
114
127
 
@@ -116,14 +129,14 @@ function moveAllLeft() {
116
129
  <div class="flex flex-col items-center justify-center gap-1">
117
130
  <MIconButton
118
131
  icon="keyboard_double_arrow_right"
119
- label="Mover todos a la derecha"
132
+ :label="moveAllRightLabel ?? locale.moveAllRight"
120
133
  :size="36"
121
134
  :disabled="!sourceItems.length"
122
135
  @click="moveAllRight"
123
136
  />
124
137
  <MIconButton
125
138
  icon="chevron_right"
126
- label="Mover seleccionados a la derecha"
139
+ :label="moveRightLabel ?? locale.moveRight"
127
140
  variant="tonal"
128
141
  :size="36"
129
142
  :disabled="!checkedSource.size"
@@ -131,7 +144,7 @@ function moveAllLeft() {
131
144
  />
132
145
  <MIconButton
133
146
  icon="chevron_left"
134
- label="Mover seleccionados a la izquierda"
147
+ :label="moveLeftLabel ?? locale.moveLeft"
135
148
  variant="tonal"
136
149
  :size="36"
137
150
  :disabled="!checkedTarget.size"
@@ -139,7 +152,7 @@ function moveAllLeft() {
139
152
  />
140
153
  <MIconButton
141
154
  icon="keyboard_double_arrow_left"
142
- label="Mover todos a la izquierda"
155
+ :label="moveAllLeftLabel ?? locale.moveAllLeft"
143
156
  :size="36"
144
157
  :disabled="!targetItems.length"
145
158
  @click="moveAllLeft"
@@ -149,14 +162,14 @@ function moveAllLeft() {
149
162
  <!-- Target list -->
150
163
  <div class="flex min-w-0 flex-1 flex-col overflow-hidden rounded-lg border border-outline-variant">
151
164
  <div class="flex items-center justify-between border-b border-outline-variant bg-surface-container px-3 py-2">
152
- <span class="text-label-large font-medium text-on-surface">{{ targetTitle }}</span>
165
+ <span class="text-label-large font-medium text-on-surface">{{ targetTitle ?? locale.selected }}</span>
153
166
  <span class="text-label-small text-on-surface-variant">{{ targetItems.length }}</span>
154
167
  </div>
155
168
  <div v-if="filterable" class="border-b border-outline-variant px-3 py-2">
156
169
  <input
157
170
  v-model="targetSearch"
158
171
  type="text"
159
- placeholder="Buscar..."
172
+ :placeholder="searchPlaceholder ?? locale.search"
160
173
  class="w-full bg-transparent text-body-medium text-on-surface outline-none placeholder:text-on-surface-variant/50"
161
174
  />
162
175
  </div>
@@ -172,9 +185,11 @@ function moveAllLeft() {
172
185
  <MIcon v-if="item.icon" :name="item.icon" :size="18" class="shrink-0 text-on-surface-variant" />
173
186
  <span class="flex-1 truncate text-body-medium text-on-surface">{{ item.label }}</span>
174
187
  </button>
175
- <p v-if="!targetItems.length" class="px-3 py-4 text-center text-body-small text-on-surface-variant">
176
- Sin elementos
177
- </p>
188
+ <slot v-if="!targetItems.length" name="target-empty">
189
+ <p class="px-3 py-4 text-center text-body-small text-on-surface-variant">
190
+ {{ emptyText ?? locale.noItems }}
191
+ </p>
192
+ </slot>
178
193
  </div>
179
194
  </div>
180
195
  </div>