@meistrari/tela-build 1.70.5 → 1.72.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 +38 -0
- package/components/tela/button/button.mdx +18 -0
- package/components/tela/button/button.vue +34 -20
- package/components/tela/chat/chat-animate-enter.vue +24 -0
- package/components/tela/chat/chat-body.vue +21 -3
- package/components/tela/chat/chat-stop-run-button.vue +31 -0
- package/components/tela/chat/chat-widget-header.vue +49 -1
- package/components/tela/chat/chat-working-indicator.vue +84 -0
- package/components/tela/chat/chat.mdx +213 -67
- 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/components/tela/chat/message/message.vue +1 -1
- package/components/tela/chat/prompt-input/prompt-input.vue +4 -9
- package/components/tela/loaders/splash-screen/splash-screen.mdx +45 -0
- package/components/tela/loaders/{splash-screen.vue → splash-screen/splash-screen.vue} +1 -22
- package/components/tela/loaders/square/square.mdx +47 -0
- package/components/tela/loaders/square/square.vue +55 -0
- package/components/tela/loaders/welcome-screen/welcome-screen.mdx +54 -0
- package/package.json +2 -1
- /package/components/tela/loaders/{welcome-screen.vue → welcome-screen/welcome-screen.vue} +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Latest updates and announcements.
|
|
4
|
+
|
|
5
|
+
## September 16, 2026
|
|
6
|
+
|
|
7
|
+
### Square Loader
|
|
8
|
+
|
|
9
|
+
- **New `TelaLoadersSquare`.** The animated square from the splash screen is now its own component, with `size="md"` (32×32) and `size="sm"` (12×12). `TelaLoadersSplashScreen` and `TelaChatWorkingIndicator` render it. See [Square Loader](/components/square).
|
|
10
|
+
|
|
11
|
+
### Button
|
|
12
|
+
|
|
13
|
+
- **`size="sm"` icons are now 10px.** The icon size lookup compared against the size's class string instead of the size name, so every size rendered the 13px default. `md` and `lg` are unchanged. See [Button](/components/button).
|
|
14
|
+
- **`size="lg"` is slightly tighter.** Vertical padding goes from 10px to 8px and the radius from 13px to 12px.
|
|
15
|
+
- **Icon gap is now 4px.** Icons get a 2px padded wrapper, so the gap between icon and label drops from 6px to 4px.
|
|
16
|
+
- **Icons sit on the cap line.** Leading and trailing icons (and the loading spinner) are lifted half a pixel so they center on the capital letters instead of rendering a device pixel low on 2x screens.
|
|
17
|
+
|
|
18
|
+
### Chat: Working indicator
|
|
19
|
+
|
|
20
|
+
- **New `TelaChatWorkingIndicator`.** Running-turn row for the conversation: `TelaLoadersSquare` (`sm`), a `label` (`Working` by default), and an elapsed counter driven by `startedAt` or `elapsed`. Render it whenever a turn is running; the host composes anything else (such as `TelaChatTextShimmer`) around it. See [Chat → Working Indicator](/templates/chat#working-indicator).
|
|
21
|
+
- **New `TelaChatStopRunButton`.** While a turn runs, render it in place of `TelaChatPromptInput`: a centered secondary pill with a stop icon that emits `stop`. Wrap it and `TelaChatPromptInput` in the new `TelaChatAnimateEnter` inside an `<AnimatePresence mode="popLayout">` to cross-fade them.
|
|
22
|
+
- **New `TelaChatAnimateEnter`.** Enter/exit preset (fade, slide, slight scale) for the prompt input and stop button; `TelaChatBody` docks the wrapper when it holds a docked surface. Props: `offset`, `scale`, `duration`. It reuses the prompt input's dock, so the conversation padding and scrollbar inset stay correct. See [Chat → Stop Run](/templates/chat#stop-run).
|
|
23
|
+
- **`TelaChatMessage` stacks its children with an 8px gap.** A running turn can hold the indicator plus a `TelaChatTextShimmer` or reasoning trace in one message; single-child messages are unaffected.
|
|
24
|
+
|
|
25
|
+
## September 15, 2026
|
|
26
|
+
|
|
27
|
+
### Chat: Floating window
|
|
28
|
+
|
|
29
|
+
- **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).
|
|
30
|
+
- **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.
|
|
31
|
+
- **Minimize.** The widget header shows a minus button inside the floating window that closes it. `TelaChatWidgetHeader` also emits `close`.
|
|
32
|
+
- **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.
|
|
33
|
+
- **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.
|
|
34
|
+
|
|
35
|
+
### Chat: Widget header
|
|
36
|
+
|
|
37
|
+
- 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.
|
|
38
|
+
- 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).
|
|
@@ -67,6 +67,24 @@ A versatile button component with multiple variants, sizes, and states. Supports
|
|
|
67
67
|
</TelaButton>
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
### Sizes
|
|
71
|
+
|
|
72
|
+
Three sizes, `md` by default. Each one sets its own text, padding, radius and icon size, so a leading or trailing icon scales with the button.
|
|
73
|
+
|
|
74
|
+
```vue
|
|
75
|
+
<div class="flex items-center gap-12px">
|
|
76
|
+
<TelaButton size="sm" icon="i-ph-plus" leading>Small</TelaButton>
|
|
77
|
+
<TelaButton size="md" icon="i-ph-plus" leading>Medium</TelaButton>
|
|
78
|
+
<TelaButton size="lg" icon="i-ph-plus" leading>Large</TelaButton>
|
|
79
|
+
</div>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| size | text | padding | radius | icon |
|
|
83
|
+
|---|---|---|---|---|
|
|
84
|
+
| `sm` | 12px / 16px | 4px × 10px | 9px | 10px |
|
|
85
|
+
| `md` | 14px / 18px | 7px × 12px | 11px | 13px |
|
|
86
|
+
| `lg` | 16px / 20px | 8px × 16px | 12px | 13px |
|
|
87
|
+
|
|
70
88
|
### With Icon Code
|
|
71
89
|
|
|
72
90
|
```vue
|
|
@@ -37,7 +37,7 @@ const hasTrailingIcon = computed(() => !!((props.loading && !props.leading) || s
|
|
|
37
37
|
const sizeStyle = computed(() => (({
|
|
38
38
|
sm: '[--padding-x:10px] text-[12px] leading-[16px] tracking-normal py-[4px] rounded-[9px]',
|
|
39
39
|
md: '[--padding-x:12px] text-[14px] leading-[18px] -tracking-[0.15px] py-[7px] rounded-[11px]',
|
|
40
|
-
lg: '[--padding-x:16px] text-[16px] leading-[20px] -tracking-[0.15px] py-[
|
|
40
|
+
lg: '[--padding-x:16px] text-[16px] leading-[20px] -tracking-[0.15px] py-[8px] rounded-[12px]',
|
|
41
41
|
}) as Record<ButtonSize, string>)[resolvedSize.value] ?? '')
|
|
42
42
|
|
|
43
43
|
const paddingStyle = computed(() => [
|
|
@@ -64,20 +64,11 @@ const resolvedStyle = computed(() => [
|
|
|
64
64
|
|
|
65
65
|
const tag = computed(() => props.to ? NuxtLink : 'button')
|
|
66
66
|
|
|
67
|
-
const iconSize = computed(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return '11px'
|
|
73
|
-
case 'md':
|
|
74
|
-
return '13px'
|
|
75
|
-
case 'lg':
|
|
76
|
-
return '13px'
|
|
77
|
-
default:
|
|
78
|
-
return '13px'
|
|
79
|
-
}
|
|
80
|
-
})
|
|
67
|
+
const iconSize = computed(() => (({
|
|
68
|
+
sm: '10px',
|
|
69
|
+
md: '13px',
|
|
70
|
+
lg: '13px',
|
|
71
|
+
}) as Record<ButtonSize, string>)[resolvedSize.value] ?? '13px')
|
|
81
72
|
</script>
|
|
82
73
|
|
|
83
74
|
<template>
|
|
@@ -85,19 +76,42 @@ const iconSize = computed(() => {
|
|
|
85
76
|
:is="tag"
|
|
86
77
|
:to="to"
|
|
87
78
|
:class="cn(
|
|
88
|
-
'box-border font-540 select-none flex items-center justify-center gap-
|
|
79
|
+
'box-border font-540 select-none flex items-center justify-center gap-4px ease-out duration-40',
|
|
89
80
|
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
90
81
|
resolvedStyle,
|
|
91
82
|
)"
|
|
92
83
|
:disabled="disabled"
|
|
93
84
|
:target="target"
|
|
94
85
|
>
|
|
95
|
-
<
|
|
86
|
+
<div v-if="loading && leading" data-button-icon class="p-2px flex items-center justify-center">
|
|
87
|
+
<TelaIconSpinner class="animate-spin" />
|
|
88
|
+
</div>
|
|
89
|
+
|
|
96
90
|
<slot v-else-if="$slots.leading" name="leading" />
|
|
97
|
-
|
|
91
|
+
|
|
92
|
+
<div v-else-if="icon && leading" data-button-icon class="p-2px flex items-center justify-center">
|
|
93
|
+
<TelaIcon :name="icon" :size="iconSize" :color="iconColorStyle" />
|
|
94
|
+
</div>
|
|
95
|
+
|
|
98
96
|
<slot />
|
|
99
|
-
|
|
97
|
+
|
|
98
|
+
<div v-if="loading && !leading" data-button-icon class="p-2px flex items-center justify-center">
|
|
99
|
+
<TelaIconSpinner class="animate-spin" />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
100
102
|
<slot v-else-if="$slots.trailing" name="trailing" />
|
|
101
|
-
|
|
103
|
+
|
|
104
|
+
<div v-else-if="icon && !leading" data-button-icon class="p-2px flex items-center justify-center">
|
|
105
|
+
<TelaIcon :name="icon" :size="iconSize" :color="iconColorStyle" />
|
|
106
|
+
</div>
|
|
102
107
|
</component>
|
|
103
108
|
</template>
|
|
109
|
+
|
|
110
|
+
<style scoped>
|
|
111
|
+
/* Optical alignment */
|
|
112
|
+
@media (min-resolution: 1.5dppx) {
|
|
113
|
+
[data-button-icon] {
|
|
114
|
+
transform: translateY(-0.5px);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(defineProps<{
|
|
3
|
+
/** Slide distance on enter/exit, as a CSS length or percentage of the element. */
|
|
4
|
+
offset?: string | number
|
|
5
|
+
scale?: number
|
|
6
|
+
duration?: number
|
|
7
|
+
}>(), {
|
|
8
|
+
offset: '100%',
|
|
9
|
+
scale: 0.97,
|
|
10
|
+
duration: 0.16,
|
|
11
|
+
})
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<Motion
|
|
16
|
+
data-chat-animate-enter
|
|
17
|
+
:initial="{ opacity: 0, y: offset, scale }"
|
|
18
|
+
:animate="{ opacity: 1, y: 0, scale: 1 }"
|
|
19
|
+
:exit="{ opacity: 0, y: offset, scale }"
|
|
20
|
+
:transition="{ duration, ease: 'easeOut' }"
|
|
21
|
+
>
|
|
22
|
+
<slot />
|
|
23
|
+
</Motion>
|
|
24
|
+
</template>
|
|
@@ -7,12 +7,16 @@ withDefaults(defineProps<{
|
|
|
7
7
|
})
|
|
8
8
|
|
|
9
9
|
const rootRef = ref<HTMLElement | null>(null)
|
|
10
|
+
|
|
11
|
+
// The docked element is the prompt-input surface, or the TelaChatAnimateEnter
|
|
12
|
+
// wrapping it (document order puts the wrapper first).
|
|
13
|
+
const DOCK_SELECTOR = '[data-chat-animate-enter]:has([data-chat-prompt-input]), [data-chat-prompt-input]'
|
|
10
14
|
let inputObserver: ResizeObserver | null = null
|
|
11
15
|
let slotObserver: MutationObserver | null = null
|
|
12
16
|
let observedInput: HTMLElement | null = null
|
|
13
17
|
|
|
14
18
|
function bindInputObserver() {
|
|
15
|
-
const input = rootRef.value?.querySelector<HTMLElement>(
|
|
19
|
+
const input = rootRef.value?.querySelector<HTMLElement>(DOCK_SELECTOR) ?? null
|
|
16
20
|
|
|
17
21
|
if (input === observedInput)
|
|
18
22
|
return
|
|
@@ -68,10 +72,24 @@ onBeforeUnmount(() => {
|
|
|
68
72
|
padding-bottom: calc(var(--chat-input-height, 64px) + 24px);
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
|
|
75
|
+
/*
|
|
76
|
+
* The dock: the prompt-input surface itself, or the TelaChatAnimateEnter that
|
|
77
|
+
* wraps it. A surface inside the wrapper stays in flow so only one box is
|
|
78
|
+
* positioned, faded and (with popLayout) pinned.
|
|
79
|
+
*/
|
|
80
|
+
[data-chat-body] :is([data-chat-prompt-input]:not([data-chat-animate-enter] *), [data-chat-animate-enter]:has([data-chat-prompt-input])) {
|
|
72
81
|
--at-apply: 'absolute bottom-0 left-0 right-0 z-2 mx-12px mb-12px';
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
/*
|
|
85
|
+
* While AnimatePresence (popLayout) holds an exiting dock element it pins it
|
|
86
|
+
* with `left`/`top` measured from the offsets, which already include the side
|
|
87
|
+
* margin; keeping the margin on top of that shifts it sideways mid-fade.
|
|
88
|
+
*/
|
|
89
|
+
[data-chat-body] :is([data-chat-prompt-input], [data-chat-animate-enter])[data-motion-pop-id] {
|
|
90
|
+
margin: 0 !important;
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
[data-chat-body]:has([data-chat-prompt-input]) .chat-conversation-scrollbar {
|
|
76
94
|
top: 8px !important;
|
|
77
95
|
bottom: calc(var(--chat-input-height, 64px) + 16px) !important;
|
|
@@ -86,7 +104,7 @@ onBeforeUnmount(() => {
|
|
|
86
104
|
padding-bottom: calc(var(--chat-input-height, 64px) + 32px);
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
[data-chat-body][data-variant='page'] [data-chat-prompt-input] {
|
|
107
|
+
[data-chat-body][data-variant='page'] :is([data-chat-prompt-input]:not([data-chat-animate-enter] *), [data-chat-animate-enter]:has([data-chat-prompt-input])) {
|
|
90
108
|
--at-apply: 'mx-24px mb-16px';
|
|
91
109
|
}
|
|
92
110
|
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
|
+
class?: HTMLAttributes['class']
|
|
6
|
+
label?: string
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
}>(), {
|
|
9
|
+
label: 'Stop Run',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
(e: 'stop'): void
|
|
14
|
+
}>()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div data-chat-prompt-input :class="cn('flex justify-center', props.class)">
|
|
19
|
+
<TelaButton
|
|
20
|
+
type="button"
|
|
21
|
+
variant="secondary"
|
|
22
|
+
size="sm"
|
|
23
|
+
leading
|
|
24
|
+
icon="i-ph-square-bold"
|
|
25
|
+
:disabled="props.disabled"
|
|
26
|
+
@click="emit('stop')"
|
|
27
|
+
>
|
|
28
|
+
{{ props.label }}
|
|
29
|
+
</TelaButton>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
@@ -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>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
|
+
class?: HTMLAttributes['class']
|
|
6
|
+
/** Row label next to the loader. */
|
|
7
|
+
label?: string
|
|
8
|
+
/** Epoch ms of when work started; the elapsed counter ticks from it every second. */
|
|
9
|
+
startedAt?: number
|
|
10
|
+
/** Elapsed seconds controlled by the host. Takes precedence over `startedAt`. */
|
|
11
|
+
elapsed?: number
|
|
12
|
+
}>(), {
|
|
13
|
+
label: 'Working',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const now = ref(Date.now())
|
|
17
|
+
let timer: ReturnType<typeof setInterval> | null = null
|
|
18
|
+
|
|
19
|
+
function stopTicking() {
|
|
20
|
+
if (timer)
|
|
21
|
+
clearInterval(timer)
|
|
22
|
+
timer = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function syncTicking() {
|
|
26
|
+
stopTicking()
|
|
27
|
+
|
|
28
|
+
if (props.elapsed !== undefined || props.startedAt === undefined)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
now.value = Date.now()
|
|
32
|
+
|
|
33
|
+
timer = setInterval(() => {
|
|
34
|
+
now.value = Date.now()
|
|
35
|
+
}, 1000)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onMounted(() => {
|
|
39
|
+
syncTicking()
|
|
40
|
+
watch(() => [props.startedAt, props.elapsed], syncTicking)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
onUnmounted(stopTicking)
|
|
44
|
+
|
|
45
|
+
const elapsedSeconds = computed(() => {
|
|
46
|
+
if (props.elapsed !== undefined)
|
|
47
|
+
return Math.max(0, Math.floor(props.elapsed))
|
|
48
|
+
|
|
49
|
+
if (props.startedAt === undefined)
|
|
50
|
+
return undefined
|
|
51
|
+
|
|
52
|
+
return Math.max(0, Math.floor((now.value - props.startedAt) / 1000))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const elapsedLabel = computed(() => {
|
|
56
|
+
const seconds = elapsedSeconds.value
|
|
57
|
+
|
|
58
|
+
if (seconds === undefined)
|
|
59
|
+
return undefined
|
|
60
|
+
|
|
61
|
+
if (seconds < 60)
|
|
62
|
+
return `${seconds}s`
|
|
63
|
+
|
|
64
|
+
const minutes = Math.floor(seconds / 60)
|
|
65
|
+
|
|
66
|
+
return `${minutes}m ${seconds % 60}s`
|
|
67
|
+
})
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<div
|
|
72
|
+
:class="cn('flex items-center gap-10px text-start', props.class)"
|
|
73
|
+
role="status"
|
|
74
|
+
aria-live="polite"
|
|
75
|
+
>
|
|
76
|
+
<TelaLoadersSquare size="sm" class="shrink-0" />
|
|
77
|
+
<div class="flex items-center gap-6px">
|
|
78
|
+
<h5 class="heading-h5-semibold text-primary">
|
|
79
|
+
{{ props.label }}
|
|
80
|
+
</h5>
|
|
81
|
+
<span v-if="elapsedLabel" aria-hidden="true" class="body-14-regular text-tertiary tabular-nums">{{ elapsedLabel }}</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
@@ -24,6 +24,7 @@ const messages = ref<ChatMessage[]>([
|
|
|
24
24
|
{ id: '2', role: 'assistant', text: 'The chat primitives landed: a conversation viewport, a prompt input, and three shells you can compose freely.' },
|
|
25
25
|
])
|
|
26
26
|
const status = ref<'ready' | 'uploading' | 'streaming'>('ready')
|
|
27
|
+
const startedAt = ref(Date.now())
|
|
27
28
|
const draft = ref('')
|
|
28
29
|
const recentFiles = [
|
|
29
30
|
{ id: 'f1', label: 'release-notes.pdf', type: 'application/pdf' },
|
|
@@ -48,6 +49,8 @@ function onInput(event: Event) {
|
|
|
48
49
|
|
|
49
50
|
function send() {
|
|
50
51
|
// append the user message, call your backend, stream the reply...
|
|
52
|
+
draft.value = ''
|
|
53
|
+
startedAt.value = Date.now()
|
|
51
54
|
conversationRef.value?.scrollToBottom()
|
|
52
55
|
}
|
|
53
56
|
</script>
|
|
@@ -63,70 +66,76 @@ function send() {
|
|
|
63
66
|
</TelaChatMessageContent>
|
|
64
67
|
</TelaChatMessage>
|
|
65
68
|
|
|
66
|
-
<
|
|
67
|
-
Thinking...
|
|
68
|
-
</TelaChatTextShimmer>
|
|
69
|
+
<TelaChatWorkingIndicator v-if="status === 'streaming'" :started-at="startedAt" />
|
|
69
70
|
</TelaChatConversationContent>
|
|
70
71
|
</TelaChatConversation>
|
|
71
72
|
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<TelaChatPromptInputActionMenuItem icon="i-ph-paperclip">
|
|
85
|
-
Add files or photos
|
|
86
|
-
</TelaChatPromptInputActionMenuItem>
|
|
87
|
-
<TelaChatPromptInputActionMenuSub>
|
|
88
|
-
<TelaChatPromptInputActionMenuSubTrigger icon="i-ph-clock-counter-clockwise">
|
|
89
|
-
Recent files
|
|
90
|
-
</TelaChatPromptInputActionMenuSubTrigger>
|
|
91
|
-
<TelaChatPromptInputActionMenuSubContent>
|
|
92
|
-
<TelaChatPromptInputActionMenuItem
|
|
93
|
-
v-for="file in recentFiles"
|
|
94
|
-
:key="file.id"
|
|
95
|
-
>
|
|
96
|
-
<img :src="getMimeTypeIcon(file.type)" class="w-16px h-16px mr-8px shrink-0" alt="">
|
|
97
|
-
<span class="min-w-0 max-w-200px truncate">{{ file.label }}</span>
|
|
98
|
-
</TelaChatPromptInputActionMenuItem>
|
|
99
|
-
</TelaChatPromptInputActionMenuSubContent>
|
|
100
|
-
</TelaChatPromptInputActionMenuSub>
|
|
101
|
-
<TelaMenubarSeparator />
|
|
102
|
-
<TelaChatPromptInputActionMenuSub>
|
|
103
|
-
<TelaChatPromptInputActionMenuSubTrigger icon="i-ph-notepad" icon-size="16px">
|
|
104
|
-
Skills
|
|
105
|
-
</TelaChatPromptInputActionMenuSubTrigger>
|
|
106
|
-
<TelaChatPromptInputActionMenuSubContent>
|
|
107
|
-
<TelaChatPromptInputActionMenuItem
|
|
108
|
-
v-for="skill in skills"
|
|
109
|
-
:key="skill.id"
|
|
110
|
-
@click="draft = skill.label"
|
|
111
|
-
>
|
|
112
|
-
<span class="min-w-0 max-w-200px truncate">{{ skill.label }}</span>
|
|
113
|
-
</TelaChatPromptInputActionMenuItem>
|
|
114
|
-
</TelaChatPromptInputActionMenuSubContent>
|
|
115
|
-
</TelaChatPromptInputActionMenuSub>
|
|
116
|
-
</TelaChatPromptInputActionMenuContent>
|
|
117
|
-
</TelaChatPromptInputActionMenu>
|
|
118
|
-
</TelaChatPromptInputTools>
|
|
119
|
-
<TelaChatPromptInputActions>
|
|
120
|
-
<TelaChatModelSelector v-model="selectedModel" :options="modelOptions" />
|
|
121
|
-
<TelaChatPromptSubmitButton
|
|
122
|
-
:status="status"
|
|
123
|
-
:disabled="!draft.trim()"
|
|
124
|
-
@send="send"
|
|
125
|
-
@stop="status = 'ready'"
|
|
73
|
+
<AnimatePresence mode="popLayout" :initial="false">
|
|
74
|
+
<TelaChatAnimateEnter v-if="status === 'streaming'" key="stop">
|
|
75
|
+
<TelaChatStopRunButton @stop="status = 'ready'" />
|
|
76
|
+
</TelaChatAnimateEnter>
|
|
77
|
+
|
|
78
|
+
<TelaChatAnimateEnter v-else key="input" :scale="0.9">
|
|
79
|
+
<TelaChatPromptInput>
|
|
80
|
+
<TelaChatPromptInputTextarea
|
|
81
|
+
placeholder="How can I help?"
|
|
82
|
+
:empty="!draft"
|
|
83
|
+
@input="onInput"
|
|
84
|
+
@keydown.enter.exact.prevent="send"
|
|
126
85
|
/>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
86
|
+
<TelaChatPromptInputToolbar>
|
|
87
|
+
<TelaChatPromptInputTools>
|
|
88
|
+
<TelaChatPromptInputActionMenu>
|
|
89
|
+
<TelaChatPromptInputActionMenuTrigger />
|
|
90
|
+
<TelaChatPromptInputActionMenuContent>
|
|
91
|
+
<TelaChatPromptInputActionMenuItem icon="i-ph-paperclip">
|
|
92
|
+
Add files or photos
|
|
93
|
+
</TelaChatPromptInputActionMenuItem>
|
|
94
|
+
<TelaChatPromptInputActionMenuSub>
|
|
95
|
+
<TelaChatPromptInputActionMenuSubTrigger icon="i-ph-clock-counter-clockwise">
|
|
96
|
+
Recent files
|
|
97
|
+
</TelaChatPromptInputActionMenuSubTrigger>
|
|
98
|
+
<TelaChatPromptInputActionMenuSubContent>
|
|
99
|
+
<TelaChatPromptInputActionMenuItem
|
|
100
|
+
v-for="file in recentFiles"
|
|
101
|
+
:key="file.id"
|
|
102
|
+
>
|
|
103
|
+
<img :src="getMimeTypeIcon(file.type)" class="w-16px h-16px mr-8px shrink-0" alt="">
|
|
104
|
+
<span class="min-w-0 max-w-200px truncate">{{ file.label }}</span>
|
|
105
|
+
</TelaChatPromptInputActionMenuItem>
|
|
106
|
+
</TelaChatPromptInputActionMenuSubContent>
|
|
107
|
+
</TelaChatPromptInputActionMenuSub>
|
|
108
|
+
<TelaMenubarSeparator />
|
|
109
|
+
<TelaChatPromptInputActionMenuSub>
|
|
110
|
+
<TelaChatPromptInputActionMenuSubTrigger icon="i-ph-notepad" icon-size="16px">
|
|
111
|
+
Skills
|
|
112
|
+
</TelaChatPromptInputActionMenuSubTrigger>
|
|
113
|
+
<TelaChatPromptInputActionMenuSubContent>
|
|
114
|
+
<TelaChatPromptInputActionMenuItem
|
|
115
|
+
v-for="skill in skills"
|
|
116
|
+
:key="skill.id"
|
|
117
|
+
@click="draft = skill.label"
|
|
118
|
+
>
|
|
119
|
+
<span class="min-w-0 max-w-200px truncate">{{ skill.label }}</span>
|
|
120
|
+
</TelaChatPromptInputActionMenuItem>
|
|
121
|
+
</TelaChatPromptInputActionMenuSubContent>
|
|
122
|
+
</TelaChatPromptInputActionMenuSub>
|
|
123
|
+
</TelaChatPromptInputActionMenuContent>
|
|
124
|
+
</TelaChatPromptInputActionMenu>
|
|
125
|
+
</TelaChatPromptInputTools>
|
|
126
|
+
<TelaChatPromptInputActions>
|
|
127
|
+
<TelaChatModelSelector v-model="selectedModel" :options="modelOptions" />
|
|
128
|
+
<TelaChatPromptSubmitButton
|
|
129
|
+
:status="status"
|
|
130
|
+
:disabled="!draft.trim()"
|
|
131
|
+
@send="send"
|
|
132
|
+
@stop="status = 'ready'"
|
|
133
|
+
/>
|
|
134
|
+
</TelaChatPromptInputActions>
|
|
135
|
+
</TelaChatPromptInputToolbar>
|
|
136
|
+
</TelaChatPromptInput>
|
|
137
|
+
</TelaChatAnimateEnter>
|
|
138
|
+
</AnimatePresence>
|
|
130
139
|
</TelaChatBody>
|
|
131
140
|
</div>
|
|
132
141
|
</template>
|
|
@@ -136,6 +145,10 @@ function send() {
|
|
|
136
145
|
|
|
137
146
|
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
147
|
|
|
148
|
+
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.
|
|
149
|
+
|
|
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
|
+
|
|
139
152
|
```vue
|
|
140
153
|
<script setup lang="ts">
|
|
141
154
|
import { getMimeTypeIcon } from '@meistrari/tela-build/utils/files'
|
|
@@ -904,7 +917,7 @@ const statusLabel = computed(() => {
|
|
|
904
917
|
|
|
905
918
|
### Widget Header & History
|
|
906
919
|
|
|
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.
|
|
920
|
+
`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
921
|
|
|
909
922
|
Click the clock icon to open the history panel.
|
|
910
923
|
|
|
@@ -917,12 +930,17 @@ const conversations = [
|
|
|
917
930
|
{ id: 'c4', title: 'Reasoning trace rendering' },
|
|
918
931
|
]
|
|
919
932
|
const selected = ref('c1')
|
|
933
|
+
const expanded = ref(false)
|
|
920
934
|
</script>
|
|
921
935
|
|
|
922
936
|
<template>
|
|
923
937
|
<TelaChatWidgetHeader
|
|
924
938
|
title="Release notes review"
|
|
925
|
-
class="w-
|
|
939
|
+
class="w-360px"
|
|
940
|
+
expandable
|
|
941
|
+
closable
|
|
942
|
+
:expanded="expanded"
|
|
943
|
+
@toggle-expanded="expanded = !expanded"
|
|
926
944
|
@select-conversation="selected = $event"
|
|
927
945
|
>
|
|
928
946
|
<template #history>
|
|
@@ -963,6 +981,91 @@ const suggestions = [
|
|
|
963
981
|
</template>
|
|
964
982
|
```
|
|
965
983
|
|
|
984
|
+
### Working Indicator
|
|
985
|
+
|
|
986
|
+
The running state of an assistant turn: a small `TelaLoadersSquare`, a label (`Working` by default) and an elapsed counter. Show it whenever something is running. A running turn is one `TelaChatMessage`: put the indicator and anything else the wait needs (a shimmering activity line, a reasoning trace) inside it, and the message stacks them 8px apart while the conversation keeps its 16px between turns. Pass `started-at` (epoch ms) and it ticks by itself; pass `elapsed` (seconds) to drive it from the host, for example from a server-reported duration.
|
|
987
|
+
|
|
988
|
+
```vue
|
|
989
|
+
<script setup lang="ts">
|
|
990
|
+
const startedAt = Date.now()
|
|
991
|
+
</script>
|
|
992
|
+
|
|
993
|
+
<template>
|
|
994
|
+
<TelaChatConversationContent>
|
|
995
|
+
<TelaChatMessage from="assistant">
|
|
996
|
+
<TelaChatWorkingIndicator :started-at="startedAt" />
|
|
997
|
+
<TelaChatTextShimmer>Processing information...</TelaChatTextShimmer>
|
|
998
|
+
</TelaChatMessage>
|
|
999
|
+
</TelaChatConversationContent>
|
|
1000
|
+
</template>
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
### Stop Run
|
|
1004
|
+
|
|
1005
|
+
While a turn is running the prompt input goes away and this button takes its place, centered where the input was. Neither it nor `TelaChatPromptInput` animates on its own: wrap each in `TelaChatAnimateEnter` and swap them with `v-if` / `v-else` (keyed) inside an `<AnimatePresence mode="popLayout">` to cross-fade them in place. It carries the same `data-chat-prompt-input` marker, so `TelaChatBody` docks it at the bottom and keeps the conversation padded for it. Emits `stop`.
|
|
1006
|
+
|
|
1007
|
+
```vue
|
|
1008
|
+
<script setup lang="ts">
|
|
1009
|
+
const running = ref(true)
|
|
1010
|
+
const startedAt = Date.now()
|
|
1011
|
+
const draft = ref('')
|
|
1012
|
+
|
|
1013
|
+
function onInput(event: Event) {
|
|
1014
|
+
draft.value = (event.target as HTMLElement).innerText
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function send() {
|
|
1018
|
+
// the editor unmounts with the input, so clear the draft it was showing
|
|
1019
|
+
draft.value = ''
|
|
1020
|
+
running.value = true
|
|
1021
|
+
}
|
|
1022
|
+
</script>
|
|
1023
|
+
|
|
1024
|
+
<template>
|
|
1025
|
+
<div class="flex flex-col w-full h-260px">
|
|
1026
|
+
<TelaChatBody variant="page">
|
|
1027
|
+
<TelaChatConversation>
|
|
1028
|
+
<TelaChatConversationContent>
|
|
1029
|
+
<TelaChatMessage from="user">
|
|
1030
|
+
<TelaChatMessageContent>Consolidate these exams into a table.</TelaChatMessageContent>
|
|
1031
|
+
</TelaChatMessage>
|
|
1032
|
+
<TelaChatMessage v-if="running" from="assistant">
|
|
1033
|
+
<TelaChatWorkingIndicator :started-at="startedAt" />
|
|
1034
|
+
<TelaChatTextShimmer>Processing information...</TelaChatTextShimmer>
|
|
1035
|
+
</TelaChatMessage>
|
|
1036
|
+
</TelaChatConversationContent>
|
|
1037
|
+
</TelaChatConversation>
|
|
1038
|
+
|
|
1039
|
+
<AnimatePresence mode="popLayout" :initial="false">
|
|
1040
|
+
<TelaChatAnimateEnter v-if="running" key="stop">
|
|
1041
|
+
<TelaChatStopRunButton @stop="running = false" />
|
|
1042
|
+
</TelaChatAnimateEnter>
|
|
1043
|
+
<TelaChatAnimateEnter v-else key="input" :scale="0.9">
|
|
1044
|
+
<TelaChatPromptInput>
|
|
1045
|
+
<TelaChatPromptInputTextarea placeholder="How can I help?" :empty="!draft" @input="onInput" @keydown.enter.exact.prevent="send" />
|
|
1046
|
+
<TelaChatPromptInputToolbar>
|
|
1047
|
+
<TelaChatPromptInputTools>
|
|
1048
|
+
<TelaChatPromptInputActionMenu>
|
|
1049
|
+
<TelaChatPromptInputActionMenuTrigger />
|
|
1050
|
+
<TelaChatPromptInputActionMenuContent>
|
|
1051
|
+
<TelaChatPromptInputActionMenuItem icon="i-ph-paperclip">
|
|
1052
|
+
Add files or photos
|
|
1053
|
+
</TelaChatPromptInputActionMenuItem>
|
|
1054
|
+
</TelaChatPromptInputActionMenuContent>
|
|
1055
|
+
</TelaChatPromptInputActionMenu>
|
|
1056
|
+
</TelaChatPromptInputTools>
|
|
1057
|
+
<TelaChatPromptInputActions>
|
|
1058
|
+
<TelaChatPromptSubmitButton status="ready" :disabled="!draft.trim()" @send="send" />
|
|
1059
|
+
</TelaChatPromptInputActions>
|
|
1060
|
+
</TelaChatPromptInputToolbar>
|
|
1061
|
+
</TelaChatPromptInput>
|
|
1062
|
+
</TelaChatAnimateEnter>
|
|
1063
|
+
</AnimatePresence>
|
|
1064
|
+
</TelaChatBody>
|
|
1065
|
+
</div>
|
|
1066
|
+
</template>
|
|
1067
|
+
```
|
|
1068
|
+
|
|
966
1069
|
### Text Shimmer
|
|
967
1070
|
|
|
968
1071
|
The shimmering thinking indicator. Render it while the model is generating: inside the conversation, under a reasoning trace, wherever the wait happens.
|
|
@@ -1026,11 +1129,14 @@ import { TelaChatIconsAnthropic, TelaChatIconsGemini, TelaChatIconsOpenai } from
|
|
|
1026
1129
|
| `TelaChatReasoningStep` | Collapsible reasoning/tool step row with status icon. |
|
|
1027
1130
|
| `TelaChatWidgetCard` | Card shell for tool-call widgets (title, icon, `TelaStatus`). |
|
|
1028
1131
|
| `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,
|
|
1132
|
+
| `TelaChatWidgetHeader` | Widget top bar: optional title, new-conversation and history actions, plus expand and minimize (floating shell only). Marked with `data-widget-header`. |
|
|
1030
1133
|
| `TelaChatWidgetHistoryList` | Searchable conversation list for the header's `#history` slot. |
|
|
1031
1134
|
| `TelaChatFloatingWindowTrigger` | Pill button that opens the floating widget. |
|
|
1032
1135
|
| `TelaChatFloatingWindow` | Anchored popover shell for the floating variant (`open` / `@update:open`). |
|
|
1033
1136
|
| `TelaChatSidepanelWindow` | Always-open, in-flow docked column for the sidepanel variant. |
|
|
1137
|
+
| `TelaChatWorkingIndicator` | Running-turn row: square loader, label, elapsed time. |
|
|
1138
|
+
| `TelaChatStopRunButton` | Replaces the prompt input while a turn runs; centered pill that emits `stop`. |
|
|
1139
|
+
| `TelaChatAnimateEnter` | Enter/exit preset (fade, slide, slight scale) for a surface that comes and goes; when it wraps the prompt input or stop button the body docks the wrapper. Pair with `AnimatePresence`. |
|
|
1034
1140
|
| `TelaChatTextShimmer` | Shimmering text, used for thinking/loading indicators. |
|
|
1035
1141
|
| `TelaChatIconsAnthropic` / `TelaChatIconsOpenai` / `TelaChatIconsGemini` | Provider icons for model options. |
|
|
1036
1142
|
| `TelaChatPoweredByTela` | "Powered by" + Tela wordmark footer. |
|
|
@@ -1046,8 +1152,8 @@ Shell (page container | TelaChatFloatingWindow | TelaChatSidepanelWindow)
|
|
|
1046
1152
|
├── TelaChatConversation
|
|
1047
1153
|
│ └── TelaChatConversationContent
|
|
1048
1154
|
│ ├── TelaChatMessage → TelaChatMessageContent
|
|
1049
|
-
│ └──
|
|
1050
|
-
└── TelaChatPromptInput
|
|
1155
|
+
│ └── TelaChatWorkingIndicator (while generating)
|
|
1156
|
+
└── TelaChatPromptInput (TelaChatStopRunButton while a turn runs)
|
|
1051
1157
|
├── TelaChatPromptInputAttachments
|
|
1052
1158
|
├── TelaChatPromptInputTextarea
|
|
1053
1159
|
└── TelaChatPromptInputToolbar
|
|
@@ -1265,8 +1371,17 @@ type WidgetSourceProps = {
|
|
|
1265
1371
|
```typescript
|
|
1266
1372
|
type WidgetHeaderProps = {
|
|
1267
1373
|
title?: string
|
|
1374
|
+
expandable?: boolean // show the expand toggle outside a floating window
|
|
1375
|
+
expanded?: boolean // standalone toggle state; ignored inside a floating window
|
|
1376
|
+
closable?: boolean // show the minimize button outside a floating window
|
|
1377
|
+
newConversationLabel?: string // default: 'New conversation'
|
|
1378
|
+
historyLabel?: string // default: 'Conversation history'
|
|
1379
|
+
expandLabel?: string // default: 'Expand'
|
|
1380
|
+
collapseLabel?: string // default: 'Collapse'
|
|
1381
|
+
closeLabel?: string // default: 'Close'
|
|
1268
1382
|
}
|
|
1269
|
-
//
|
|
1383
|
+
// 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).
|
|
1384
|
+
// emits: newConversation, selectConversation(id: string), toggleExpanded, close
|
|
1270
1385
|
|
|
1271
1386
|
type WidgetHistoryListProps = {
|
|
1272
1387
|
items: { id: string, title: string }[]
|
|
@@ -1291,6 +1406,37 @@ type FloatingWindowProps = {
|
|
|
1291
1406
|
// TelaChatSidepanelWindow takes no props.
|
|
1292
1407
|
```
|
|
1293
1408
|
|
|
1409
|
+
### Working Indicator
|
|
1410
|
+
|
|
1411
|
+
```typescript
|
|
1412
|
+
type WorkingIndicatorProps = {
|
|
1413
|
+
class?: HTMLAttributes['class']
|
|
1414
|
+
label?: string // default: 'Working'
|
|
1415
|
+
startedAt?: number // epoch ms; the counter ticks every second from it
|
|
1416
|
+
elapsed?: number // seconds; host-controlled, wins over startedAt
|
|
1417
|
+
}
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
### Animate Enter
|
|
1421
|
+
|
|
1422
|
+
```typescript
|
|
1423
|
+
type AnimateEnterProps = {
|
|
1424
|
+
offset?: string | number // default: '100%' (slide distance)
|
|
1425
|
+
scale?: number // default: 0.97; the input reads better at 0.9, the pill at the default
|
|
1426
|
+
duration?: number // default: 0.16 (seconds)
|
|
1427
|
+
}
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
### Stop Run
|
|
1431
|
+
|
|
1432
|
+
```typescript
|
|
1433
|
+
type StopRunButtonProps = {
|
|
1434
|
+
class?: HTMLAttributes['class']
|
|
1435
|
+
label?: string // default: 'Stop Run'
|
|
1436
|
+
disabled?: boolean
|
|
1437
|
+
}
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1294
1440
|
### Text Shimmer
|
|
1295
1441
|
|
|
1296
1442
|
```typescript
|
|
@@ -1307,7 +1453,7 @@ type TextShimmerProps = {
|
|
|
1307
1453
|
- `TelaChatSidepanelWindow`: `default`.
|
|
1308
1454
|
- `TelaChatWidgetHeader`: `history` (rendered only while the history panel is open).
|
|
1309
1455
|
- `TelaChatWidgetCard`: `subtitle`, `default` (tool output).
|
|
1310
|
-
- `TelaChatBody` / `TelaChatConversation` / `TelaChatConversationContent` / `TelaChatMessage` / `TelaChatMessageContent` / `TelaChatPromptInput` / `TelaChatPromptInputToolbar` / `TelaChatPromptInputTools` / `TelaChatPromptInputActions` / `TelaChatPromptInputAttachments` / `TelaChatReasoningStep` / `TelaChatWidgetSources`: `default`.
|
|
1456
|
+
- `TelaChatBody` / `TelaChatConversation` / `TelaChatConversationContent` / `TelaChatMessage` / `TelaChatMessageContent` / `TelaChatPromptInput` / `TelaChatPromptInputToolbar` / `TelaChatPromptInputTools` / `TelaChatPromptInputActions` / `TelaChatPromptInputAttachments` / `TelaChatReasoningStep` / `TelaChatWidgetSources` / `TelaChatAnimateEnter`: `default`.
|
|
1311
1457
|
|
|
1312
1458
|
```vue
|
|
1313
1459
|
<TelaChatConversation scroll-area>
|
|
@@ -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-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
203
|
+
<div ref="launcher" :class="cn('bottom-16px', 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>
|
|
@@ -9,7 +9,7 @@ const resolvedAlign = computed(() => props.align ?? (props.from === 'user' ? 'en
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
|
-
<div flex="~ col" :class="resolvedAlign === 'end' ? 'items-end' : 'items-start'">
|
|
12
|
+
<div flex="~ col" gap-8px :class="resolvedAlign === 'end' ? 'items-end' : 'items-start'">
|
|
13
13
|
<slot />
|
|
14
14
|
</div>
|
|
15
15
|
</template>
|
|
@@ -41,15 +41,10 @@ function focusEditor(event: MouseEvent) {
|
|
|
41
41
|
<div
|
|
42
42
|
ref="rootRef"
|
|
43
43
|
data-chat-prompt-input
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
bg border-0.5px border-strong
|
|
49
|
-
overflow-hidden
|
|
50
|
-
relative
|
|
51
|
-
transition
|
|
52
|
-
:class="locked ? 'bg-muted cursor-not-allowed' : 'cursor-text'"
|
|
44
|
+
:class="cn(
|
|
45
|
+
'flex flex-col gap-8px pl-16px pb-8px pr-8px rounded-12px bg border-[0.5px] border-strong overflow-hidden relative transition',
|
|
46
|
+
locked ? 'bg-muted cursor-not-allowed' : 'cursor-text',
|
|
47
|
+
)"
|
|
53
48
|
@mousedown="focusEditor"
|
|
54
49
|
>
|
|
55
50
|
<div class="contents" :inert="locked">
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Meta } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Loaders/Splash Screen" />
|
|
4
|
+
|
|
5
|
+
# TelaLoadersSplashScreen
|
|
6
|
+
|
|
7
|
+
Full-viewport white overlay shown while the app boots. It centers a `TelaLoadersSquare` and, when `modelValue` turns false, wipes the loader with a diagonal mask and fades the overlay out before emitting `hidden`. Mount it once at the app root and flip the flag when the first page is ready.
|
|
8
|
+
|
|
9
|
+
## Props
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
type SplashScreenProps = {
|
|
13
|
+
modelValue: boolean // true while the app is loading
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Emits
|
|
18
|
+
|
|
19
|
+
| event | payload | when |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `update:modelValue` | `false` | the exit animation has finished |
|
|
22
|
+
| `hidden` | — | same moment, for side effects such as starting a welcome screen |
|
|
23
|
+
|
|
24
|
+
## Basic Usage
|
|
25
|
+
|
|
26
|
+
The overlay keeps rendering during the exit animation, so it is safe to set `modelValue` to `false` as soon as data is ready.
|
|
27
|
+
|
|
28
|
+
```vue
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
const isLoading = ref(true)
|
|
31
|
+
|
|
32
|
+
onMounted(() => {
|
|
33
|
+
isLoading.value = false
|
|
34
|
+
})
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<TelaLoadersSplashScreen v-model="isLoading" />
|
|
39
|
+
</template>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Rules
|
|
43
|
+
|
|
44
|
+
1. **One per app.** The overlay is `fixed`, full-viewport and `z-9999`; render it from the root layout, not from pages.
|
|
45
|
+
2. **Do not restyle the loader.** The wipe targets the loader's `#square` id; wrap or replace the whole component instead of overriding it.
|
|
@@ -48,27 +48,6 @@ watch(() => props.modelValue, (visible) => {
|
|
|
48
48
|
id="square-screen"
|
|
49
49
|
fixed w-100dvw h-100dvh inset-0 z-9999 flex items-center justify-center bg-white
|
|
50
50
|
>
|
|
51
|
-
<
|
|
52
|
-
<div
|
|
53
|
-
absolute border-4 border-transparent inset="-1"
|
|
54
|
-
class="[mask-clip:padding-box,border-box] [mask-composite:intersect] [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)]"
|
|
55
|
-
>
|
|
56
|
-
<Motion
|
|
57
|
-
absolute aspect-square bg-black h-4
|
|
58
|
-
:animate="{
|
|
59
|
-
offsetDistance: ['0%', '100%'],
|
|
60
|
-
}"
|
|
61
|
-
:style="{
|
|
62
|
-
width: 40,
|
|
63
|
-
offsetPath: 'rect(0 auto auto 0)',
|
|
64
|
-
}"
|
|
65
|
-
:transition="{
|
|
66
|
-
repeat: Number.POSITIVE_INFINITY,
|
|
67
|
-
duration: 0.8,
|
|
68
|
-
ease: 'easeInOut',
|
|
69
|
-
}"
|
|
70
|
-
/>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
51
|
+
<TelaLoadersSquare id="square" size="md" />
|
|
73
52
|
</div>
|
|
74
53
|
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Meta } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Loaders/Square" tags={['new']} />
|
|
4
|
+
|
|
5
|
+
# TelaLoadersSquare
|
|
6
|
+
|
|
7
|
+
The Tela square loader: a bar orbiting inside a bordered square. It is the animation behind `TelaLoadersSplashScreen` and the chat working indicator.
|
|
8
|
+
|
|
9
|
+
## Props
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
type SquareLoaderProps = {
|
|
13
|
+
size?: 'sm' | 'md' // default: 'md'
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic Usage
|
|
18
|
+
|
|
19
|
+
Renders the 32×32 loader.
|
|
20
|
+
|
|
21
|
+
```vue
|
|
22
|
+
<TelaLoadersSquare />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Variants
|
|
26
|
+
|
|
27
|
+
### Sizes
|
|
28
|
+
|
|
29
|
+
`md` is 32×32 with a 4px border; `sm` is 12×12 with a 2px border, for inline states next to text.
|
|
30
|
+
|
|
31
|
+
```vue
|
|
32
|
+
<div class="flex items-center gap-4">
|
|
33
|
+
<TelaLoadersSquare size="sm" />
|
|
34
|
+
<TelaLoadersSquare size="md" />
|
|
35
|
+
</div>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Section loading
|
|
39
|
+
|
|
40
|
+
Center it in the area that is loading, with an optional label below.
|
|
41
|
+
|
|
42
|
+
```vue
|
|
43
|
+
<div class="flex flex-col items-center justify-center gap-4 py-12">
|
|
44
|
+
<TelaLoadersSquare />
|
|
45
|
+
<p class="body-14-regular text-secondary">Loading…</p>
|
|
46
|
+
</div>
|
|
47
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
const props = withDefaults(defineProps<{
|
|
3
|
+
size?: 'sm' | 'md'
|
|
4
|
+
}>(), { size: 'md' })
|
|
5
|
+
|
|
6
|
+
const sizes = {
|
|
7
|
+
sm: { box: 'w-3 h-3 border-2', mask: 'border-2 -inset-0.5' },
|
|
8
|
+
md: { box: 'w-8 h-8 border-4', mask: 'border-4 -inset-1' },
|
|
9
|
+
} as const
|
|
10
|
+
|
|
11
|
+
const current = computed(() => sizes[props.size])
|
|
12
|
+
const barColor = DT.colors.icon.DEFAULT
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<div class="relative border-icon-subtle bg-white" :class="current.box">
|
|
17
|
+
<div
|
|
18
|
+
:class="cn(
|
|
19
|
+
'absolute border-transparent [mask-clip:padding-box,border-box] [mask-composite:intersect] [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)]',
|
|
20
|
+
current.mask,
|
|
21
|
+
)"
|
|
22
|
+
>
|
|
23
|
+
<div
|
|
24
|
+
data-square-loader-bar
|
|
25
|
+
class="absolute -inset-1/2"
|
|
26
|
+
:style="{ '--bar-color': barColor }"
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<style scoped>
|
|
33
|
+
[data-square-loader-bar] {
|
|
34
|
+
background: conic-gradient(from 0deg, var(--bar-color) 0 90deg, transparent 90deg 360deg);
|
|
35
|
+
animation: square-loader-bar 0.8s ease-in-out infinite;
|
|
36
|
+
will-change: transform;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@keyframes square-loader-bar {
|
|
40
|
+
from {
|
|
41
|
+
transform: rotate(0deg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
to {
|
|
45
|
+
transform: rotate(360deg);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@media (prefers-reduced-motion: reduce) {
|
|
50
|
+
[data-square-loader-bar] {
|
|
51
|
+
animation-duration: 2.4s;
|
|
52
|
+
animation-timing-function: linear;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Meta } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Loaders/Welcome Screen" />
|
|
4
|
+
|
|
5
|
+
# TelaLoadersWelcomeScreen
|
|
6
|
+
|
|
7
|
+
Full-viewport greeting shown once after the splash screen. It waits `delayAfterTrigger` after `trigger` turns true, reveals `text` with `TelaTextMaskAnimation` (optionally highlighting one word), then wipes the text and fades out before emitting `hidden`.
|
|
8
|
+
|
|
9
|
+
## Props
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
type WelcomeScreenProps = {
|
|
13
|
+
modelValue: boolean // true while the screen should be shown
|
|
14
|
+
text: string // greeting to animate
|
|
15
|
+
highlightWord?: string // word inside `text` to emphasize
|
|
16
|
+
trigger: boolean // starts the text animation when true
|
|
17
|
+
delayAfterTrigger?: number // ms to wait after `trigger`, default: 2700
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Emits
|
|
22
|
+
|
|
23
|
+
| event | payload | when |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `update:modelValue` | `false` | the exit animation has finished |
|
|
26
|
+
| `hidden` | — | same moment |
|
|
27
|
+
|
|
28
|
+
## Basic Usage
|
|
29
|
+
|
|
30
|
+
Chain it after the splash screen by using the inverse of the splash flag as `trigger`, so the greeting starts once the loader is gone.
|
|
31
|
+
|
|
32
|
+
```vue
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
const isLoading = ref(true)
|
|
35
|
+
const showWelcome = ref(true)
|
|
36
|
+
const firstName = 'Ana'
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<TelaLoadersSplashScreen v-model="isLoading" />
|
|
41
|
+
<TelaLoadersWelcomeScreen
|
|
42
|
+
v-model="showWelcome"
|
|
43
|
+
:text="`Welcome to Tela, ${firstName}.`"
|
|
44
|
+
:highlight-word="firstName"
|
|
45
|
+
:trigger="!isLoading"
|
|
46
|
+
/>
|
|
47
|
+
</template>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Rules
|
|
51
|
+
|
|
52
|
+
1. **Show it once.** Persist that the user has seen it; do not replay on every navigation.
|
|
53
|
+
2. **Keep `text` short.** It renders as a single centered line; long greetings wrap awkwardly on narrow viewports.
|
|
54
|
+
3. **`highlightWord` must appear in `text`.** Otherwise nothing is emphasized.
|
package/package.json
CHANGED
|
File without changes
|