@meistrari/tela-build 1.70.1 → 1.70.3

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.
@@ -9,7 +9,7 @@ withDefaults(defineProps<{
9
9
  </script>
10
10
 
11
11
  <template>
12
- <TelaMenubarContent :side="side" :side-offset="sideOffset" class="!min-w-244px">
12
+ <TelaMenubarContent :side="side" :side-offset="sideOffset" class="!min-w-244px z-700!">
13
13
  <slot />
14
14
  </TelaMenubarContent>
15
15
  </template>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <TelaMenubarSubContent class="!min-w-180px">
2
+ <TelaMenubarSubContent class="!min-w-180px z-700!">
3
3
  <slot />
4
4
  </TelaMenubarSubContent>
5
5
  </template>
@@ -1,7 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { TreeItem } from 'radix-vue'
3
3
  import { isToday } from 'date-fns'
4
- import { useI18n } from 'vue-i18n'
5
4
  import type { User } from '../types'
6
5
 
7
6
  interface ChatMessageProps {
@@ -22,9 +21,16 @@ interface ChatMessageProps {
22
21
  currentUser?: User
23
22
  otherUsers?: User[]
24
23
  isExpanded?: boolean
24
+ locale?: string
25
+ todayAtLabel?: (time: string) => string
26
+ onDateLabel?: (date: string) => string
25
27
  }
26
28
 
27
- const props = defineProps<ChatMessageProps>()
29
+ const props = withDefaults(defineProps<ChatMessageProps>(), {
30
+ locale: 'en-US',
31
+ todayAtLabel: (time: string) => `Today at ${time}`,
32
+ onDateLabel: (date: string) => `On ${date}`,
33
+ })
28
34
  const emit = defineEmits<{
29
35
  reply: []
30
36
  delete: [id: string]
@@ -90,7 +96,6 @@ async function calculateTotalHeight(message: ChatMessageProps): Promise<number>
90
96
  }
91
97
 
92
98
  const hasReplies = computed(() => props.replies && props.replies.length > 0)
93
- const { t, locale } = useI18n()
94
99
 
95
100
  const timestampString = computed(() => {
96
101
  if (!props.timestamp)
@@ -98,9 +103,9 @@ const timestampString = computed(() => {
98
103
 
99
104
  const date = new Date(props.timestamp)
100
105
  if (isToday(date))
101
- return t('chat.todayAt', { time: date.toLocaleTimeString(locale.value ?? 'en-US', { hour: '2-digit', minute: '2-digit', hour12: false }) })
106
+ return props.todayAtLabel(date.toLocaleTimeString(props.locale, { hour: '2-digit', minute: '2-digit', hour12: false }))
102
107
 
103
- return t('chat.onDate', { date: date.toLocaleDateString(locale.value ?? 'en-US', { day: '2-digit', month: 'short', year: 'numeric' }) })
108
+ return props.onDateLabel(date.toLocaleDateString(props.locale, { day: '2-digit', month: 'short', year: 'numeric' }))
104
109
  })
105
110
 
106
111
  const beforeWidth = computed(() => {
@@ -1,7 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, watch } from 'vue'
3
3
  import { useMagicKeys } from '@vueuse/core'
4
- import { useI18n } from 'vue-i18n'
5
4
 
6
5
  import { ComboboxRoot as Combobox, ComboboxTrigger } from 'reka-ui'
7
6
  import type { AllActionsInfo } from 'tela-workflow/src/compat-v1-schemas'
@@ -40,6 +39,19 @@ const props = withDefaults(defineProps<{
40
39
  inline?: boolean
41
40
  to?: string | HTMLElement
42
41
  hideTrigger?: boolean
42
+ labelEverything?: string
43
+ labelModules?: string
44
+ labelResources?: string
45
+ labelActions?: string
46
+ labelTemplates?: string
47
+ labelSearchPlaceholder?: string
48
+ labelNoResults?: string
49
+ labelNoResultsWithSearch?: (search: string) => string
50
+ labelPress?: string
51
+ labelToClear?: string
52
+ labelClear?: string
53
+ labelComingSoon?: string
54
+ getTabLabel?: (tab: string) => string
43
55
  }>(), {
44
56
  closeOnOutsideClick: true,
45
57
  closeOnEscape: true,
@@ -47,6 +59,18 @@ const props = withDefaults(defineProps<{
47
59
  priority: 'normal',
48
60
  inline: false,
49
61
  hideTrigger: false,
62
+ labelEverything: 'Everything',
63
+ labelModules: 'Modules',
64
+ labelResources: 'Resources',
65
+ labelActions: 'Actions',
66
+ labelTemplates: 'Templates',
67
+ labelSearchPlaceholder: 'Search',
68
+ labelNoResults: 'No items found',
69
+ labelNoResultsWithSearch: (search: string) => `No items found with '${search}'`,
70
+ labelPress: 'Press',
71
+ labelToClear: 'to clear',
72
+ labelClear: 'Clear',
73
+ labelComingSoon: 'Coming soon',
50
74
  })
51
75
 
52
76
  const emit = defineEmits<{
@@ -55,8 +79,6 @@ const emit = defineEmits<{
55
79
  'selectWorkflowModule': [value: string]
56
80
  }>()
57
81
 
58
- const { t } = useI18n()
59
-
60
82
  const open = defineModel<boolean>('open')
61
83
 
62
84
  const selectedTab = ref('everything')
@@ -86,12 +108,27 @@ const effectiveOptions = computed((): CombinedOption[] => {
86
108
  }))
87
109
  })
88
110
 
111
+ function getLabelForTab(tab: string): string {
112
+ if (props.getTabLabel)
113
+ return props.getTabLabel(tab)
114
+
115
+ const knownLabels: Record<string, string> = {
116
+ everything: props.labelEverything,
117
+ modules: props.labelModules,
118
+ resources: props.labelResources,
119
+ actions: props.labelActions,
120
+ templates: props.labelTemplates,
121
+ }
122
+
123
+ return knownLabels[tab] ?? tab.charAt(0).toUpperCase() + tab.slice(1)
124
+ }
125
+
89
126
  const tabs = computed(() => {
90
127
  const allTabs = effectiveOptions.value.reduce((acc, option) => {
91
128
  if (option.tabs) {
92
129
  option.tabs.forEach((tab) => {
93
130
  if (!acc.find(t => t.tab === tab)) {
94
- acc.push({ tab, label: tab.charAt(0).toUpperCase() + tab.slice(1) })
131
+ acc.push({ tab, label: getLabelForTab(tab) })
95
132
  }
96
133
  })
97
134
  }
@@ -101,7 +138,7 @@ const tabs = computed(() => {
101
138
  const hasAll = allTabs.find(t => t.tab === 'everything')
102
139
 
103
140
  if (!hasAll) {
104
- allTabs.unshift({ tab: 'everything', label: t('workflow.modules.everything') })
141
+ allTabs.unshift({ tab: 'everything', label: props.labelEverything })
105
142
  }
106
143
 
107
144
  return allTabs
@@ -136,28 +173,28 @@ const groupedOptions = computed(() => {
136
173
 
137
174
  filteredOptions.forEach((option) => {
138
175
  if (option.tabs?.includes('modules')) {
139
- const modulesLabel = t('workflow.modules.modules')
176
+ const modulesLabel = props.labelModules
140
177
  if (!categoryGroups.modules) {
141
178
  categoryGroups.modules = { label: modulesLabel, category: 'Modules', children: [] }
142
179
  }
143
180
  categoryGroups.modules.children.push(option)
144
181
  }
145
182
  if (option.tabs?.includes('resources')) {
146
- const resourcesLabel = t('workflow.modules.resources')
183
+ const resourcesLabel = props.labelResources
147
184
  if (!categoryGroups.resources) {
148
185
  categoryGroups.resources = { label: resourcesLabel, category: 'Resources', children: [] }
149
186
  }
150
187
  categoryGroups.resources.children.push(option)
151
188
  }
152
189
  if (option.tabs?.includes('actions')) {
153
- const actionsLabel = t('workflow.modules.actions')
190
+ const actionsLabel = props.labelActions
154
191
  if (!categoryGroups.actions) {
155
192
  categoryGroups.actions = { label: actionsLabel, category: 'Actions', children: [] }
156
193
  }
157
194
  categoryGroups.actions.children.push(option)
158
195
  }
159
196
  if (option.tabs?.includes('templates')) {
160
- const templatesLabel = t('workflow.modules.templates')
197
+ const templatesLabel = props.labelTemplates
161
198
  if (!categoryGroups.templates) {
162
199
  categoryGroups.templates = { label: templatesLabel, category: 'Templates', children: [] }
163
200
  }
@@ -175,7 +212,7 @@ const groupedOptions = computed(() => {
175
212
  }
176
213
 
177
214
  return [{
178
- heading: selectedTab.value.charAt(0).toUpperCase() + selectedTab.value.slice(1),
215
+ heading: getLabelForTab(selectedTab.value),
179
216
  category: selectedTab.value.charAt(0).toUpperCase() + selectedTab.value.slice(1),
180
217
  children: filteredOptions,
181
218
  }].filter(group => group.children.length > 0)
@@ -265,7 +302,7 @@ watch(innerValue, (value) => {
265
302
  >
266
303
  <ComboboxInput
267
304
  v-model="search"
268
- :placeholder="t('models.labels.search')"
305
+ :placeholder="labelSearchPlaceholder"
269
306
  />
270
307
  <Tabs v-model="selectedTab" :default-value="selectedTab">
271
308
  <TabsList class="sticky z-10 top-0 shrink-0 flex gap-2 w-full">
@@ -282,10 +319,10 @@ watch(innerValue, (value) => {
282
319
  <div class="flex flex-col items-center justify-center gap-3">
283
320
  <TelaIcon name="i-ph-smiley-sad" size="24px" class="text-gray-400" />
284
321
  <span class="text-xl font-medium leading-none">
285
- {{ t('common.noItemsFound') }}{{ search.trim() ? ` with '${search}'` : '' }}
322
+ {{ search.trim() ? labelNoResultsWithSearch(search.trim()) : labelNoResults }}
286
323
  </span>
287
324
  <span v-if="search.trim()" class="inline-flex gap-1.5 text-md text-gray-500">
288
- {{ t('common.press') }}
325
+ {{ labelPress }}
289
326
  <div class="flex items-center gap-1">
290
327
  <div class="p-2px bg-gray-200 rounded">
291
328
  <TelaIcon name="i-ph-command" size="16px" />
@@ -294,14 +331,14 @@ watch(innerValue, (value) => {
294
331
  <TelaIcon name="i-ph-backspace" size="16px" />
295
332
  </div>
296
333
  </div>
297
- {{ t('common.toClear') }}
334
+ {{ labelToClear }}
298
335
  </span>
299
336
  </div>
300
337
  <TelaButton
301
338
  v-if="search.trim()" variant="secondary" size="sm" class="rounded-lg"
302
339
  @click="handleDeleteSearchValue"
303
340
  >
304
- {{ t('common.clear') }}
341
+ {{ labelClear }}
305
342
  </TelaButton>
306
343
  </ComboboxEmpty>
307
344
  <template v-for="group in groupedOptions" :key="group.heading">
@@ -330,7 +367,7 @@ watch(innerValue, (value) => {
330
367
  <span class="font-medium truncate w-full">{{ item.label }}</span>
331
368
  <span
332
369
  class="font-semibold uppercase text-9px tracking-wider text-gray-700 bg-gray-100 rounded leading-none py-2px px-1"
333
- >{{ t('common.comingSoon') }}</span>
370
+ >{{ labelComingSoon }}</span>
334
371
  </div>
335
372
  <span v-else class="font-medium truncate w-full">{{ item.label }}</span>
336
373
  <span v-if="item.description" class="font-normal text-sm leading-none text-gray-500">
@@ -298,6 +298,10 @@ Example with explicit labels:
298
298
  />
299
299
  ```
300
300
 
301
+ ### Module Selector Labels
302
+
303
+ `TelaComboboxModuleSelector` follows the same host-owned translation boundary. Its visible copy is configurable through `labelEverything`, `labelModules`, `labelResources`, `labelActions`, `labelTemplates`, `labelSearchPlaceholder`, `labelNoResults`, `labelNoResultsWithSearch`, `labelPress`, `labelToClear`, `labelClear`, `labelComingSoon`, and `getTabLabel`. Omitted values use English defaults.
304
+
301
305
  ## Slots
302
306
 
303
307
  - `tags` - Custom tags/badges for each option (receives `option` as slot prop)
@@ -5,7 +5,7 @@ import TelaTableCell from './complex-table-cell.vue'
5
5
  import { isHorizontalSpacerColumn } from './composables/horizontal-virtual-columns'
6
6
  import { isLoading, hasContent, hasError, getRowTitle, readRowPath } from './utils'
7
7
 
8
- const props = defineProps<{
8
+ const props = withDefaults(defineProps<{
9
9
  row: Row
10
10
  rowIndex: number
11
11
  columns: Column[]
@@ -25,10 +25,13 @@ const props = defineProps<{
25
25
  isLastRow?: boolean
26
26
  totalRows?: number
27
27
  hideError?: boolean
28
+ errorLabel?: string
28
29
  selectClass?: string
29
30
  showBorder?: boolean
30
31
  borderMode?: 'default' | 'visible'
31
- }>()
32
+ }>(), {
33
+ errorLabel: 'Error',
34
+ })
32
35
 
33
36
  const emit = defineEmits<{
34
37
  select: [id: string, shiftKey: boolean]
@@ -153,7 +156,7 @@ function handleCheckboxChange() {
153
156
  <td v-if="hasError(row)" :colspan="columns.length">
154
157
  <div v-if="!hideError" flex="~ row" items-center py-12px pl-12px h-64px b="gray-200" :class="rowClass">
155
158
  <span z-1 body-14-medium class="text-[#BE2741]">
156
- {{ $t('common.error') }}
159
+ {{ errorLabel }}
157
160
  </span>
158
161
  <span v-if="row.errorMessage" line-clamp-2 body-14-medium class="text-[#BE2741]">
159
162
  : {{ row.errorMessage }}
@@ -360,6 +360,7 @@ watch([hasRowIndex, hasSelect, mainTableEl], () => {
360
360
  :is-first-row="idx === 0"
361
361
  :is-last-row="idx === rows.length - 1"
362
362
  :hide-error="props.hideError"
363
+ :error-label="props.errorLabel"
363
364
  :total-rows="rows.length"
364
365
  :show-border="isVisible"
365
366
  :border-mode="virtualScroll ? 'visible' : 'default'"
@@ -412,6 +412,7 @@ type ComplexTableProps = {
412
412
  headerClass?: string
413
413
  hideScrollbar?: boolean
414
414
  hideError?: boolean
415
+ errorLabel?: string // default: 'Error'; pass a translated label from the host app
415
416
  useVirtualization?: boolean
416
417
  virtualizedOverscan?: number
417
418
  virtualizedRowHeight?: number
@@ -259,6 +259,7 @@ watch([hasRowIndex, hasSelect, mainTableEl], () => {
259
259
  :is-first-row="idx === 0"
260
260
  :is-last-row="idx === rows.length - 1"
261
261
  :hide-error="props.hideError"
262
+ :error-label="props.errorLabel"
262
263
  :total-rows="rows.length"
263
264
  @select="handleRowSelect"
264
265
  @open="(id: string) => emit('open', id)"
@@ -60,6 +60,7 @@ export type ComplexTableType = {
60
60
  hideScrollbar?: boolean
61
61
  showVerticalScrollbar?: boolean
62
62
  hideError?: boolean
63
+ errorLabel?: string
63
64
  useVirtualization?: boolean
64
65
  virtualizedOverscan?: number
65
66
  virtualizedRowHeight?: number
@@ -127,14 +127,32 @@ const removeFile = (index) => {
127
127
  <ArgTypes />
128
128
 
129
129
  ```typescript
130
+ type FileUploadLabels = {
131
+ dragAndDrop?: string
132
+ orBrowse?: string
133
+ supportedFormats?: string
134
+ and?: string
135
+ more?: string
136
+ maxSize?: (maxSize: number) => string
137
+ filesCount?: (current: number, max: number) => string
138
+ invalidFormat?: (formats: string) => string
139
+ fileTooLarge?: (maxSize: number) => string
140
+ tooManyFiles?: (maxFiles: number) => string
141
+ }
142
+
130
143
  type FileUploadProps = {
131
- accept?: string
144
+ class?: string
132
145
  multiple?: boolean
133
- disabled?: boolean
134
- maxSize?: number
146
+ maxFiles?: number
147
+ maxFileSize?: number
148
+ acceptedFormats?: string[]
149
+ acceptedExtensions?: string[]
150
+ labels?: FileUploadLabels
135
151
  }
136
152
  ```
137
153
 
154
+ Pass translated copy and validation messages through `labels`. English defaults are used when labels are omitted; no i18n plugin is required.
155
+
138
156
  ## Events
139
157
 
140
158
  - `change` - Emitted when files are selected with FileList
@@ -1,6 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import { ref } from 'vue'
3
- import { useI18n } from 'vue-i18n'
3
+
4
+ interface FileUploadLabels {
5
+ dragAndDrop?: string
6
+ orBrowse?: string
7
+ supportedFormats?: string
8
+ and?: string
9
+ more?: string
10
+ maxSize?: (maxSize: number) => string
11
+ filesCount?: (current: number, max: number) => string
12
+ invalidFormat?: (formats: string) => string
13
+ fileTooLarge?: (maxSize: number) => string
14
+ tooManyFiles?: (maxFiles: number) => string
15
+ }
4
16
 
5
17
  const props = withDefaults(defineProps<{
6
18
  class?: string
@@ -9,19 +21,32 @@ const props = withDefaults(defineProps<{
9
21
  maxFileSize?: number // in MB
10
22
  acceptedFormats?: string[] // e.g., ['image/png', 'image/jpeg', 'application/pdf']
11
23
  acceptedExtensions?: string[] // e.g., ['.png', '.jpg', '.pdf'] - for display
24
+ labels?: FileUploadLabels
12
25
  }>(), {
13
26
  multiple: true,
14
27
  maxFiles: 5,
15
28
  maxFileSize: 10, // 10MB default
16
29
  acceptedFormats: () => [],
17
30
  acceptedExtensions: () => [],
31
+ labels: () => ({}),
18
32
  })
19
33
 
20
34
  const emit = defineEmits<{
21
35
  error: [message: string]
22
36
  }>()
23
37
 
24
- const { t } = useI18n()
38
+ const labels = computed(() => ({
39
+ dragAndDrop: props.labels.dragAndDrop ?? 'Drag and drop files here',
40
+ orBrowse: props.labels.orBrowse ?? 'or browse',
41
+ supportedFormats: props.labels.supportedFormats ?? 'Supported formats',
42
+ and: props.labels.and ?? 'and',
43
+ more: props.labels.more ?? 'more',
44
+ maxSize: props.labels.maxSize ?? ((maxSize: number) => `Max ${maxSize}MB`),
45
+ filesCount: props.labels.filesCount ?? ((current: number, max: number) => `${current}/${max} files`),
46
+ invalidFormat: props.labels.invalidFormat ?? ((formats: string) => `Invalid file format. Accepted formats: ${formats}`),
47
+ fileTooLarge: props.labels.fileTooLarge ?? ((maxSize: number) => `Some files exceed the ${maxSize}MB size limit`),
48
+ tooManyFiles: props.labels.tooManyFiles ?? ((maxFiles: number) => `Maximum ${maxFiles} files allowed`),
49
+ }))
25
50
  const files = defineModel<File[]>({ required: true })
26
51
  const isDragging = ref(false)
27
52
 
@@ -48,7 +73,7 @@ function validateFiles(newFiles: FileList | File[]): File[] {
48
73
  const formatsDisplay = props.acceptedExtensions.length > 0
49
74
  ? props.acceptedExtensions.join(', ').toUpperCase()
50
75
  : props.acceptedFormats.join(', ')
51
- emit('error', t('common.fileUpload.errors.invalidFormat', { formats: formatsDisplay }))
76
+ emit('error', labels.value.invalidFormat(formatsDisplay))
52
77
  return []
53
78
  }
54
79
  }
@@ -56,14 +81,14 @@ function validateFiles(newFiles: FileList | File[]): File[] {
56
81
  // Check file sizes
57
82
  const oversizedFiles = fileArray.filter(file => file.size > maxSizeBytes)
58
83
  if (oversizedFiles.length > 0) {
59
- emit('error', t('common.fileUpload.errors.fileTooLarge', { maxSize: props.maxFileSize }))
84
+ emit('error', labels.value.fileTooLarge(props.maxFileSize))
60
85
  return []
61
86
  }
62
87
 
63
88
  // Check total file count
64
89
  const totalFiles = files.value.length + fileArray.length
65
90
  if (totalFiles > props.maxFiles) {
66
- emit('error', t('common.fileUpload.errors.tooManyFiles', { maxFiles: props.maxFiles }))
91
+ emit('error', labels.value.tooManyFiles(props.maxFiles))
67
92
  return []
68
93
  }
69
94
 
@@ -169,15 +194,15 @@ const fileTypesDisplay = computed(() => {
169
194
  <TelaIcon name="i-ph-upload-simple-bold" size="16px" color="gray-400" />
170
195
  <div class="flex flex-col items-center justify-center gap-1">
171
196
  <h5 heading-h5-semibold text-primary>
172
- {{ $t('common.fileUpload.dragAndDrop') }} <TelaLinkDecoration class="body-14-regular ml-1px text-gray-700">{{ $t('common.fileUpload.orBrowse') }}</TelaLinkDecoration>
197
+ {{ labels.dragAndDrop }} <TelaLinkDecoration class="body-14-regular ml-1px text-gray-700">{{ labels.orBrowse }}</TelaLinkDecoration>
173
198
  </h5>
174
199
  <div flex="~ col" gap-1px items-center>
175
200
  <p flex items-center gap-4px body-12-regular text-tertiary>
176
- {{ $t('common.fileUpload.supportedFormats') }}: {{ fileTypesDisplay.text }}
201
+ {{ labels.supportedFormats }}: {{ fileTypesDisplay.text }}
177
202
  <template v-if="fileTypesDisplay.hasMore">
178
- {{ t('common.and') }}
203
+ {{ labels.and }}
179
204
  <TelaLinkDecoration class="text-inherit underline-offset-2px">
180
- {{ fileTypesDisplay.moreCount }} {{ t('common.more') }}
205
+ {{ fileTypesDisplay.moreCount }} {{ labels.more }}
181
206
  </TelaLinkDecoration>
182
207
  <TelaTooltip variant="multiline" content-class="w-fit min-w-100px max-w-100px" :description="fileTypesDisplay.moreTooltip" side="right">
183
208
  <TelaIcon name="i-ph-info-bold" size="14px" color="gray-400" />
@@ -185,7 +210,7 @@ const fileTypesDisplay = computed(() => {
185
210
  </template>
186
211
  </p>
187
212
  <p body-12-regular text-tertiary>
188
- {{ t('common.fileUpload.maxSize', { maxSize: maxFileSize }) }} · {{ t('common.fileUpload.filesCount', { current: files.length, max: maxFiles }) }}
213
+ {{ labels.maxSize(maxFileSize) }} · {{ labels.filesCount(files.length, maxFiles) }}
189
214
  </p>
190
215
  </div>
191
216
  </div>
@@ -0,0 +1,48 @@
1
+ import { readdirSync, readFileSync } from 'node:fs'
2
+ import { dirname, extname, join, relative, resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { describe, expect, it } from 'vitest'
5
+
6
+ const componentsRoot = dirname(fileURLToPath(import.meta.url))
7
+ const buildRoot = resolve(componentsRoot, '../..')
8
+ const excludedDirectories = new Set(['.nuxt', '.output', '__tests__', 'coverage', 'node_modules'])
9
+
10
+ function runtimeSourceFiles(directory: string): string[] {
11
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
12
+ const path = join(directory, entry.name)
13
+
14
+ if (entry.isDirectory())
15
+ return excludedDirectories.has(entry.name) ? [] : runtimeSourceFiles(path)
16
+
17
+ if (entry.name.endsWith('.test.ts') || entry.name.endsWith('.spec.ts'))
18
+ return []
19
+
20
+ return ['.ts', '.vue'].includes(extname(entry.name)) ? [path] : []
21
+ })
22
+ }
23
+
24
+ describe('tela Build translation boundary', () => {
25
+ it('keeps components independent from a host i18n plugin', () => {
26
+ const violations = runtimeSourceFiles(buildRoot).flatMap((path) => {
27
+ const source = readFileSync(path, 'utf8')
28
+ const matches = [
29
+ /from\s+['"]vue-i18n['"]/.test(source) && 'imports vue-i18n',
30
+ source.includes('useI18n(') && 'calls useI18n',
31
+ source.includes('$t(') && 'calls global $t',
32
+ ].filter(Boolean)
33
+
34
+ return matches.map(message => `${relative(buildRoot, path)}: ${message}`)
35
+ })
36
+
37
+ expect(violations).toEqual([])
38
+ })
39
+
40
+ it('does not ask Vite to resolve vue-i18n for consumers', () => {
41
+ const nuxtConfig = readFileSync(resolve(buildRoot, 'nuxt.config.ts'), 'utf8')
42
+ const packageJson = JSON.parse(readFileSync(resolve(buildRoot, 'package.json'), 'utf8'))
43
+
44
+ expect(nuxtConfig).not.toContain('vue-i18n')
45
+ expect(packageJson.dependencies ?? {}).not.toHaveProperty('vue-i18n')
46
+ expect(packageJson.peerDependencies ?? {}).not.toHaveProperty('vue-i18n')
47
+ })
48
+ })
@@ -71,6 +71,7 @@ const value = ref('')
71
71
  v-model="value"
72
72
  placeholder="you@example.com"
73
73
  show-clear-button
74
+ clear-label="Clear"
74
75
  />
75
76
  ```
76
77
 
@@ -147,6 +148,7 @@ type InputProps = {
147
148
  disabled?: boolean
148
149
  error?: string | false
149
150
  showClearButton?: boolean
151
+ clearLabel?: string // default: 'Clear'; pass a translated label from the host app
150
152
  tabindex?: number
151
153
  autofocus?: boolean
152
154
  autocomplete?: 'on' | 'off'
@@ -166,7 +168,7 @@ type InputProps = {
166
168
  - **Label Support**: Built-in label with hide option for accessibility
167
169
  - **Error Handling**: Display validation errors with styling
168
170
  - **Icon Integration**: Add icons inside the input field
169
- - **Clear Button**: Quick clear functionality with show/hide control
171
+ - **Clear Button**: Quick clear functionality with a host-provided accessible tooltip label
170
172
  - **Auto-focus**: Automatically focus on mount
171
173
  - **Disabled State**: Proper disabled styling and behavior
172
174
  - **Character Limit**: Maxlength support with counter
@@ -10,6 +10,7 @@ const props = withDefaults(defineProps<{
10
10
  disabled?: boolean
11
11
  error?: string | false
12
12
  showClearButton?: boolean
13
+ clearLabel?: string
13
14
  tabindex?: number
14
15
  autofocus?: boolean
15
16
  autocomplete?: 'off' | 'on'
@@ -25,6 +26,7 @@ const props = withDefaults(defineProps<{
25
26
  size?: 'sm' | 'md'
26
27
  }>(), {
27
28
  variant: 'default',
29
+ clearLabel: 'Clear',
28
30
  })
29
31
 
30
32
  const emit = defineEmits<{
@@ -201,7 +203,7 @@ defineExpose({
201
203
 
202
204
  <div v-if="showClearButton && modelValue" class="top-60% -translate-y-60%" right-1 absolute>
203
205
  <TelaTooltip
204
- :content="$t('common.clear')"
206
+ :content="clearLabel"
205
207
  >
206
208
  <button
207
209
  hover:bg="#EBEFF199"
@@ -206,6 +206,26 @@ type SelectMenuOption = {
206
206
  to?: string
207
207
  }
208
208
 
209
+ type MultipleSelectLabels = {
210
+ selectOptions?: string
211
+ andMore?: (count: number) => string
212
+ add?: string
213
+ addAnother?: string
214
+ more?: string
215
+ and?: string
216
+ assignedItems?: string
217
+ otherCount?: (count: number) => string
218
+ itemCount?: (count: number) => string
219
+ selectAll?: string
220
+ clearAll?: string
221
+ noOptionsFound?: (search: string) => string
222
+ noRemainingValues?: string
223
+ press?: string
224
+ toClear?: string
225
+ clear?: string
226
+ selectMultipleOptions?: string
227
+ }
228
+
209
229
  type MultipleSelectProps = {
210
230
  modelValue?: string[]
211
231
  readOnly?: boolean
@@ -222,9 +242,12 @@ type MultipleSelectProps = {
222
242
  variant?: 'list' | 'preview' | 'compact'
223
243
  previewMaxItems?: number
224
244
  description?: string
245
+ labels?: MultipleSelectLabels
225
246
  }
226
247
  ```
227
248
 
249
+ Pass translated copy through `labels` when the host supports localization. English defaults are used for omitted labels, and the component does not require an i18n plugin.
250
+
228
251
  ## Slots
229
252
 
230
253
  - `trigger` - Custom trigger button (receives selectedOptions, selectedValues, displayText, readOnly, buttonIcon, buttonLabel, and toggleSelection)
@@ -13,7 +13,26 @@ import {
13
13
  } from 'radix-vue'
14
14
  import { nextTick } from 'vue'
15
15
  import PopoverList from '../popover-list/popover-list.vue'
16
- import { useI18n } from 'vue-i18n'
16
+
17
+ interface MultipleSelectLabels {
18
+ selectOptions?: string
19
+ andMore?: (count: number) => string
20
+ add?: string
21
+ addAnother?: string
22
+ more?: string
23
+ and?: string
24
+ assignedItems?: string
25
+ otherCount?: (count: number) => string
26
+ itemCount?: (count: number) => string
27
+ selectAll?: string
28
+ clearAll?: string
29
+ noOptionsFound?: (search: string) => string
30
+ noRemainingValues?: string
31
+ press?: string
32
+ toClear?: string
33
+ clear?: string
34
+ selectMultipleOptions?: string
35
+ }
17
36
 
18
37
  const props = withDefaults(defineProps<{
19
38
  modelValue?: string[]
@@ -32,6 +51,7 @@ const props = withDefaults(defineProps<{
32
51
  previewMaxItems?: number
33
52
  description?: string
34
53
  avatarOptions?: boolean
54
+ labels?: MultipleSelectLabels
35
55
  }>(), {
36
56
  maxSelectedDisplay: 10,
37
57
  searchPlaceholder: 'Search options...',
@@ -40,6 +60,7 @@ const props = withDefaults(defineProps<{
40
60
  buttonIcon: 'i-ph-plus',
41
61
  variant: 'list',
42
62
  previewMaxItems: 3,
63
+ labels: () => ({}),
43
64
  })
44
65
 
45
66
  const emit = defineEmits<{
@@ -51,7 +72,25 @@ const emit = defineEmits<{
51
72
  'blur': []
52
73
  }>()
53
74
 
54
- const { t } = useI18n()
75
+ const labels = computed(() => ({
76
+ selectOptions: props.labels.selectOptions ?? 'Select options...',
77
+ andMore: props.labels.andMore ?? ((count: number) => `+${count} more`),
78
+ add: props.labels.add ?? 'Add',
79
+ addAnother: props.labels.addAnother ?? 'Add another',
80
+ more: props.labels.more ?? 'more',
81
+ and: props.labels.and ?? 'and',
82
+ assignedItems: props.labels.assignedItems ?? 'Assigned items',
83
+ otherCount: props.labels.otherCount ?? ((count: number) => `${count} ${count === 1 ? 'other' : 'others'}`),
84
+ itemCount: props.labels.itemCount ?? ((count: number) => `${count} ${count === 1 ? 'item' : 'items'}`),
85
+ selectAll: props.labels.selectAll ?? 'Select all',
86
+ clearAll: props.labels.clearAll ?? 'Clear all',
87
+ noOptionsFound: props.labels.noOptionsFound ?? ((search: string) => `No options found with '${search}'`),
88
+ noRemainingValues: props.labels.noRemainingValues ?? 'No remaining values',
89
+ press: props.labels.press ?? 'Press',
90
+ toClear: props.labels.toClear ?? 'to clear',
91
+ clear: props.labels.clear ?? 'Clear',
92
+ selectMultipleOptions: props.labels.selectMultipleOptions ?? 'Select multiple options',
93
+ }))
55
94
 
56
95
  type SelectMenuOption = {
57
96
  value: string
@@ -185,7 +224,7 @@ const hasAvailableOptions = computed(() => {
185
224
 
186
225
  const displayText = computed(() => {
187
226
  if (selectedOptions.value.length === 0) {
188
- return props.placeholder || t('common.selectOptions')
227
+ return props.placeholder || labels.value.selectOptions
189
228
  }
190
229
 
191
230
  const maxDisplay = props.maxSelectedDisplay
@@ -193,7 +232,7 @@ const displayText = computed(() => {
193
232
  return selectedOptions.value.map(option => option.label).join(', ')
194
233
  }
195
234
 
196
- return `${selectedOptions.value.slice(0, maxDisplay).map(option => option.label).join(', ')} ${t('common.andMore', { count: selectedOptions.value.length - maxDisplay })}`
235
+ return `${selectedOptions.value.slice(0, maxDisplay).map(option => option.label).join(', ')} ${labels.value.andMore(selectedOptions.value.length - maxDisplay)}`
197
236
  })
198
237
 
199
238
  const previewDisplayText = computed(() => {
@@ -222,7 +261,7 @@ const remainingItems = computed(() => {
222
261
 
223
262
  const isSelected = (value: string) => selectedValues.value.includes(value)
224
263
 
225
- const buttonLabel = computed(() => props.buttonLabel ?? (selectedOptions.value.length === 0 ? t('common.add') : t('common.addAnother')))
264
+ const buttonLabel = computed(() => props.buttonLabel ?? (selectedOptions.value.length === 0 ? labels.value.add : labels.value.addAnother))
226
265
 
227
266
  function toggleSelection(value: string) {
228
267
  if (!value)
@@ -365,7 +404,7 @@ function handleSearchKeydown(e: KeyboardEvent) {
365
404
  border-t="0.5px gray-300"
366
405
  pt-4px
367
406
  >
368
- +{{ selectedOptions.length - (props.maxSelectedDisplay) }} {{ t('common.more') }}
407
+ +{{ selectedOptions.length - (props.maxSelectedDisplay) }} {{ labels.more }}
369
408
  </span>
370
409
  </div>
371
410
  </div>
@@ -385,7 +424,7 @@ function handleSearchKeydown(e: KeyboardEvent) {
385
424
  rounded-8px
386
425
  transition
387
426
  outline-none
388
- aria-label="Select multiple options"
427
+ :aria-label="labels.selectMultipleOptions"
389
428
  w-full
390
429
  v-bind="$attrs"
391
430
  :class="[
@@ -423,17 +462,17 @@ function handleSearchKeydown(e: KeyboardEvent) {
423
462
  v-if="previewDisplayText.remainingCount > 0"
424
463
  class="body-12-regular flex-shrink-0 ml-4px"
425
464
  >
426
- {{ t('common.and') }}
465
+ {{ labels.and }}
427
466
  </span>
428
467
  <PopoverList
429
468
  v-if="previewDisplayText.remainingCount > 0"
430
469
  :items="remainingItems.map(item => item.label)"
431
470
  position="center"
432
- :title="t('workstationComponents.assignedColumns')"
471
+ :title="labels.assignedItems"
433
472
  >
434
473
  <template #trigger>
435
474
  <span class="body-12-regular underline flex-shrink-0 ml-4px cursor-pointer hover:text-blue-600 transition-colors">
436
- {{ t('common.otherCount', { count: previewDisplayText.remainingCount }) }}
475
+ {{ labels.otherCount(previewDisplayText.remainingCount) }}
437
476
  </span>
438
477
  </template>
439
478
  </PopoverList>
@@ -478,11 +517,11 @@ function handleSearchKeydown(e: KeyboardEvent) {
478
517
  </div>
479
518
 
480
519
  <div v-if="filteredOptions.length > 0" flex items-center justify-between pl-10px pr-10px py-4px border-b="0.5px border">
481
- <span body-12-semibold text-gray-700>{{ t('common.itemCount', { count: filteredOptions.length }) }}</span>
520
+ <span body-12-semibold text-gray-700>{{ labels.itemCount(filteredOptions.length) }}</span>
482
521
 
483
522
  <TelaButton variant="ghost" size="sm" class="rounded-lg" @click="selectAll">
484
523
  <span body-12-regular text-gray-900 underline>
485
- {{ selectedValues.length !== filteredOptions.length ? t('common.selectAll') : t('common.clearAll') }}
524
+ {{ selectedValues.length !== filteredOptions.length ? labels.selectAll : labels.clearAll }}
486
525
  </span>
487
526
  </TelaButton>
488
527
  </div>
@@ -495,10 +534,10 @@ function handleSearchKeydown(e: KeyboardEvent) {
495
534
  <div class="flex flex-col items-center justify-center gap-3">
496
535
  <TelaIcon name="i-ph-smiley-sad" size="24px" class="text-gray-400" />
497
536
  <span class="text-xl font-medium leading-none text-center">
498
- {{ search.trim() ? t('common.noOptionsFound', { search: search.trim() }) : t('common.noRemainingValues') }}
537
+ {{ search.trim() ? labels.noOptionsFound(search.trim()) : labels.noRemainingValues }}
499
538
  </span>
500
539
  <span v-if="search.trim()" class="inline-flex gap-1.5 text-md text-gray-500 items-center">
501
- {{ t('common.press') }}
540
+ {{ labels.press }}
502
541
  <div class="flex items-center gap-1">
503
542
  <div class="p-2px bg-gray-200 rounded">
504
543
  <TelaIcon name="i-ph-command" size="16px" />
@@ -507,7 +546,7 @@ function handleSearchKeydown(e: KeyboardEvent) {
507
546
  <TelaIcon name="i-ph-backspace" size="16px" />
508
547
  </div>
509
548
  </div>
510
- {{ t('common.toClear') }}
549
+ {{ labels.toClear }}
511
550
  </span>
512
551
  </div>
513
552
  <TelaButton
@@ -517,7 +556,7 @@ function handleSearchKeydown(e: KeyboardEvent) {
517
556
  class="rounded-lg"
518
557
  @click="handleDeleteSearchValue"
519
558
  >
520
- {{ t('common.clear') }}
559
+ {{ labels.clear }}
521
560
  </TelaButton>
522
561
  </div>
523
562
 
@@ -6,6 +6,7 @@ interface PopoverListProps<T> {
6
6
  items: T[]
7
7
  title?: string
8
8
  subtitle?: string
9
+ emptyLabel?: string
9
10
  position?: 'left' | 'center' | 'right'
10
11
  placement?: 'top' | 'bottom'
11
12
  nested?: boolean
@@ -19,6 +20,7 @@ const props = withDefaults(defineProps<PopoverListProps<T>>(), {
19
20
  placement: 'bottom',
20
21
  nested: false,
21
22
  nestedVariant: 'attributes',
23
+ emptyLabel: 'No items available',
22
24
  })
23
25
 
24
26
  const popoverOpen = defineModel('open', {
@@ -180,7 +182,7 @@ function getChildren(item: T): T[] {
180
182
  <slot name="empty">
181
183
  <div class="flex flex-col items-start gap-2 w-full">
182
184
  <p class="text-[#D4D9DE] text-left font-inter text-[12px] font-normal leading-[16px] w-full">
183
- {{ $t('common.noItemsAvailable') }}
185
+ {{ emptyLabel }}
184
186
  </p>
185
187
  </div>
186
188
  </slot>
@@ -90,11 +90,11 @@ const showMoreTextWithCount = computed(() => {
90
90
  <div flex="~ col" gap-10px :class="props.contentClass">
91
91
  <div flex items-center justify-between pl-4px pb-10px border-b-0.5px border>
92
92
  <h5 heading-h5-semibold text-primary class="@2xl:heading-h4-semibold">
93
- {{ $t('workflow.agent.tools.todoList') }}
93
+ {{ props.title }}
94
94
  </h5>
95
- <TelaStatus v-if="completedPercent === 100" variant="completed" :label="$t('workflow.agent.tools.completed')" />
95
+ <TelaStatus v-if="completedPercent === 100" variant="completed" :label="props.completedText" />
96
96
  <p v-else body-14-medium text-secondary class="@2xl:body-16-medium">
97
- <TelaAnimatedNumber :value="completedPercent" />% {{ $t('workflow.agent.tools.completed') }}
97
+ <TelaAnimatedNumber :value="completedPercent" />% {{ props.completedText }}
98
98
  </p>
99
99
  </div>
100
100
  <div flex="~ col" gap-7px class="@2xl:gap-10px" px-1px>
@@ -22,6 +22,11 @@ const props = withDefaults(defineProps<{
22
22
  errorLabel?: string
23
23
  itemsLabel?: string
24
24
  propertiesLabel?: string
25
+ itemsCountLabel?: (count: number) => string
26
+ propertiesCountLabel?: (count: number) => string
27
+ showMoreLabel?: string
28
+ showLessLabel?: string
29
+ resultLabel?: string
25
30
  iconWrapperClass?: any
26
31
  iconClass?: any
27
32
  iconColor?: string
@@ -33,6 +38,11 @@ const props = withDefaults(defineProps<{
33
38
  errorLabel: 'Error',
34
39
  itemsLabel: 'items',
35
40
  propertiesLabel: 'properties',
41
+ itemsCountLabel: (count: number) => `${count} items`,
42
+ propertiesCountLabel: (count: number) => `${count} properties`,
43
+ showMoreLabel: 'Show more',
44
+ showLessLabel: 'Show less',
45
+ resultLabel: 'Result',
36
46
  iconColor: 'icon-secondary',
37
47
  })
38
48
 
@@ -236,7 +246,7 @@ function getIconName() {
236
246
 
237
247
  <div v-else-if="Array.isArray(value)" flex="~ col" gap-4px>
238
248
  <span text-xs text-gray-500>
239
- {{ $t('reasoningSteps.itemsCount', { count: value.length }) }}
249
+ {{ props.itemsCountLabel(value.length) }}
240
250
  </span>
241
251
  <div bg-gray-50 rounded-10px bg border-0.5px border-gray-200 p-3>
242
252
  <div v-for="(item, idx) in value" :key="idx" text-sm py-0.5>
@@ -248,7 +258,7 @@ function getIconName() {
248
258
 
249
259
  <div v-else-if="typeof value === 'object' && value !== null" flex="~ col" gap-4px>
250
260
  <span text-xs text-gray-500>
251
- {{ $t('reasoningSteps.propertiesCount', { count: Object.keys(value).length }) }}
261
+ {{ props.propertiesCountLabel(Object.keys(value).length) }}
252
262
  </span>
253
263
  <div bg-gray-50 rounded-10px bg border-0.5px border-gray-200 p-3>
254
264
  <div
@@ -259,7 +269,7 @@ function getIconName() {
259
269
  </div>
260
270
  <div v-if="needsToggle(value)" mt-2 flex justify-center>
261
271
  <button type="button" text-sm text-primary-600 hover:opacity-80 flex items-center gap-2 @click.prevent="toggle(String(key))">
262
- {{ expanded[String(key)] ? $t('reasoningSteps.showLess') : $t('reasoningSteps.showMore') }}
272
+ {{ expanded[String(key)] ? props.showLessLabel : props.showMoreLabel }}
263
273
  <i class="i-ph-caret-down" :style="{ transform: expanded[String(key)] ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }" />
264
274
  </button>
265
275
  </div>
@@ -276,7 +286,7 @@ function getIconName() {
276
286
  </div>
277
287
  <div v-if="needsToggle(value)" mt-2 flex justify-center>
278
288
  <button type="button" text-sm text-primary-600 hover:opacity-80 flex items-center gap-2 @click.prevent="toggle(String(key))">
279
- {{ expanded[String(key)] ? $t('reasoningSteps.showLess') : $t('reasoningSteps.showMore') }}
289
+ {{ expanded[String(key)] ? props.showLessLabel : props.showMoreLabel }}
280
290
  <i class="i-ph-caret-down" :style="{ transform: expanded[String(key)] ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }" />
281
291
  </button>
282
292
  </div>
@@ -284,7 +294,7 @@ function getIconName() {
284
294
  </div>
285
295
  </div>
286
296
  <div v-if="toolResult?.output || isRunning" flex="~ col" gap-6px>
287
- <span body-12-regular text-gray-700 class="@2xl:body-14-regular">{{ $t('workflow.result') }}</span>
297
+ <span body-12-regular text-gray-700 class="@2xl:body-14-regular">{{ props.resultLabel }}</span>
288
298
  <div v-if="toolResult?.output" bg border-0.5px border-gray-200 rounded-10px pl-10px py-2>
289
299
  <TelaScrollArea class="flex flex-col max-h-300px">
290
300
  <div max-h-300px pr-4>
@@ -1,6 +1,4 @@
1
1
  <script setup lang="ts">
2
- import { useI18n } from 'vue-i18n'
3
-
4
2
  interface Variable {
5
3
  name: string
6
4
  type: 'file' | 'text' | 'mixed'
@@ -8,15 +6,38 @@ interface Variable {
8
6
  processingOptions?: { allowMultimodal: boolean }
9
7
  }
10
8
 
11
- defineProps<{
9
+ interface VariableInputLabels {
10
+ file?: string
11
+ text?: string
12
+ mixed?: string
13
+ multimodal?: string
14
+ required?: string
15
+ placeholder?: string
16
+ }
17
+
18
+ const props = withDefaults(defineProps<{
12
19
  variable: Variable
13
20
  tabindex?: number
14
21
  error?: boolean
15
- }>()
22
+ labels?: VariableInputLabels
23
+ }>(), {
24
+ labels: () => ({}),
25
+ })
16
26
  const content = defineModel<string>()
17
27
  const markdownContent = defineModel<string>('markdownContent')
18
28
 
19
- const { t } = useI18n()
29
+ const labels = computed(() => ({
30
+ file: props.labels.file ?? 'file',
31
+ text: props.labels.text ?? 'text',
32
+ mixed: props.labels.mixed ?? 'mixed',
33
+ multimodal: props.labels.multimodal ?? 'multimodal',
34
+ required: props.labels.required ?? 'Required',
35
+ placeholder: props.labels.placeholder ?? 'Write, paste, or drop a file',
36
+ }))
37
+
38
+ const variableTypeLabel = computed(() => props.variable.processingOptions?.allowMultimodal
39
+ ? labels.value.multimodal
40
+ : labels.value[props.variable.type])
20
41
  </script>
21
42
 
22
43
  <template>
@@ -28,11 +49,11 @@ const { t } = useI18n()
28
49
  {{ variable.name }}{{ variable.required ? "*" : "" }}
29
50
  </p>
30
51
  <p body-12-regular text-gray-400>
31
- ({{ variable.processingOptions?.allowMultimodal ? t('canvas.variableTypes.multimodal') : t(`canvas.variableTypes.${variable.type}`) }})
52
+ ({{ variableTypeLabel }})
32
53
  </p>
33
54
  </div>
34
55
  <p v-if="variable.required" body-12-regular text-gray-400>
35
- * {{ t('canvas.required') }}
56
+ * {{ labels.required }}
36
57
  </p>
37
58
  <slot name="actions" />
38
59
  </div>
@@ -47,7 +68,7 @@ const { t } = useI18n()
47
68
  v-model:markdown-content="markdownContent"
48
69
  v-model="content"
49
70
  basic
50
- :placeholder="t('canvas.variablePlaceholder')"
71
+ :placeholder="labels.placeholder"
51
72
  text="12px textcolor/90"
52
73
  leading-1.4em tracking--0.01em
53
74
  :multimodal="variable.processingOptions?.allowMultimodal"
package/nuxt.config.ts CHANGED
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url'
7
7
  import { createResolver } from 'nuxt/kit'
8
8
 
9
9
  const currentDir = dirname(fileURLToPath(import.meta.url))
10
- const { resolve } = createResolver(import.meta.url)
10
+ const resolver = createResolver(import.meta.url)
11
11
  const componentsDir = join(currentDir, './components')
12
12
  const composablesDir = join(currentDir, './composables')
13
13
  const utilsDir = join(currentDir, './utils')
@@ -25,7 +25,7 @@ export default defineNuxtConfig({
25
25
  '@vueuse/nuxt',
26
26
  'motion-v/nuxt',
27
27
  'nuxt-svgo',
28
- resolve('./modules/tela-build-docs'),
28
+ resolver.resolve('./modules/tela-build-docs'),
29
29
  ],
30
30
 
31
31
  svgo: {
@@ -116,7 +116,6 @@ export default defineNuxtConfig({
116
116
  'shiki/core',
117
117
  'shiki/engine/javascript',
118
118
  'tailwind-merge',
119
- 'vue-i18n',
120
119
  'vue-input-otp',
121
120
  ],
122
121
  exclude: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.70.1",
3
+ "version": "1.70.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",