@meistrari/tela-build 1.70.5 → 1.71.0
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 +18 -0
- package/components/tela/chat/chat-widget-header.vue +49 -1
- package/components/tela/chat/chat.mdx +22 -4
- package/components/tela/chat/floating/floating-window-context.ts +9 -0
- package/components/tela/chat/floating/floating-window.vue +139 -16
- package/components/tela/chat/message/message-content.vue +2 -2
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Latest updates and announcements.
|
|
4
|
+
|
|
5
|
+
## September 15, 2026
|
|
6
|
+
|
|
7
|
+
### Chat: Floating window
|
|
8
|
+
|
|
9
|
+
- **Morph open/close.** `TelaChatFloatingWindow` now opens over its own launcher instead of above it. The surface springs out of the launcher's box and shrinks back into it on close, while the conversation is revealed in place. The launcher fades out while the window is open. See [Chat → Floating](/templates/chat#floating).
|
|
10
|
+
- **Expand.** The widget header shows an expand toggle inside the floating window. It grows the window until 8px from the top of the viewport; toggling again restores the default height.
|
|
11
|
+
- **Minimize.** The widget header shows a minus button inside the floating window that closes it. `TelaChatWidgetHeader` also emits `close`.
|
|
12
|
+
- **No layout shift on close.** The page scroll lock is released only after the exit animation has finished, so the returning scrollbar no longer shifts the window mid-animation.
|
|
13
|
+
- **Expanded input fix.** The conversation body is sized from the popover's rendered height, so the prompt input is no longer clipped when the window is expanded.
|
|
14
|
+
|
|
15
|
+
### Chat: Widget header
|
|
16
|
+
|
|
17
|
+
- New props on `TelaChatWidgetHeader`: `expandable`, `expanded`, and `closable` render the expand and minimize actions outside a floating window; `expandLabel`, `collapseLabel`, and `closeLabel` set their accessible names.
|
|
18
|
+
- New emits: `toggleExpanded` and `close`. Inside `TelaChatFloatingWindow` the header drives the window directly and the emits still fire. See [Chat → Widget Header & History](/templates/chat#widget-header--history).
|
|
@@ -1,21 +1,53 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
import { floatingWindowContextKey } from './floating/floating-window-context'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
3
5
|
title?: string
|
|
6
|
+
/** Show the expand toggle outside a floating window (inside one it is always shown). */
|
|
7
|
+
expandable?: boolean
|
|
8
|
+
/** Expanded state for the standalone toggle; ignored inside a floating window. */
|
|
9
|
+
expanded?: boolean
|
|
10
|
+
/** Show the minimize button outside a floating window (inside one it is always shown). */
|
|
11
|
+
closable?: boolean
|
|
4
12
|
newConversationLabel?: string
|
|
5
13
|
historyLabel?: string
|
|
14
|
+
expandLabel?: string
|
|
15
|
+
collapseLabel?: string
|
|
16
|
+
closeLabel?: string
|
|
6
17
|
}>(), {
|
|
7
18
|
newConversationLabel: 'New conversation',
|
|
8
19
|
historyLabel: 'Conversation history',
|
|
20
|
+
expandLabel: 'Expand',
|
|
21
|
+
collapseLabel: 'Collapse',
|
|
22
|
+
closeLabel: 'Close',
|
|
9
23
|
})
|
|
10
24
|
|
|
11
25
|
const emit = defineEmits<{
|
|
12
26
|
(e: 'newConversation'): void
|
|
13
27
|
(e: 'selectConversation', id: string): void
|
|
28
|
+
(e: 'close'): void
|
|
29
|
+
(e: 'toggleExpanded'): void
|
|
14
30
|
}>()
|
|
15
31
|
|
|
32
|
+
const floatingWindow = inject(floatingWindowContextKey, null)
|
|
33
|
+
|
|
34
|
+
const showExpand = computed(() => Boolean(floatingWindow) || props.expandable)
|
|
35
|
+
const showClose = computed(() => Boolean(floatingWindow) || props.closable)
|
|
36
|
+
const isExpanded = computed(() => floatingWindow ? floatingWindow.expanded.value : props.expanded)
|
|
37
|
+
|
|
16
38
|
const historyOpen = ref(false)
|
|
17
39
|
const historySelection = ref('')
|
|
18
40
|
|
|
41
|
+
function close() {
|
|
42
|
+
emit('close')
|
|
43
|
+
floatingWindow?.close()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toggleExpanded() {
|
|
47
|
+
emit('toggleExpanded')
|
|
48
|
+
floatingWindow?.toggleExpanded()
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
function selectConversation(id: unknown) {
|
|
20
52
|
if (typeof id !== 'string' || !id)
|
|
21
53
|
return
|
|
@@ -77,6 +109,22 @@ function selectConversation(id: unknown) {
|
|
|
77
109
|
<slot v-if="historyOpen" name="history" />
|
|
78
110
|
</TelaComboboxList>
|
|
79
111
|
</TelaComboboxRoot>
|
|
112
|
+
<TelaIconButton
|
|
113
|
+
v-if="showExpand"
|
|
114
|
+
:icon="isExpanded ? 'i-ph-arrows-in-simple' : 'i-ph-arrows-out-simple'"
|
|
115
|
+
size="sm"
|
|
116
|
+
color="secondary"
|
|
117
|
+
:aria-label="isExpanded ? collapseLabel : expandLabel"
|
|
118
|
+
@click.stop.prevent="toggleExpanded"
|
|
119
|
+
/>
|
|
120
|
+
<TelaIconButton
|
|
121
|
+
v-if="showClose"
|
|
122
|
+
icon="i-ph-minus"
|
|
123
|
+
size="sm"
|
|
124
|
+
color="secondary"
|
|
125
|
+
:aria-label="closeLabel"
|
|
126
|
+
@click.stop.prevent="close"
|
|
127
|
+
/>
|
|
80
128
|
</div>
|
|
81
129
|
</div>
|
|
82
130
|
</template>
|
|
@@ -136,6 +136,10 @@ function send() {
|
|
|
136
136
|
|
|
137
137
|
A launcher pill fixed to the bottom-right corner opens the chat in an anchored popover. The host controls `open`. While the window is open, page scroll is blocked.
|
|
138
138
|
|
|
139
|
+
The window opens over its own launcher: the surface springs out of the launcher's box (and shrinks back into it on close) while the conversation is revealed in place, pinned to the shared bottom-right corner. The launcher fades out while the window is open and returns as the surface settles. The scroll lock is released only after the exit finishes, so the scrollbar coming back never shifts the window mid-animation.
|
|
140
|
+
|
|
141
|
+
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).
|
|
142
|
+
|
|
139
143
|
```vue
|
|
140
144
|
<script setup lang="ts">
|
|
141
145
|
import { getMimeTypeIcon } from '@meistrari/tela-build/utils/files'
|
|
@@ -904,7 +908,7 @@ const statusLabel = computed(() => {
|
|
|
904
908
|
|
|
905
909
|
### Widget Header & History
|
|
906
910
|
|
|
907
|
-
`TelaChatWidgetHeader` is the widget top bar: optional title, new-conversation and history actions, close. `TelaChatWidgetHistoryList` fills its `#history` slot with a searchable conversation list. It must stay inside that slot, since the header owns the surrounding `TelaComboboxRoot` that emits the selection.
|
|
911
|
+
`TelaChatWidgetHeader` is the widget top bar: optional title, new-conversation and history actions, plus expand and minimize. Inside `TelaChatFloatingWindow` the expand and minimize buttons render automatically and drive the window; elsewhere opt in with `expandable` / `closable` and handle `toggleExpanded` / `close` yourself. `TelaChatWidgetHistoryList` fills its `#history` slot with a searchable conversation list. It must stay inside that slot, since the header owns the surrounding `TelaComboboxRoot` that emits the selection.
|
|
908
912
|
|
|
909
913
|
Click the clock icon to open the history panel.
|
|
910
914
|
|
|
@@ -917,12 +921,17 @@ const conversations = [
|
|
|
917
921
|
{ id: 'c4', title: 'Reasoning trace rendering' },
|
|
918
922
|
]
|
|
919
923
|
const selected = ref('c1')
|
|
924
|
+
const expanded = ref(false)
|
|
920
925
|
</script>
|
|
921
926
|
|
|
922
927
|
<template>
|
|
923
928
|
<TelaChatWidgetHeader
|
|
924
929
|
title="Release notes review"
|
|
925
|
-
class="w-
|
|
930
|
+
class="w-360px"
|
|
931
|
+
expandable
|
|
932
|
+
closable
|
|
933
|
+
:expanded="expanded"
|
|
934
|
+
@toggle-expanded="expanded = !expanded"
|
|
926
935
|
@select-conversation="selected = $event"
|
|
927
936
|
>
|
|
928
937
|
<template #history>
|
|
@@ -1026,7 +1035,7 @@ import { TelaChatIconsAnthropic, TelaChatIconsGemini, TelaChatIconsOpenai } from
|
|
|
1026
1035
|
| `TelaChatReasoningStep` | Collapsible reasoning/tool step row with status icon. |
|
|
1027
1036
|
| `TelaChatWidgetCard` | Card shell for tool-call widgets (title, icon, `TelaStatus`). |
|
|
1028
1037
|
| `TelaChatWidgetSources` / `TelaChatWidgetSource` | Reference rows cited by a tool widget (icon, title, domain, optional link). |
|
|
1029
|
-
| `TelaChatWidgetHeader` | Widget top bar: optional title, new-conversation and history actions,
|
|
1038
|
+
| `TelaChatWidgetHeader` | Widget top bar: optional title, new-conversation and history actions, plus expand and minimize (floating shell only). Marked with `data-widget-header`. |
|
|
1030
1039
|
| `TelaChatWidgetHistoryList` | Searchable conversation list for the header's `#history` slot. |
|
|
1031
1040
|
| `TelaChatFloatingWindowTrigger` | Pill button that opens the floating widget. |
|
|
1032
1041
|
| `TelaChatFloatingWindow` | Anchored popover shell for the floating variant (`open` / `@update:open`). |
|
|
@@ -1265,8 +1274,17 @@ type WidgetSourceProps = {
|
|
|
1265
1274
|
```typescript
|
|
1266
1275
|
type WidgetHeaderProps = {
|
|
1267
1276
|
title?: string
|
|
1277
|
+
expandable?: boolean // show the expand toggle outside a floating window
|
|
1278
|
+
expanded?: boolean // standalone toggle state; ignored inside a floating window
|
|
1279
|
+
closable?: boolean // show the minimize button outside a floating window
|
|
1280
|
+
newConversationLabel?: string // default: 'New conversation'
|
|
1281
|
+
historyLabel?: string // default: 'Conversation history'
|
|
1282
|
+
expandLabel?: string // default: 'Expand'
|
|
1283
|
+
collapseLabel?: string // default: 'Collapse'
|
|
1284
|
+
closeLabel?: string // default: 'Close'
|
|
1268
1285
|
}
|
|
1269
|
-
//
|
|
1286
|
+
// Inside TelaChatFloatingWindow the header also renders an expand toggle that grows the window up to 8px from the top of the viewport, and a minus button that closes the window (emits `close` and sets `open` to false).
|
|
1287
|
+
// emits: newConversation, selectConversation(id: string), toggleExpanded, close
|
|
1270
1288
|
|
|
1271
1289
|
type WidgetHistoryListProps = {
|
|
1272
1290
|
items: { id: string, title: string }[]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type FloatingWindowContext = {
|
|
4
|
+
expanded: Ref<boolean>
|
|
5
|
+
toggleExpanded: () => void
|
|
6
|
+
close: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const floatingWindowContextKey: InjectionKey<FloatingWindowContext> = Symbol('tela-chat-floating-window')
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { floatingWindowContextKey } from './floating-window-context'
|
|
3
|
+
|
|
2
4
|
const props = defineProps<{
|
|
3
5
|
open: boolean
|
|
4
6
|
/** Anchors the launcher to the nearest positioned ancestor instead of the viewport (docs/embedded previews). */
|
|
@@ -11,6 +13,88 @@ const emit = defineEmits<{
|
|
|
11
13
|
(e: 'update:open', value: boolean): void
|
|
12
14
|
}>()
|
|
13
15
|
|
|
16
|
+
const expanded = ref(false)
|
|
17
|
+
const launcher = ref<HTMLElement | null>(null)
|
|
18
|
+
const launcherWidth = ref(0)
|
|
19
|
+
const launcherHeight = ref(0)
|
|
20
|
+
const sideOffset = computed(() => -launcherHeight.value)
|
|
21
|
+
|
|
22
|
+
const morphTransition = { type: 'spring', stiffness: 420, damping: 42, mass: 1 } as const
|
|
23
|
+
|
|
24
|
+
const exitTransition = { type: 'spring', stiffness: 360, damping: 36, mass: 1 } as const
|
|
25
|
+
|
|
26
|
+
const launcherBox = computed(() => launcherWidth.value && launcherHeight.value
|
|
27
|
+
? { width: launcherWidth.value, height: launcherHeight.value, borderRadius: 8 }
|
|
28
|
+
: { width: '100%', height: '100%', borderRadius: 12 })
|
|
29
|
+
|
|
30
|
+
const shellProbe = ref<HTMLElement | null>(null)
|
|
31
|
+
|
|
32
|
+
const shellEl = computed(() => shellProbe.value?.parentElement ?? null)
|
|
33
|
+
|
|
34
|
+
const shellHeight = ref(0)
|
|
35
|
+
|
|
36
|
+
let shellObserver: ResizeObserver | undefined
|
|
37
|
+
|
|
38
|
+
watch(shellEl, (el) => {
|
|
39
|
+
shellObserver?.disconnect()
|
|
40
|
+
shellObserver = undefined
|
|
41
|
+
|
|
42
|
+
if (!el)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
shellHeight.value = el.offsetHeight
|
|
46
|
+
shellObserver = new ResizeObserver(() => {
|
|
47
|
+
shellHeight.value = el.offsetHeight
|
|
48
|
+
})
|
|
49
|
+
shellObserver.observe(el)
|
|
50
|
+
}, { flush: 'post' })
|
|
51
|
+
|
|
52
|
+
onBeforeUnmount(() => {
|
|
53
|
+
shellObserver?.disconnect()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const contentEnterScale = 1
|
|
57
|
+
const contentExitScale = 0.94
|
|
58
|
+
|
|
59
|
+
const surfaceAnimate = computed(() => props.open
|
|
60
|
+
? { width: '100%', height: '100%', borderRadius: 12, opacity: 1 }
|
|
61
|
+
: { ...launcherBox.value, opacity: 0 })
|
|
62
|
+
const surfaceTransition = computed(() => props.open
|
|
63
|
+
? morphTransition
|
|
64
|
+
: { ...exitTransition, opacity: { duration: 0.16, delay: 0.16 } })
|
|
65
|
+
const contentInitial = { scale: contentEnterScale, opacity: 0 }
|
|
66
|
+
const contentAnimate = computed(() => props.open
|
|
67
|
+
? { scale: 1, opacity: 1 }
|
|
68
|
+
: { scale: contentExitScale, opacity: 0 })
|
|
69
|
+
const contentTransition = computed(() => props.open
|
|
70
|
+
? { ...morphTransition, opacity: { duration: 0.16 } }
|
|
71
|
+
: { ...exitTransition, opacity: { duration: 0.14, delay: 0.08 } })
|
|
72
|
+
|
|
73
|
+
function measureLauncher() {
|
|
74
|
+
launcherWidth.value = launcher.value?.offsetWidth ?? 0
|
|
75
|
+
launcherHeight.value = launcher.value?.offsetHeight ?? 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
watch(() => props.open, (open) => {
|
|
79
|
+
if (open)
|
|
80
|
+
measureLauncher()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
onMounted(() => {
|
|
84
|
+
if (props.open)
|
|
85
|
+
measureLauncher()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
provide(floatingWindowContextKey, {
|
|
89
|
+
expanded,
|
|
90
|
+
toggleExpanded: () => {
|
|
91
|
+
expanded.value = !expanded.value
|
|
92
|
+
},
|
|
93
|
+
close: () => {
|
|
94
|
+
emit('update:open', false)
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
14
98
|
let previousOverflow = ''
|
|
15
99
|
let previousPaddingRight = ''
|
|
16
100
|
let hasScrollLock = false
|
|
@@ -42,17 +126,27 @@ function unlockScroll() {
|
|
|
42
126
|
hasScrollLock = false
|
|
43
127
|
}
|
|
44
128
|
|
|
129
|
+
const exitHoldMs = 380
|
|
130
|
+
|
|
131
|
+
let unlockTimer: ReturnType<typeof setTimeout> | undefined
|
|
132
|
+
|
|
45
133
|
watch(() => props.open, (open) => {
|
|
46
134
|
if (typeof document === 'undefined')
|
|
47
135
|
return
|
|
48
136
|
|
|
49
|
-
|
|
137
|
+
clearTimeout(unlockTimer)
|
|
138
|
+
|
|
139
|
+
if (open) {
|
|
50
140
|
lockScroll()
|
|
51
|
-
|
|
52
|
-
|
|
141
|
+
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
unlockTimer = setTimeout(unlockScroll, exitHoldMs)
|
|
53
146
|
}, { immediate: true })
|
|
54
147
|
|
|
55
148
|
onBeforeUnmount(() => {
|
|
149
|
+
clearTimeout(unlockTimer)
|
|
56
150
|
unlockScroll()
|
|
57
151
|
})
|
|
58
152
|
|
|
@@ -106,33 +200,62 @@ function focusEditor(event: Event) {
|
|
|
106
200
|
|
|
107
201
|
<template>
|
|
108
202
|
<TelaPopover :open="open" @update:open="emit('update:open', $event)">
|
|
109
|
-
<div :class="cn('bottom-20px', contained ? 'absolute right-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
203
|
+
<div ref="launcher" :class="cn('bottom-20px', contained ? 'absolute right-16px' : 'fixed right-[calc(16px+var(--scrollbar-width,0px))]')">
|
|
204
|
+
<div :class="cn('transition-opacity', open ? 'opacity-0 duration-100' : 'duration-160 delay-160')">
|
|
205
|
+
<TelaPopoverTrigger as-child>
|
|
206
|
+
<slot name="launcher">
|
|
207
|
+
<TelaChatFloatingWindowTrigger :open="open" :label="triggerLabel" />
|
|
208
|
+
</slot>
|
|
209
|
+
</TelaPopoverTrigger>
|
|
210
|
+
</div>
|
|
116
211
|
</div>
|
|
117
212
|
<TelaPopoverContent
|
|
118
213
|
side="top"
|
|
119
214
|
align="end"
|
|
120
|
-
:side-offset="
|
|
215
|
+
:side-offset="sideOffset"
|
|
121
216
|
:class="cn(
|
|
122
|
-
'widget-popover
|
|
123
|
-
|
|
217
|
+
'widget-popover relative w-[min(480px,calc(100vw-32px))] p-0 z-40! transition-[height] duration-200 ease-out',
|
|
218
|
+
expanded
|
|
219
|
+
? 'h-[calc(var(--reka-popover-content-available-height)-16px)]'
|
|
220
|
+
: 'h-[min(512px,calc(100dvh-120px))]',
|
|
221
|
+
'bg-transparent! border-0! shadow-none! rounded-none! overflow-visible!',
|
|
124
222
|
)"
|
|
125
223
|
@open-auto-focus="focusEditor"
|
|
126
224
|
@interact-outside="handleInteractOutside"
|
|
127
225
|
@focus-outside.prevent
|
|
128
226
|
>
|
|
129
|
-
<
|
|
227
|
+
<div ref="shellProbe" hidden />
|
|
228
|
+
<Motion
|
|
229
|
+
:initial="launcherBox"
|
|
230
|
+
:animate="surfaceAnimate"
|
|
231
|
+
:transition="surfaceTransition"
|
|
232
|
+
class="absolute bottom-0 right-0 w-full h-full overflow-hidden bg rounded-12px border-0.5px border-black/12 [box-shadow:-24px_-24px_249px_0_rgba(0,0,0,0.08),-12px_-8px_48px_0_rgba(0,0,0,0.03)]"
|
|
233
|
+
>
|
|
234
|
+
<Motion
|
|
235
|
+
:initial="contentInitial"
|
|
236
|
+
:animate="contentAnimate"
|
|
237
|
+
:transition="contentTransition"
|
|
238
|
+
:style="{ originX: 1, originY: 1, height: shellHeight ? `${shellHeight}px` : '100%' }"
|
|
239
|
+
class="absolute bottom-0 right-0 w-[min(480px,calc(100vw-32px))] flex flex-col"
|
|
240
|
+
>
|
|
241
|
+
<slot />
|
|
242
|
+
</Motion>
|
|
243
|
+
</Motion>
|
|
130
244
|
</TelaPopoverContent>
|
|
131
245
|
</TelaPopover>
|
|
132
246
|
</template>
|
|
133
247
|
|
|
134
248
|
<style>
|
|
135
|
-
.widget-popover {
|
|
136
|
-
|
|
249
|
+
.widget-popover.widget-popover[data-state='open'] {
|
|
250
|
+
animation: none;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.widget-popover.widget-popover[data-state='closed'] {
|
|
254
|
+
animation: widgetShellHoldOut 0.38s linear both;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@keyframes widgetShellHoldOut {
|
|
258
|
+
from { opacity: 1; }
|
|
259
|
+
to { opacity: 1; }
|
|
137
260
|
}
|
|
138
261
|
</style>
|
|
@@ -13,13 +13,13 @@ withDefaults(defineProps<{
|
|
|
13
13
|
max-w-prose
|
|
14
14
|
flex="~ col"
|
|
15
15
|
gap-8px
|
|
16
|
-
px-12px py-10px
|
|
16
|
+
px-12px py-10px text-start
|
|
17
17
|
bg-muted text-primary
|
|
18
18
|
rounded-12px
|
|
19
19
|
>
|
|
20
20
|
<slot />
|
|
21
21
|
</div>
|
|
22
|
-
<div v-else max-w-prose py-4px text-primary>
|
|
22
|
+
<div v-else max-w-prose py-4px text-primary text-start>
|
|
23
23
|
<slot />
|
|
24
24
|
</div>
|
|
25
25
|
</template>
|