@meistrari/tela-build 1.57.4 → 1.58.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.
Files changed (55) hide show
  1. package/components/tela/chat/chat-body.vue +107 -0
  2. package/components/tela/chat/chat-empty-state.vue +40 -0
  3. package/components/tela/chat/chat-model-selector.vue +64 -0
  4. package/components/tela/chat/chat-powered-by-tela.vue +8 -0
  5. package/components/tela/chat/chat-prompt-submit-button.vue +48 -0
  6. package/components/tela/chat/chat-reasoning-step.vue +109 -0
  7. package/components/tela/chat/chat-suggestion.vue +15 -0
  8. package/components/tela/chat/chat-suggestions.vue +5 -0
  9. package/components/tela/chat/chat-text-shimmer.vue +48 -0
  10. package/components/tela/chat/chat-widget-card.vue +62 -0
  11. package/components/tela/chat/chat-widget-header.vue +96 -0
  12. package/components/tela/chat/chat-widget-history-list.vue +63 -0
  13. package/components/tela/chat/chat-widget-source.vue +46 -0
  14. package/components/tela/chat/chat-widget-sources.vue +5 -0
  15. package/components/tela/chat/chat.mdx +1241 -182
  16. package/components/tela/chat/conversation/conversation-content.vue +5 -0
  17. package/components/tela/chat/conversation/conversation.vue +68 -0
  18. package/components/tela/chat/floating/floating-window-trigger.vue +21 -0
  19. package/components/tela/chat/floating/floating-window.vue +148 -0
  20. package/components/tela/chat/icons/anthropic.vue +5 -0
  21. package/components/tela/chat/icons/gemini.vue +3 -0
  22. package/components/tela/chat/icons/openai.vue +3 -0
  23. package/components/tela/chat/icons/tela-wordmark.vue +9 -0
  24. package/components/tela/chat/message/message-avatar.vue +15 -0
  25. package/components/tela/chat/message/message-content.vue +34 -0
  26. package/components/tela/chat/message/message-feedback.vue +275 -0
  27. package/components/tela/chat/message/message.vue +15 -0
  28. package/components/tela/chat/prompt-input/prompt-input-action-menu-content.vue +15 -0
  29. package/components/tela/chat/prompt-input/prompt-input-action-menu-item.vue +21 -0
  30. package/components/tela/chat/prompt-input/prompt-input-action-menu-sub-content.vue +5 -0
  31. package/components/tela/chat/prompt-input/prompt-input-action-menu-sub-trigger.vue +21 -0
  32. package/components/tela/chat/prompt-input/prompt-input-action-menu-sub.vue +5 -0
  33. package/components/tela/chat/prompt-input/prompt-input-action-menu-trigger.vue +24 -0
  34. package/components/tela/chat/prompt-input/prompt-input-action-menu.vue +9 -0
  35. package/components/tela/chat/prompt-input/prompt-input-actions.vue +5 -0
  36. package/components/tela/chat/prompt-input/prompt-input-attachment.vue +55 -0
  37. package/components/tela/chat/prompt-input/prompt-input-attachments.vue +27 -0
  38. package/components/tela/chat/prompt-input/prompt-input-hints.vue +53 -0
  39. package/components/tela/chat/prompt-input/prompt-input-textarea.vue +149 -0
  40. package/components/tela/chat/prompt-input/prompt-input-toolbar.vue +5 -0
  41. package/components/tela/chat/prompt-input/prompt-input-tools.vue +5 -0
  42. package/components/tela/chat/prompt-input/prompt-input.vue +59 -0
  43. package/components/tela/chat/pure-text-input/chat-text-input.vue +1 -2
  44. package/components/tela/chat/sidepanel/sidepanel-window.vue +7 -0
  45. package/components/tela/chat/text-input/index.vue +1 -2
  46. package/components/tela/chat/text-message/index.vue +2 -3
  47. package/components/tela/combobox/combobox.vue +2 -1
  48. package/components/tela/icon-button/icon-button.vue +1 -0
  49. package/components/tela/menubar/menubar-sub-trigger.vue +1 -1
  50. package/components/tela/scroll-area/scroll-area.vue +3 -1
  51. package/components/tela/skeleton/skeleton.vue +20 -29
  52. package/components/tela/tabs/tabs-trigger.vue +1 -1
  53. package/composables/scroll-fog.ts +48 -0
  54. package/lib/doc-generator.ts +8 -1
  55. package/package.json +1 -1
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ withDefaults(defineProps<{
3
+ /** Spacing scale: `widget` for floating/sidepanel shells, `page` for full-page chats. */
4
+ variant?: 'widget' | 'page'
5
+ }>(), {
6
+ variant: 'widget',
7
+ })
8
+
9
+ const rootRef = ref<HTMLElement | null>(null)
10
+ let inputObserver: ResizeObserver | null = null
11
+ let slotObserver: MutationObserver | null = null
12
+ let observedInput: HTMLElement | null = null
13
+
14
+ function bindInputObserver() {
15
+ const input = rootRef.value?.querySelector<HTMLElement>('[data-chat-prompt-input]') ?? null
16
+
17
+ if (input === observedInput)
18
+ return
19
+
20
+ if (observedInput)
21
+ inputObserver?.unobserve(observedInput)
22
+
23
+ observedInput = input
24
+
25
+ if (!input) {
26
+ rootRef.value?.style.removeProperty('--chat-input-height')
27
+ return
28
+ }
29
+
30
+ inputObserver ??= new ResizeObserver(() => {
31
+ if (observedInput)
32
+ rootRef.value?.style.setProperty('--chat-input-height', `${observedInput.offsetHeight}px`)
33
+ })
34
+ inputObserver.observe(input)
35
+ }
36
+
37
+ onMounted(() => {
38
+ if (!rootRef.value || typeof ResizeObserver === 'undefined')
39
+ return
40
+
41
+ bindInputObserver()
42
+
43
+ slotObserver = new MutationObserver(bindInputObserver)
44
+ slotObserver.observe(rootRef.value, { childList: true, subtree: true })
45
+ })
46
+
47
+ onBeforeUnmount(() => {
48
+ inputObserver?.disconnect()
49
+ inputObserver = null
50
+ slotObserver?.disconnect()
51
+ slotObserver = null
52
+ observedInput = null
53
+ })
54
+ </script>
55
+
56
+ <template>
57
+ <div ref="rootRef" data-chat-body :data-variant="variant" class="relative flex-1 min-h-0 flex flex-col">
58
+ <slot />
59
+ </div>
60
+ </template>
61
+
62
+ <style>
63
+ [data-chat-body] [data-chat-conversation] {
64
+ --at-apply: 'pl-18px pr-12px py-16px';
65
+ }
66
+
67
+ [data-chat-body]:has([data-chat-prompt-input]) [data-chat-conversation] {
68
+ padding-bottom: calc(var(--chat-input-height, 64px) + 24px);
69
+ }
70
+
71
+ [data-chat-body] [data-chat-prompt-input] {
72
+ --at-apply: 'absolute bottom-0 left-0 right-0 z-2 mx-12px mb-12px';
73
+ }
74
+
75
+ [data-chat-body]:has([data-chat-prompt-input]) [data-reka-scroll-area-viewport]:has([data-chat-conversation]),
76
+ [data-chat-body]:has([data-chat-prompt-input]) > [data-chat-conversation] {
77
+ animation: none;
78
+ mask: linear-gradient(
79
+ to bottom,
80
+ #000 calc(100% - var(--chat-input-height, 64px) - 40px),
81
+ transparent calc(100% - var(--chat-input-height, 64px))
82
+ );
83
+ -webkit-mask: linear-gradient(
84
+ to bottom,
85
+ #000 calc(100% - var(--chat-input-height, 64px) - 40px),
86
+ transparent calc(100% - var(--chat-input-height, 64px))
87
+ );
88
+ }
89
+
90
+ [data-chat-body]:has([data-chat-prompt-input]) .chat-conversation-scrollbar {
91
+ top: 8px !important;
92
+ bottom: calc(var(--chat-input-height, 64px) + 16px) !important;
93
+ height: auto !important;
94
+ }
95
+
96
+ [data-chat-body][data-variant='page'] [data-chat-conversation] {
97
+ --at-apply: 'pl-24px pr-16px py-16px';
98
+ }
99
+
100
+ [data-chat-body][data-variant='page']:has([data-chat-prompt-input]) [data-chat-conversation] {
101
+ padding-bottom: calc(var(--chat-input-height, 64px) + 32px);
102
+ }
103
+
104
+ [data-chat-body][data-variant='page'] [data-chat-prompt-input] {
105
+ --at-apply: 'mx-24px mb-16px';
106
+ }
107
+ </style>
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ /** Tighter spacing/typography for widget shells (floating/sidepanel). */
4
+ compact?: boolean
5
+ /** Greeting headline, e.g. "Hey, user". */
6
+ title?: string
7
+ /** Supporting line under the title. */
8
+ description?: string
9
+ }>()
10
+ </script>
11
+
12
+ <template>
13
+ <div
14
+ flex-1 flex="~ col" items-center justify-center
15
+ :class="compact ? 'gap-24px px-12px pt-16px' : 'gap-40px px-24px'"
16
+ >
17
+ <div flex="~ col" gap-32px :class="compact ? 'mt-auto' : ''">
18
+ <div v-if="title || description" flex="~ col" items-center gap-2px text-center>
19
+ <component
20
+ :is="compact ? 'h5' : 'h1'"
21
+ v-if="title"
22
+ text-primary
23
+ :class="compact ? 'heading-h4-semibold' : 'text-24px font-semibold leading-28px tracking-[-.8px]'"
24
+ >
25
+ {{ title }}
26
+ </component>
27
+ <p v-if="description" text-secondary :class="compact ? 'body-14-regular' : 'text-16px leading-20px tracking-[-.2px]'">
28
+ {{ description }}
29
+ </p>
30
+ </div>
31
+
32
+ <!-- Suggestions, hints, or any other content under the greeting. -->
33
+ <slot />
34
+ </div>
35
+
36
+ <div w-full max-w-700px mx-auto :class="compact ? 'mt-auto pb-12px' : ''">
37
+ <slot name="input" />
38
+ </div>
39
+ </div>
40
+ </template>
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import type { Component } from 'vue'
3
+
4
+ interface ModelOption {
5
+ value: string
6
+ label: string
7
+ description?: string
8
+ group?: string
9
+ icon?: string | Component
10
+ cost?: number
11
+ maxInputTokens?: number
12
+ maxOutputTokens?: number
13
+ isMultiModal?: boolean
14
+ tabs?: string[]
15
+ }
16
+
17
+ const props = withDefaults(defineProps<{
18
+ /** Model options, already resolved by the host (labels, provider icons, tabs...). */
19
+ options: ModelOption[]
20
+ disabled?: boolean
21
+ placeholder?: string
22
+ inputPlaceholder?: string
23
+ /** Maps tab keys (e.g. `advanced`) to display labels. */
24
+ tabLabels?: Record<string, string>
25
+ side?: 'top' | 'right' | 'bottom' | 'left'
26
+ align?: 'start' | 'center' | 'end'
27
+ }>(), {
28
+ placeholder: 'Auto',
29
+ inputPlaceholder: 'Search models...',
30
+ side: 'top',
31
+ align: 'end',
32
+ tabLabels: () => ({
33
+ all: 'All',
34
+ advanced: 'Advanced',
35
+ cheap: 'Economical',
36
+ multimodal: 'Multimodal',
37
+ }),
38
+ })
39
+
40
+ const model = defineModel<string | null>({ default: null })
41
+
42
+ function getTabLabel(tab: string) {
43
+ return props.tabLabels[tab] ?? tab
44
+ }
45
+ </script>
46
+
47
+ <template>
48
+ <TelaCombobox
49
+ :model-value="model ?? undefined"
50
+ :options="options"
51
+ :read-only="disabled"
52
+ :class="disabled && 'opacity-50 pointer-events-none'"
53
+ :placeholder="placeholder"
54
+ :side="side"
55
+ :align="align"
56
+ :label-icon-visible="false"
57
+ :input-placeholder="inputPlaceholder"
58
+ :get-tab-label="getTabLabel"
59
+ icon-class="scale-90"
60
+ content-class="w-[min(440px,calc(100vw-32px))]"
61
+ trigger-class="rounded-8px! gap-6px! bg-transparent! border-none! hover:bg-muted! shadow-none! duration-80 pl-10px! pr-8px! data-[state=open]:bg-muted!"
62
+ @update:model-value="model = $event ?? null"
63
+ />
64
+ </template>
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <div flex="~ col" gap-8px items-start>
3
+ <div body-9-medium text="#9DA2AA">
4
+ Powered by
5
+ </div>
6
+ <TelaChatIconsTelaWordmark h-12px text="#030C16" />
7
+ </div>
8
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ status?: 'ready' | 'uploading' | 'streaming'
4
+ disabled?: boolean
5
+ sendLabel?: string
6
+ uploadingLabel?: string
7
+ stopLabel?: string
8
+ }>(), {
9
+ status: 'ready',
10
+ sendLabel: 'Send message',
11
+ uploadingLabel: 'Uploading attachments',
12
+ stopLabel: 'Stop generating',
13
+ })
14
+
15
+ const emit = defineEmits<{
16
+ (e: 'send'): void
17
+ (e: 'stop'): void
18
+ }>()
19
+
20
+ const isDisabled = computed(() => props.disabled || props.status === 'uploading')
21
+ </script>
22
+
23
+ <template>
24
+ <TelaIconButton
25
+ v-if="status === 'streaming'"
26
+ size="sm"
27
+ icon="i-ph-stop-fill"
28
+ :aria-label="stopLabel"
29
+ @click="emit('stop')"
30
+ />
31
+ <TelaIconButton
32
+ v-else
33
+ :icon="status === 'uploading' ? 'i-ph-circle-notch' : 'i-ph-arrow-up-bold'"
34
+ size="sm"
35
+ :color="!isDisabled ? 'primary' : 'secondary'"
36
+ class="disabled:bg-muted!"
37
+ :class="cn(
38
+ isDisabled && 'bg-subtle! hover:bg-muted!',
39
+ )"
40
+ :icon-class="cn(
41
+ !isDisabled ? 'text-icon-reverse' : 'text-icon-tertiary',
42
+ status === 'uploading' && 'animate-spin',
43
+ )"
44
+ :disabled="isDisabled"
45
+ :aria-label="status === 'uploading' ? uploadingLabel : sendLabel"
46
+ @click="emit('send')"
47
+ />
48
+ </template>
@@ -0,0 +1,109 @@
1
+ <script setup lang="ts">
2
+ interface Props {
3
+ title: string
4
+ icon?: string
5
+ isLast?: boolean
6
+ isFirst?: boolean
7
+ defaultExpanded?: boolean
8
+ hasResult?: boolean
9
+ isError?: boolean
10
+ isTextStep?: boolean
11
+ hideExpand?: boolean
12
+ isActive?: boolean
13
+ completed?: boolean
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ icon: 'i-ph-info',
18
+ isLast: false,
19
+ isFirst: false,
20
+ defaultExpanded: false,
21
+ hasResult: false,
22
+ isError: false,
23
+ isTextStep: false,
24
+ hideExpand: false,
25
+ isActive: false,
26
+ completed: false,
27
+ })
28
+
29
+ // Check if waiting for next step (last step with result but reasoning not completed)
30
+ const isWaitingForNext = computed(() => {
31
+ return props.isLast && props.hasResult && !props.completed
32
+ })
33
+
34
+ const isExpanded = ref(props.defaultExpanded)
35
+
36
+ watch(() => props.defaultExpanded, (newVal) => {
37
+ isExpanded.value = newVal
38
+ })
39
+
40
+ const statusIconClass = computed(() => {
41
+ if (props.completed)
42
+ return props.isError ? 'i-ph-x-circle' : props.icon
43
+
44
+ if (props.isActive || isWaitingForNext.value || (!props.isTextStep && !props.hasResult))
45
+ return 'i-ph-circle-notch animate-spin'
46
+
47
+ if (props.isTextStep)
48
+ return 'i-ph-check-circle'
49
+
50
+ return props.isError ? 'i-ph-x-circle' : 'i-ph-check-circle'
51
+ })
52
+ </script>
53
+
54
+ <template>
55
+ <!-- Non-expandable row (when content is same as title) -->
56
+ <div
57
+ v-if="hideExpand"
58
+ relative flex items-center gap-10px h-28px
59
+ class="group/step"
60
+ >
61
+ <div class="flex items-center justify-center flex-shrink-0">
62
+ <div class="text-14px text-tertiary" :class="statusIconClass" />
63
+ </div>
64
+ <div class="flex-1 min-w-0 h-24px flex items-center">
65
+ <span class="text-14px font-medium text-tertiary leading-18px">
66
+ {{ title }}
67
+ </span>
68
+ </div>
69
+ </div>
70
+
71
+ <TelaCollapsible
72
+ v-else
73
+ :open="isExpanded"
74
+ @update:open="isExpanded = $event"
75
+ >
76
+ <TelaCollapsibleTrigger
77
+ as="button"
78
+ type="button"
79
+ relative flex items-center gap-10px h-32px w-full
80
+ class="group/step"
81
+ >
82
+ <div class="relative flex items-center justify-center flex-shrink-0">
83
+ <TelaIcon
84
+ size="16px"
85
+ color="icon-tertiary"
86
+ class="transition-opacity duration-80 group-hover/step:opacity-0"
87
+ :name="statusIconClass"
88
+ :class="[{ 'opacity-0': isExpanded }]"
89
+ />
90
+ <TelaIcon
91
+ name="i-ph-caret-right"
92
+ size="16px"
93
+ color="icon-tertiary"
94
+ class="absolute inset-0 m-auto opacity-0 group-hover/step:opacity-100 duration-80 [transition-property:transform,opacity]"
95
+ :class="{ 'rotate-90 opacity-100': isExpanded }"
96
+ />
97
+ </div>
98
+ <span body-14-medium :class="cn(isExpanded ? 'text-primary' : 'text-secondary group-hover/step:text-primary')">
99
+ {{ title }}
100
+ </span>
101
+ </TelaCollapsibleTrigger>
102
+
103
+ <TelaCollapsibleContent>
104
+ <div pl-26px pb-4px>
105
+ <slot />
106
+ </div>
107
+ </TelaCollapsibleContent>
108
+ </TelaCollapsible>
109
+ </template>
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ suggestion: string
4
+ }>()
5
+
6
+ const emit = defineEmits<{
7
+ (e: 'click', suggestion: string): void
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <TelaButton type="button" variant="secondary" @click="emit('click', suggestion)">
13
+ <slot>{{ suggestion }}</slot>
14
+ </TelaButton>
15
+ </template>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div flex flex-wrap items-center justify-center gap-8px mx-auto px-6px>
3
+ <slot />
4
+ </div>
5
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+
4
+ const props = withDefaults(defineProps<{
5
+ class?: HTMLAttributes['class']
6
+ duration?: number
7
+ }>(), {
8
+ duration: 2.8,
9
+ })
10
+
11
+ const baseColor = DT.colors.text.secondary
12
+ const shineColor = DT.colors.text.subtle
13
+ const spread = 20
14
+ </script>
15
+
16
+ <template>
17
+ <span
18
+ data-shimmer
19
+ :class="cn(
20
+ 'relative inline-block bg-[length:200%,auto] bg-clip-text text-transparent',
21
+ props.class,
22
+ )"
23
+ :style="{
24
+ '--base-color': baseColor,
25
+ '--base-gradient-color': shineColor,
26
+ 'backgroundImage': `linear-gradient(to right, var(--base-color) ${50 - spread}%, var(--base-gradient-color) 50%, var(--base-color) ${50 + spread}%)`,
27
+ 'animationDuration': `${props.duration}s`,
28
+ }"
29
+ >
30
+ <slot />
31
+ </span>
32
+ </template>
33
+
34
+ <style scoped>
35
+ [data-shimmer] {
36
+ animation: shimmer 4s infinite linear;
37
+ }
38
+
39
+ @keyframes shimmer {
40
+ 0% {
41
+ background-position: 200%;
42
+ }
43
+
44
+ 100% {
45
+ background-position: -200%;
46
+ }
47
+ }
48
+ </style>
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import type { VNode } from 'vue'
3
+ import type { TelaStatusVariant } from '@meistrari/tela-build/types'
4
+ import { Comment, Fragment, Text, useSlots } from 'vue'
5
+
6
+ defineProps<{
7
+ title: string
8
+ statusVariant: TelaStatusVariant
9
+ statusLabel: string
10
+ icon?: string
11
+ isIconAnimated?: boolean
12
+ }>()
13
+
14
+ const slots = useSlots()
15
+
16
+ function isRenderedNode(node: VNode): boolean {
17
+ if (node.type === Comment)
18
+ return false
19
+
20
+ if (node.type === Text)
21
+ return typeof node.children === 'string' && node.children.trim().length > 0
22
+
23
+ if (node.type === Fragment)
24
+ return Array.isArray(node.children) && (node.children as VNode[]).some(isRenderedNode)
25
+
26
+ return true
27
+ }
28
+
29
+ function hasContent(): boolean {
30
+ return (slots.default?.() ?? []).some(isRenderedNode)
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <div border-0.5px border p-12px rounded-12px>
36
+ <!-- Header -->
37
+ <div flex items-center justify-between>
38
+ <div flex items-center gap-8px flex-1 min-w-0>
39
+ <div v-if="icon" size-24px rounded-7px flex items-center justify-center bg-lowered>
40
+ <TelaIcon :name="icon" size="16px" color="icon-secondary" />
41
+ </div>
42
+ <div v-if="$slots.subtitle" class="min-w-0">
43
+ <p body-14-medium text-primary>
44
+ {{ title }}
45
+ </p>
46
+ <p body-12-regular text-secondary>
47
+ <slot name="subtitle" />
48
+ </p>
49
+ </div>
50
+ <h3 v-else body-14-medium text-primary truncate>
51
+ {{ title }}
52
+ </h3>
53
+ </div>
54
+ <TelaStatus :variant="statusVariant" :label="statusLabel" :is-icon-animated="isIconAnimated" />
55
+ </div>
56
+
57
+ <!-- Content -->
58
+ <div v-if="hasContent()" flex="~ col" gap-16px mt-12px>
59
+ <slot />
60
+ </div>
61
+ </div>
62
+ </template>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ withDefaults(defineProps<{
3
+ title?: string
4
+ newConversationLabel?: string
5
+ historyLabel?: string
6
+ closeLabel?: string
7
+ }>(), {
8
+ newConversationLabel: 'New conversation',
9
+ historyLabel: 'Conversation history',
10
+ closeLabel: 'Close',
11
+ })
12
+
13
+ const emit = defineEmits<{
14
+ (e: 'newConversation'): void
15
+ (e: 'selectConversation', id: string): void
16
+ (e: 'close'): void
17
+ }>()
18
+
19
+ const historyOpen = ref(false)
20
+ const historySelection = ref('')
21
+
22
+ const instance = getCurrentInstance()
23
+ const hasCloseListener = computed(() => Boolean(instance?.vnode.props?.onClose))
24
+
25
+ function selectConversation(id: unknown) {
26
+ if (typeof id !== 'string' || !id)
27
+ return
28
+
29
+ historyOpen.value = false
30
+ emit('selectConversation', id)
31
+
32
+ nextTick(() => {
33
+ historySelection.value = ''
34
+ })
35
+ }
36
+ </script>
37
+
38
+ <template>
39
+ <div
40
+ data-widget-header
41
+ flex items-center justify-between gap-8px
42
+ h-48px pl-18px pr-10px flex-shrink-0
43
+ bg border-b-0.5px border
44
+ >
45
+ <h5 v-if="title" heading-h5-semibold text-primary truncate>
46
+ {{ title }}
47
+ </h5>
48
+
49
+ <div flex items-center gap-4px ml-auto>
50
+ <TelaIconButton
51
+ icon="i-ph-plus"
52
+ size="sm"
53
+ color="secondary"
54
+ :aria-label="newConversationLabel"
55
+ @click="emit('newConversation')"
56
+ />
57
+ <TelaComboboxRoot
58
+ v-if="$slots.history"
59
+ v-model="historySelection"
60
+ v-model:open="historyOpen"
61
+ reset-search-term-on-select
62
+ @update:model-value="selectConversation"
63
+ >
64
+ <TelaComboboxAnchor>
65
+ <TelaComboboxTrigger as-child class="group">
66
+ <TelaIconButton
67
+ icon="i-ph-clock"
68
+ size="sm"
69
+ color="secondary"
70
+ icon-class="group-data-[state=open]:text-icon"
71
+ :aria-label="historyLabel"
72
+ :class="historyOpen && 'bg-muted!'"
73
+ />
74
+ </TelaComboboxTrigger>
75
+ </TelaComboboxAnchor>
76
+ <TelaComboboxList
77
+ side="bottom"
78
+ align="end"
79
+ :side-offset="4"
80
+ class="w-300px no-scrollbar"
81
+ >
82
+ <!-- Host supplies the history content (e.g. ChatWidgetHistoryList). -->
83
+ <slot v-if="historyOpen" name="history" />
84
+ </TelaComboboxList>
85
+ </TelaComboboxRoot>
86
+ <TelaIconButton
87
+ v-if="hasCloseListener"
88
+ icon="i-ph-x"
89
+ size="sm"
90
+ color="secondary"
91
+ :aria-label="closeLabel"
92
+ @click="emit('close')"
93
+ />
94
+ </div>
95
+ </div>
96
+ </template>
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ /** Conversations to list. Selection is emitted by the surrounding `TelaComboboxRoot`. */
4
+ items: { id: string, title: string }[]
5
+ loading?: boolean
6
+ searchPlaceholder?: string
7
+ emptyMessage?: string
8
+ emptySearchMessage?: string
9
+ }>(), {
10
+ searchPlaceholder: 'Search conversations',
11
+ emptyMessage: 'No conversations yet',
12
+ emptySearchMessage: 'No conversations found',
13
+ })
14
+
15
+ const skeletonRows = [
16
+ { id: 'row-1', width: '72%' },
17
+ { id: 'row-2', width: '54%' },
18
+ { id: 'row-3', width: '64%' },
19
+ { id: 'row-4', width: '46%' },
20
+ { id: 'row-5', width: '78%' },
21
+ ]
22
+
23
+ const searchQuery = ref('')
24
+
25
+ const showSkeleton = computed(() => props.loading && props.items.length === 0)
26
+ const resolvedEmptyMessage = computed(() =>
27
+ searchQuery.value.trim() ? props.emptySearchMessage : props.emptyMessage,
28
+ )
29
+ </script>
30
+
31
+ <template>
32
+ <TelaComboboxInput
33
+ v-model="searchQuery"
34
+ auto-focus
35
+ :placeholder="searchPlaceholder"
36
+ />
37
+
38
+ <div flex flex-col gap-2px p-4px max-h-280px overflow-y-auto>
39
+ <template v-if="showSkeleton">
40
+ <div
41
+ v-for="row in skeletonRows"
42
+ :key="row.id"
43
+ flex items-center h-32px px-8px
44
+ >
45
+ <TelaSkeleton h-14px rounded-4px :style="{ width: row.width }" />
46
+ </div>
47
+ </template>
48
+
49
+ <template v-else>
50
+ <TelaComboboxEmpty class="body-14-regular! text-center text-tertiary py-8px">
51
+ {{ resolvedEmptyMessage }}
52
+ </TelaComboboxEmpty>
53
+ <TelaComboboxItem
54
+ v-for="item in items"
55
+ :key="item.id"
56
+ :value="item.id"
57
+ :text-value="item.title"
58
+ >
59
+ <span truncate>{{ item.title }}</span>
60
+ </TelaComboboxItem>
61
+ </template>
62
+ </div>
63
+ </template>