@meistrari/tela-build 1.70.1 → 1.70.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.
- package/components/tela/chat/text-message/index.vue +10 -5
- package/components/tela/combobox/combobox-module-selector.vue +53 -16
- package/components/tela/combobox/combobox.mdx +4 -0
- package/components/tela/complex-table/complex-table-row.vue +6 -3
- package/components/tela/complex-table/complex-table-virtualized.vue +1 -0
- package/components/tela/complex-table/complex-table.mdx +1 -0
- package/components/tela/complex-table/complex-table.vue +1 -0
- package/components/tela/complex-table/types.ts +1 -0
- package/components/tela/file-upload/file-upload.mdx +21 -3
- package/components/tela/file-upload/file-upload.vue +35 -10
- package/components/tela/i18n-boundary.test.ts +48 -0
- package/components/tela/input/input.mdx +3 -1
- package/components/tela/input/input.vue +3 -1
- package/components/tela/multiple-select/multiple-select.mdx +23 -0
- package/components/tela/multiple-select/multiple-select.vue +55 -16
- package/components/tela/popover-list/popover-list.vue +3 -1
- package/components/tela/reasoning/todo-list/todo-list.vue +3 -3
- package/components/tela/reasoning/tool-use-display/tool-use-display.vue +15 -5
- package/components/tela/variable-input.vue +29 -8
- package/nuxt.config.ts +2 -3
- package/package.json +1 -1
|
@@ -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
|
|
106
|
+
return props.todayAtLabel(date.toLocaleTimeString(props.locale, { hour: '2-digit', minute: '2-digit', hour12: false }))
|
|
102
107
|
|
|
103
|
-
return
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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="
|
|
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
|
-
{{
|
|
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
|
-
{{
|
|
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
|
-
{{
|
|
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
|
-
{{
|
|
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
|
-
>{{
|
|
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
|
-
{{
|
|
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)"
|
|
@@ -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
|
-
|
|
144
|
+
class?: string
|
|
132
145
|
multiple?: boolean
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
-
{{
|
|
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
|
-
{{
|
|
201
|
+
{{ labels.supportedFormats }}: {{ fileTypesDisplay.text }}
|
|
177
202
|
<template v-if="fileTypesDisplay.hasMore">
|
|
178
|
-
{{
|
|
203
|
+
{{ labels.and }}
|
|
179
204
|
<TelaLinkDecoration class="text-inherit underline-offset-2px">
|
|
180
|
-
{{ fileTypesDisplay.moreCount }} {{
|
|
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
|
-
{{
|
|
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
|
|
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="
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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(', ')} ${
|
|
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 ?
|
|
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) }} {{
|
|
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="
|
|
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
|
-
{{
|
|
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="
|
|
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
|
-
{{
|
|
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>{{
|
|
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 ?
|
|
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() ?
|
|
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
|
-
{{
|
|
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
|
-
{{
|
|
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
|
-
{{
|
|
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
|
-
{{
|
|
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
|
-
{{
|
|
93
|
+
{{ props.title }}
|
|
94
94
|
</h5>
|
|
95
|
-
<TelaStatus v-if="completedPercent === 100" variant="completed" :label="
|
|
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" />% {{
|
|
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
|
-
{{
|
|
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
|
-
{{
|
|
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)] ?
|
|
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)] ?
|
|
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">{{
|
|
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
|
-
|
|
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
|
|
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
|
-
({{
|
|
52
|
+
({{ variableTypeLabel }})
|
|
32
53
|
</p>
|
|
33
54
|
</div>
|
|
34
55
|
<p v-if="variable.required" body-12-regular text-gray-400>
|
|
35
|
-
* {{
|
|
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="
|
|
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
|
|
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: [
|