@meistrari/tela-build 1.74.0 → 1.74.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/CHANGELOG.md +4 -0
- package/components/tela/chat/answer/answer.vue +1 -1
- package/components/tela/chat/chat.mdx +137 -53
- package/components/tela/dropdown-menu/DropdownMenu.vue +2 -1
- package/components/tela/dropdown-menu/dropdown-menu.mdx +10 -0
- package/composables/use-pdf.ts +11 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ Latest updates and announcements.
|
|
|
4
4
|
|
|
5
5
|
## September 18, 2026
|
|
6
6
|
|
|
7
|
+
- [Floating chat](/templates/chat#floating) docs now show `TelaChatAnswer` docked above the prompt input inside the window.
|
|
8
|
+
|
|
9
|
+
- [Chat Answer](/templates/chat#answer) centers the card horizontally in its container and keeps it within the available width on narrow screens.
|
|
10
|
+
|
|
7
11
|
- [Floating chat](/templates/chat) adds `closeOnOutsideClick` (default `true`); set it to `false` to keep the window open while using the surrounding page.
|
|
8
12
|
|
|
9
13
|
## September 17, 2026
|
|
@@ -174,7 +174,7 @@ function onKeydown(event: KeyboardEvent) {
|
|
|
174
174
|
|
|
175
175
|
<template>
|
|
176
176
|
<div ref="root" tabindex="-1" outline-none>
|
|
177
|
-
<TelaCard size="sm" class="!p-0 w-
|
|
177
|
+
<TelaCard size="sm" class="!p-0 w-450px" mx-auto max-w-full overflow-hidden>
|
|
178
178
|
<TelaCollapsible :open="expanded" @update:open="emit('update:expanded', $event)">
|
|
179
179
|
<div
|
|
180
180
|
flex items-center justify-between gap-8px min-h-56px py-8px pl-16px pr-12px
|
|
@@ -149,8 +149,11 @@ The window opens over its own launcher: the surface springs out of the launcher'
|
|
|
149
149
|
|
|
150
150
|
The widget header inside the window gets two extra actions automatically: **expand** grows the window until 8px from the top of the viewport (and back), and **minimize** closes it. See [Widget Header & History](#widget-header--history).
|
|
151
151
|
|
|
152
|
+
When the assistant needs a decision, dock a [`TelaChatAnswer`](#answer) above the prompt input: wrap both in one `[data-chat-composer]` element with a 12px gap so `TelaChatBody` pins and measures them together.
|
|
153
|
+
|
|
152
154
|
```vue
|
|
153
155
|
<script setup lang="ts">
|
|
156
|
+
import type { TelaChatAnswerQuestion } from '@meistrari/tela-build/types'
|
|
154
157
|
import { getMimeTypeIcon } from '@meistrari/tela-build/utils/files'
|
|
155
158
|
|
|
156
159
|
const isOpen = ref(false)
|
|
@@ -164,20 +167,75 @@ const skills = [
|
|
|
164
167
|
{ id: 's1', label: 'Summarize document' },
|
|
165
168
|
{ id: 's2', label: 'Draft release announcement' },
|
|
166
169
|
]
|
|
167
|
-
const messages = [
|
|
170
|
+
const messages = ref([
|
|
168
171
|
{ id: '1', role: 'user' as const, text: 'What changed in the latest release?' },
|
|
169
|
-
{ id: '2', role: 'assistant' as const, text: 'The chat primitives landed: a conversation viewport, a prompt input, and three shells you can compose freely.' },
|
|
170
|
-
]
|
|
172
|
+
{ id: '2', role: 'assistant' as const, text: 'The chat primitives landed: a conversation viewport, a prompt input, and three shells you can compose freely. Want me to draft the announcement?' },
|
|
173
|
+
])
|
|
171
174
|
const conversations = [
|
|
172
175
|
{ id: 'c1', title: 'Release notes review' },
|
|
173
176
|
{ id: 'c2', title: 'Sidepanel layout questions' },
|
|
174
177
|
{ id: 'c3', title: 'Model selector options' },
|
|
175
178
|
]
|
|
179
|
+
|
|
180
|
+
// The assistant needs a decision before it continues. The host owns the
|
|
181
|
+
// question sequence and the selection; the card only emits intent.
|
|
182
|
+
const questions: TelaChatAnswerQuestion[] = [
|
|
183
|
+
{
|
|
184
|
+
id: 'tone',
|
|
185
|
+
title: 'Which tone should the announcement use?',
|
|
186
|
+
type: 'single',
|
|
187
|
+
recommendFirst: true,
|
|
188
|
+
options: [
|
|
189
|
+
{ id: 'concise', label: 'Concise', description: 'Short and factual, for the changelog.' },
|
|
190
|
+
{ id: 'friendly', label: 'Friendly', description: 'Warmer copy, for the newsletter.' },
|
|
191
|
+
{ id: 'formal', label: 'Formal', description: 'For enterprise customers.' },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'sections',
|
|
196
|
+
title: 'Which sections should it include?',
|
|
197
|
+
type: 'multiple',
|
|
198
|
+
options: [
|
|
199
|
+
{ id: 'highlights', label: 'Highlights' },
|
|
200
|
+
{ id: 'breaking', label: 'Breaking changes' },
|
|
201
|
+
{ id: 'upgrade', label: 'Upgrade guide' },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
]
|
|
205
|
+
const index = ref(0)
|
|
206
|
+
const expanded = ref(true)
|
|
207
|
+
const answering = ref(true)
|
|
208
|
+
const answers = ref<Record<string, string[]>>({})
|
|
209
|
+
const question = computed(() => questions[index.value]!)
|
|
210
|
+
const selected = computed(() => answers.value[question.value.id] ?? [])
|
|
211
|
+
|
|
212
|
+
function select(id: string) {
|
|
213
|
+
answers.value[question.value.id] = [id]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function toggle(id: string, checked: boolean) {
|
|
217
|
+
answers.value[question.value.id] = checked
|
|
218
|
+
? [...selected.value, id]
|
|
219
|
+
: selected.value.filter(value => value !== id)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function advance() {
|
|
223
|
+
if (index.value < questions.length - 1) {
|
|
224
|
+
index.value++
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const summary = questions
|
|
229
|
+
.map(item => `${item.title} ${(answers.value[item.id] ?? []).join(', ') || 'skipped'}`)
|
|
230
|
+
.join(' ')
|
|
231
|
+
messages.value.push({ id: `${Date.now()}`, role: 'user', text: summary })
|
|
232
|
+
answering.value = false
|
|
233
|
+
}
|
|
176
234
|
</script>
|
|
177
235
|
|
|
178
236
|
<template>
|
|
179
237
|
<!-- `contained` anchors the launcher to this box instead of the viewport. -->
|
|
180
|
-
<div class="relative w-full h-
|
|
238
|
+
<div class="relative w-full h-640px">
|
|
181
239
|
<TelaChatFloatingWindow contained :open="isOpen" @update:open="isOpen = $event">
|
|
182
240
|
<!-- The default launcher is TelaChatFloatingWindowTrigger; override it via the #launcher slot. -->
|
|
183
241
|
<TelaChatWidgetHeader title="Release notes review">
|
|
@@ -197,53 +255,76 @@ const conversations = [
|
|
|
197
255
|
</TelaChatConversationContent>
|
|
198
256
|
</TelaChatConversation>
|
|
199
257
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
258
|
+
<!-- One composer wrapper: the answer card docks above the prompt input. -->
|
|
259
|
+
<div data-chat-composer class="flex flex-col gap-12px">
|
|
260
|
+
<TelaChatAnswer
|
|
261
|
+
v-if="answering"
|
|
262
|
+
v-model:expanded="expanded"
|
|
263
|
+
:question="question"
|
|
264
|
+
:selected="selected"
|
|
265
|
+
:current="index + 1"
|
|
266
|
+
:total="questions.length"
|
|
267
|
+
:can-continue="selected.length > 0"
|
|
268
|
+
show-navigation
|
|
269
|
+
:can-previous="index > 0"
|
|
270
|
+
:can-next="index < questions.length - 1"
|
|
271
|
+
@select="select"
|
|
272
|
+
@toggle="toggle"
|
|
273
|
+
@previous="index--"
|
|
274
|
+
@next="index++"
|
|
275
|
+
@skip="advance"
|
|
276
|
+
@continue="advance"
|
|
277
|
+
@dismiss="answering = false"
|
|
278
|
+
/>
|
|
279
|
+
|
|
280
|
+
<TelaChatPromptInput>
|
|
281
|
+
<TelaChatPromptInputTextarea placeholder="Ask anything" :empty="!draft" />
|
|
282
|
+
<TelaChatPromptInputToolbar>
|
|
283
|
+
<TelaChatPromptInputTools>
|
|
284
|
+
<TelaChatPromptInputActionMenu>
|
|
285
|
+
<TelaChatPromptInputActionMenuTrigger />
|
|
286
|
+
<TelaChatPromptInputActionMenuContent>
|
|
287
|
+
<TelaChatPromptInputActionMenuItem icon="i-ph-paperclip">
|
|
288
|
+
Add files or photos
|
|
289
|
+
</TelaChatPromptInputActionMenuItem>
|
|
290
|
+
<TelaChatPromptInputActionMenuSub>
|
|
291
|
+
<TelaChatPromptInputActionMenuSubTrigger icon="i-ph-clock-counter-clockwise">
|
|
292
|
+
Recent files
|
|
293
|
+
</TelaChatPromptInputActionMenuSubTrigger>
|
|
294
|
+
<TelaChatPromptInputActionMenuSubContent>
|
|
295
|
+
<TelaChatPromptInputActionMenuItem
|
|
296
|
+
v-for="file in recentFiles"
|
|
297
|
+
:key="file.id"
|
|
298
|
+
>
|
|
299
|
+
<img :src="getMimeTypeIcon(file.type)" class="w-16px h-16px mr-8px shrink-0" alt="">
|
|
300
|
+
<span class="min-w-0 max-w-200px truncate">{{ file.label }}</span>
|
|
301
|
+
</TelaChatPromptInputActionMenuItem>
|
|
302
|
+
</TelaChatPromptInputActionMenuSubContent>
|
|
303
|
+
</TelaChatPromptInputActionMenuSub>
|
|
304
|
+
<TelaMenubarSeparator />
|
|
305
|
+
<TelaChatPromptInputActionMenuSub>
|
|
306
|
+
<TelaChatPromptInputActionMenuSubTrigger icon="i-ph-notepad" icon-size="16px">
|
|
307
|
+
Skills
|
|
308
|
+
</TelaChatPromptInputActionMenuSubTrigger>
|
|
309
|
+
<TelaChatPromptInputActionMenuSubContent>
|
|
310
|
+
<TelaChatPromptInputActionMenuItem
|
|
311
|
+
v-for="skill in skills"
|
|
312
|
+
:key="skill.id"
|
|
313
|
+
@click="draft = skill.label"
|
|
314
|
+
>
|
|
315
|
+
<span class="min-w-0 max-w-200px truncate">{{ skill.label }}</span>
|
|
316
|
+
</TelaChatPromptInputActionMenuItem>
|
|
317
|
+
</TelaChatPromptInputActionMenuSubContent>
|
|
318
|
+
</TelaChatPromptInputActionMenuSub>
|
|
319
|
+
</TelaChatPromptInputActionMenuContent>
|
|
320
|
+
</TelaChatPromptInputActionMenu>
|
|
321
|
+
</TelaChatPromptInputTools>
|
|
322
|
+
<TelaChatPromptInputActions>
|
|
323
|
+
<TelaChatPromptSubmitButton status="ready" :disabled="!draft.trim()" />
|
|
324
|
+
</TelaChatPromptInputActions>
|
|
325
|
+
</TelaChatPromptInputToolbar>
|
|
326
|
+
</TelaChatPromptInput>
|
|
327
|
+
</div>
|
|
247
328
|
</TelaChatBody>
|
|
248
329
|
</TelaChatFloatingWindow>
|
|
249
330
|
</div>
|
|
@@ -1016,6 +1097,7 @@ const questions: TelaChatAnswerQuestion[] = [
|
|
|
1016
1097
|
const index = ref(0)
|
|
1017
1098
|
const expanded = ref(true)
|
|
1018
1099
|
const done = ref(false)
|
|
1100
|
+
const dismissed = ref(false)
|
|
1019
1101
|
const answers = ref<Record<string, string[]>>({})
|
|
1020
1102
|
|
|
1021
1103
|
const question = computed(() => questions[index.value]!)
|
|
@@ -1045,6 +1127,7 @@ function restart() {
|
|
|
1045
1127
|
index.value = 0
|
|
1046
1128
|
answers.value = {}
|
|
1047
1129
|
done.value = false
|
|
1130
|
+
dismissed.value = false
|
|
1048
1131
|
expanded.value = true
|
|
1049
1132
|
}
|
|
1050
1133
|
</script>
|
|
@@ -1052,6 +1135,7 @@ function restart() {
|
|
|
1052
1135
|
<template>
|
|
1053
1136
|
<div class="flex flex-col items-start gap-12px w-full max-w-400px">
|
|
1054
1137
|
<TelaChatAnswer
|
|
1138
|
+
v-if="!dismissed"
|
|
1055
1139
|
v-model:expanded="expanded"
|
|
1056
1140
|
class="w-full"
|
|
1057
1141
|
:question="question"
|
|
@@ -1069,9 +1153,9 @@ function restart() {
|
|
|
1069
1153
|
@next="index++"
|
|
1070
1154
|
@skip="advance"
|
|
1071
1155
|
@continue="advance"
|
|
1072
|
-
@dismiss="
|
|
1156
|
+
@dismiss="dismissed = true"
|
|
1073
1157
|
/>
|
|
1074
|
-
<TelaButton v-if="done" variant="secondary" size="sm" @click="restart">
|
|
1158
|
+
<TelaButton v-if="done || dismissed" variant="secondary" size="sm" @click="restart">
|
|
1075
1159
|
Ask again
|
|
1076
1160
|
</TelaButton>
|
|
1077
1161
|
</div>
|
|
@@ -41,7 +41,7 @@ const props = withDefaults(defineProps<{
|
|
|
41
41
|
iconSide: 'left',
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
defineEmits(['click'])
|
|
44
|
+
const emit = defineEmits(['click', 'update:open'])
|
|
45
45
|
|
|
46
46
|
const search = ref('')
|
|
47
47
|
const searchInputEl = ref<HTMLInputElement>()
|
|
@@ -58,6 +58,7 @@ const groups = computed(() => filteredItems.value.reduce((acc, item) => {
|
|
|
58
58
|
}, {} as Record<string, typeof props.items[number][]>))
|
|
59
59
|
|
|
60
60
|
function onToggle(open: boolean) {
|
|
61
|
+
emit('update:open', open)
|
|
61
62
|
if (!open)
|
|
62
63
|
return
|
|
63
64
|
|
|
@@ -24,6 +24,16 @@ Use `TelaDropdownMenu` when clicking triggers a side effect. Use `TelaSelectMenu
|
|
|
24
24
|
|
|
25
25
|
**The test:** If clicking triggers a side effect → `TelaDropdownMenu`. If it updates a bound value → `TelaSelectMenu`.
|
|
26
26
|
|
|
27
|
+
## Loading menu data on open
|
|
28
|
+
|
|
29
|
+
`update:open` emits a boolean whenever the menu opens or closes, including keyboard activation. Use it to fetch menu data only when needed:
|
|
30
|
+
|
|
31
|
+
```vue
|
|
32
|
+
<TelaDropdownMenu :items="items" @update:open="open => open && loadItems()">
|
|
33
|
+
<TelaButton>Open menu</TelaButton>
|
|
34
|
+
</TelaDropdownMenu>
|
|
35
|
+
```
|
|
36
|
+
|
|
27
37
|
## Examples
|
|
28
38
|
|
|
29
39
|
### Basic Usage
|
package/composables/use-pdf.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import * as pdfjsLib from 'pdfjs-dist'
|
|
2
1
|
import type { PDFDocumentProxy, PDFPageProxy, RenderTask } from 'pdfjs-dist'
|
|
3
2
|
import type { Ref } from 'vue'
|
|
4
3
|
import { markRaw, onUnmounted, reactive, watch } from 'vue'
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
pdfjsLib
|
|
5
|
+
async function loadPdfLibrary() {
|
|
6
|
+
const pdfjsLib = await import('pdfjs-dist')
|
|
7
|
+
if (!pdfjsLib.GlobalWorkerOptions.workerSrc)
|
|
8
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`
|
|
9
|
+
return pdfjsLib
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
function normalizeForMatch(text: string): string {
|
|
@@ -70,7 +72,11 @@ export function usePdf(url: Ref<string>) {
|
|
|
70
72
|
state.loadError = null
|
|
71
73
|
|
|
72
74
|
try {
|
|
73
|
-
const
|
|
75
|
+
const requestedUrl = url.value
|
|
76
|
+
const pdfjsLib = await loadPdfLibrary()
|
|
77
|
+
if (isUnmounted)
|
|
78
|
+
return null
|
|
79
|
+
const loadingTask = pdfjsLib.getDocument(requestedUrl)
|
|
74
80
|
const doc = await loadingTask.promise
|
|
75
81
|
|
|
76
82
|
if (isUnmounted) {
|
|
@@ -304,6 +310,7 @@ export function usePdf(url: Ref<string>) {
|
|
|
304
310
|
}
|
|
305
311
|
}
|
|
306
312
|
|
|
313
|
+
const pdfjsLib = await loadPdfLibrary()
|
|
307
314
|
for (let itemIndex = 0; itemIndex < textItems.length; itemIndex++) {
|
|
308
315
|
if (isUnmounted)
|
|
309
316
|
return
|