@meistrari/tela-build 1.71.0 → 1.73.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 +24 -0
- package/components/tela/avatar/label/avatar-label.mdx +2 -0
- package/components/tela/avatar/label/avatar-label.vue +25 -3
- 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-working-indicator.vue +84 -0
- package/components/tela/chat/chat.mdx +191 -63
- package/components/tela/chat/floating/floating-window.vue +1 -1
- package/components/tela/chat/message/message.vue +1 -1
- package/components/tela/chat/prompt-input/prompt-input.vue +4 -9
- package/components/tela/copy-button.vue +4 -0
- package/components/tela/dropdown-menu/DropdownMenuItem.vue +2 -2
- package/components/tela/filter/filter-content.test.ts +60 -0
- package/components/tela/filter/filter-content.vue +3 -0
- package/components/tela/filter/filter-trigger.vue +2 -1
- package/components/tela/filter/filter.mdx +13 -1
- package/components/tela/filter/filter.vue +2 -1
- package/components/tela/filter/types.ts +1 -0
- package/components/tela/live-label.vue +36 -20
- 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/components/tela/pane.vue +11 -3
- package/components/tela/timeline-waterfall/timeline-waterfall.mdx +85 -8
- package/components/tela/timeline-waterfall/timeline-waterfall.test.ts +917 -0
- package/components/tela/timeline-waterfall/timeline-waterfall.vue +905 -199
- package/components/tela/tooltip/tooltip-body.vue +19 -7
- package/components/tela/tooltip/tooltip.mdx +30 -31
- package/components/tela/tooltip/tooltip.test.ts +46 -0
- package/components/tela/tooltip/tooltip.vue +2 -0
- package/lib/extractors/volar-extract.ts +6 -6
- package/package.json +1 -1
- /package/components/tela/loaders/{welcome-screen.vue → welcome-screen/welcome-screen.vue} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
Latest updates and announcements.
|
|
4
4
|
|
|
5
|
+
## September 17, 2026
|
|
6
|
+
|
|
7
|
+
- **Dropdown menu spacing.** Items with tooltips now use the same icon-to-label gap as other items. See [Dropdown menu](/components/dropdown-menu).
|
|
8
|
+
|
|
9
|
+
## September 16, 2026
|
|
10
|
+
|
|
11
|
+
### Square Loader
|
|
12
|
+
|
|
13
|
+
- **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).
|
|
14
|
+
|
|
15
|
+
### Button
|
|
16
|
+
|
|
17
|
+
- **`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).
|
|
18
|
+
- **`size="lg"` is slightly tighter.** Vertical padding goes from 10px to 8px and the radius from 13px to 12px.
|
|
19
|
+
- **Icon gap is now 4px.** Icons get a 2px padded wrapper, so the gap between icon and label drops from 6px to 4px.
|
|
20
|
+
- **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.
|
|
21
|
+
|
|
22
|
+
### Chat: Working indicator
|
|
23
|
+
|
|
24
|
+
- **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).
|
|
25
|
+
- **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.
|
|
26
|
+
- **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).
|
|
27
|
+
- **`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.
|
|
28
|
+
|
|
5
29
|
## September 15, 2026
|
|
6
30
|
|
|
7
31
|
### Chat: Floating window
|
|
@@ -7,6 +7,8 @@ import * as AvatarLabelStories from './avatar-label.stories.ts';
|
|
|
7
7
|
|
|
8
8
|
An inline avatar + text chip. Pairs a `TelaAvatar` with a text label so any "entity reference" (created-by, assigned-to, owner, reviewer, member, etc.) renders consistently across summary rows, list cells, and metadata strips. The component is named after its **shape**, not a role — pick the role at the call site via the `label`.
|
|
9
9
|
|
|
10
|
+
The avatar retains its configured size in narrow containers. The label stays on one line and truncates with an ellipsis when space is limited; hover the component to read the full label.
|
|
11
|
+
|
|
10
12
|
## Examples
|
|
11
13
|
|
|
12
14
|
### Basic Usage
|
|
@@ -14,15 +14,37 @@ withDefaults(defineProps<Props>(), {
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<template>
|
|
17
|
-
<div
|
|
17
|
+
<div class="avatar-label" :title="label">
|
|
18
18
|
<TelaAvatar
|
|
19
|
+
class="avatar-label-image"
|
|
19
20
|
:size="avatarSize"
|
|
20
|
-
:
|
|
21
|
+
:image="avatarSrc"
|
|
21
22
|
:alt="avatarAlt ?? label"
|
|
22
23
|
rounded-full
|
|
23
24
|
/>
|
|
24
|
-
<p body-12-regular text-secondary>
|
|
25
|
+
<p class="avatar-label-text" body-12-regular text-secondary>
|
|
25
26
|
{{ label }}
|
|
26
27
|
</p>
|
|
27
28
|
</div>
|
|
28
29
|
</template>
|
|
30
|
+
|
|
31
|
+
<style scoped>
|
|
32
|
+
.avatar-label {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 4px;
|
|
36
|
+
min-width: 0;
|
|
37
|
+
max-width: 100%;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.avatar-label-image {
|
|
41
|
+
flex: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.avatar-label-text {
|
|
45
|
+
min-width: 0;
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
text-overflow: ellipsis;
|
|
48
|
+
white-space: nowrap;
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -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>
|
|
@@ -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>
|