@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/components/tela/avatar/label/avatar-label.mdx +2 -0
  3. package/components/tela/avatar/label/avatar-label.vue +25 -3
  4. package/components/tela/button/button.mdx +18 -0
  5. package/components/tela/button/button.vue +34 -20
  6. package/components/tela/chat/chat-animate-enter.vue +24 -0
  7. package/components/tela/chat/chat-body.vue +21 -3
  8. package/components/tela/chat/chat-stop-run-button.vue +31 -0
  9. package/components/tela/chat/chat-working-indicator.vue +84 -0
  10. package/components/tela/chat/chat.mdx +191 -63
  11. package/components/tela/chat/floating/floating-window.vue +1 -1
  12. package/components/tela/chat/message/message.vue +1 -1
  13. package/components/tela/chat/prompt-input/prompt-input.vue +4 -9
  14. package/components/tela/copy-button.vue +4 -0
  15. package/components/tela/dropdown-menu/DropdownMenuItem.vue +2 -2
  16. package/components/tela/filter/filter-content.test.ts +60 -0
  17. package/components/tela/filter/filter-content.vue +3 -0
  18. package/components/tela/filter/filter-trigger.vue +2 -1
  19. package/components/tela/filter/filter.mdx +13 -1
  20. package/components/tela/filter/filter.vue +2 -1
  21. package/components/tela/filter/types.ts +1 -0
  22. package/components/tela/live-label.vue +36 -20
  23. package/components/tela/loaders/splash-screen/splash-screen.mdx +45 -0
  24. package/components/tela/loaders/{splash-screen.vue → splash-screen/splash-screen.vue} +1 -22
  25. package/components/tela/loaders/square/square.mdx +47 -0
  26. package/components/tela/loaders/square/square.vue +55 -0
  27. package/components/tela/loaders/welcome-screen/welcome-screen.mdx +54 -0
  28. package/components/tela/pane.vue +11 -3
  29. package/components/tela/timeline-waterfall/timeline-waterfall.mdx +85 -8
  30. package/components/tela/timeline-waterfall/timeline-waterfall.test.ts +917 -0
  31. package/components/tela/timeline-waterfall/timeline-waterfall.vue +905 -199
  32. package/components/tela/tooltip/tooltip-body.vue +19 -7
  33. package/components/tela/tooltip/tooltip.mdx +30 -31
  34. package/components/tela/tooltip/tooltip.test.ts +46 -0
  35. package/components/tela/tooltip/tooltip.vue +2 -0
  36. package/lib/extractors/volar-extract.ts +6 -6
  37. package/package.json +1 -1
  38. /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 flex items-center gap-4px>
17
+ <div class="avatar-label" :title="label">
18
18
  <TelaAvatar
19
+ class="avatar-label-image"
19
20
  :size="avatarSize"
20
- :src="avatarSrc"
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-[10px] rounded-[13px]',
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
- switch (sizeStyle.value) {
69
- case 'xs':
70
- return '11px'
71
- case 'sm':
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-6px ease-out duration-40',
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
- <TelaIconSpinner v-if="loading && leading" class="animate-spin" />
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
- <TelaIcon v-else-if="icon && leading" :name="icon" :size="iconSize" :color="iconColorStyle" class="min-h-14px min-w-14px" />
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
- <TelaIconSpinner v-if="loading && !leading" class="animate-spin min-h-14px min-w-14px" />
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
- <TelaIcon v-else-if="icon && !leading" :name="icon" :size="iconSize" :color="iconColorStyle" class="min-h-14px min-w-14px" />
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>('[data-chat-prompt-input]') ?? null
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
- [data-chat-body] [data-chat-prompt-input] {
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>