@meistrari/tela-build 1.1.0 → 1.2.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.
@@ -22,7 +22,7 @@ const tag = computed(() => props.to ? NuxtLink : 'div')
22
22
  :class="cn(
23
23
  'inline-block px-[5px] rounded-[5px] select-none',
24
24
  variant === 'outline' && 'border-[0.5px] ring-border-strong',
25
- variant === 'filled' && 'bg-background-lowered',
25
+ variant === 'filled' && 'bg-lowered',
26
26
  props.class,
27
27
  )"
28
28
  >
@@ -65,6 +65,7 @@ const variantStyle = computed(() => (({
65
65
  const resolvedStyle = computed(() => [
66
66
  sizeStyle.value,
67
67
  variantStyle.value,
68
+ '[corner-shape:superellipse(1.3)]',
68
69
  ].join(' '))
69
70
 
70
71
  const tag = computed(() => props.to ? NuxtLink : 'button')
@@ -21,10 +21,10 @@ function handleOpenAutoComplete() {
21
21
  <CollapsibleContent
22
22
  v-bind="props"
23
23
  data-collapsible-content
24
- class="overflow-hidden collapsible-content"
24
+ class="overflow-hidden collapsible-content h-full"
25
25
  @animation-start="handleOpenAutoComplete"
26
26
  >
27
- <div ref="containerEl">
27
+ <div ref="containerEl" class="h-full">
28
28
  <slot />
29
29
  </div>
30
30
  </CollapsibleContent>
@@ -1,13 +1,16 @@
1
1
  <script setup lang="ts">
2
2
  import type { TelaCustomIconName } from '~/types'
3
+ import { Motion } from 'motion-v'
3
4
 
4
5
  const props = withDefaults(defineProps<{
5
- size?: string;
6
- color?: string;
7
- name: TelaCustomIconName;
6
+ size?: string
7
+ color?: string
8
+ name: TelaCustomIconName
9
+ isAnimated?: boolean
8
10
  }>(), {
9
11
  size: '12px',
10
12
  color: 'neutral-400',
13
+ isAnimated: true,
11
14
  })
12
15
 
13
16
  const colorValue = computed(() => {
@@ -27,9 +30,10 @@ const colorValue = computed(() => {
27
30
  </script>
28
31
 
29
32
  <template>
30
- <svg :width="size" :height="size" fill="none" :style="{ color: colorValue }" xmlns="http://www.w3.org/2000/svg">
33
+ <svg :width="size" :height="size" viewBox="0 0 12 12" fill="none" :style="{ color: colorValue }" xmlns="http://www.w3.org/2000/svg">
31
34
  <template v-if="name === 'circle'">
32
- <Motion
35
+ <component
36
+ :is="isAnimated ? Motion : 'path'"
33
37
  as="path"
34
38
  d="M12 6C12 9.31371 9.31371 12 6 12C2.68629 12 0 9.31371 0 6C0 2.68629 2.68629 0 6 0C9.31371 0 12 2.68629 12 6ZM1.2 6C1.2 8.65097 3.34903 10.8 6 10.8C8.65097 10.8 10.8 8.65097 10.8 6C10.8 3.34903 8.65097 1.2 6 1.2C3.34903 1.2 1.2 3.34903 1.2 6Z"
35
39
  fill="currentColor"
@@ -49,7 +53,8 @@ const colorValue = computed(() => {
49
53
  </template>
50
54
 
51
55
  <template v-if="name === 'queued'">
52
- <Motion
56
+ <component
57
+ :is="isAnimated ? Motion : 'path'"
53
58
  as="path"
54
59
  d="M12 6C12 9.31371 9.31371 12 6 12C2.68629 12 0 9.31371 0 6C0 2.68629 2.68629 0 6 0C9.31371 0 12 2.68629 12 6ZM1.2 6C1.2 8.65097 3.34903 10.8 6 10.8C8.65097 10.8 10.8 8.65097 10.8 6C10.8 3.34903 8.65097 1.2 6 1.2C3.34903 1.2 1.2 3.34903 1.2 6Z"
55
60
  fill="currentColor"
@@ -57,7 +62,8 @@ const colorValue = computed(() => {
57
62
  :animate="{ opacity: [0.2, 1] }"
58
63
  :transition="{ duration: 0.8, ease: [0.4, 0, 0.2, 1] }"
59
64
  />
60
- <Motion
65
+ <component
66
+ :is="isAnimated ? Motion : 'rect'"
61
67
  as="rect"
62
68
  x="3.5"
63
69
  y="6.5"
@@ -69,7 +75,8 @@ const colorValue = computed(() => {
69
75
  :animate="{ opacity: [0.2, 1] }"
70
76
  :transition="{ duration: 0.8, ease: 'easeOut', delay: 0.17, repeat: Infinity, repeatDelay: 1 }"
71
77
  />
72
- <Motion
78
+ <component
79
+ :is="isAnimated ? Motion : 'rect'"
73
80
  as="rect"
74
81
  x="4"
75
82
  y="4.5"
@@ -81,7 +88,8 @@ const colorValue = computed(() => {
81
88
  :animate="{ opacity: [0.2, 1] }"
82
89
  :transition="{ duration: 0.8, ease: 'easeOut', delay: 0.12, repeat: Infinity, repeatDelay: 1 }"
83
90
  />
84
- <Motion
91
+ <component
92
+ :is="isAnimated ? Motion : 'rect'"
85
93
  as="rect"
86
94
  x="4.5"
87
95
  y="3"
@@ -122,7 +130,8 @@ const colorValue = computed(() => {
122
130
  </template>
123
131
 
124
132
  <template v-if="name === 'circle-minus'">
125
- <Motion
133
+ <component
134
+ :is="isAnimated ? Motion : 'path'"
126
135
  as="path"
127
136
  d="M12 6C12 9.31371 9.31371 12 6 12C2.68629 12 0 9.31371 0 6C0 2.68629 2.68629 0 6 0C9.31371 0 12 2.68629 12 6ZM1.2 6C1.2 8.65097 3.34903 10.8 6 10.8C8.65097 10.8 10.8 8.65097 10.8 6C10.8 3.34903 8.65097 1.2 6 1.2C3.34903 1.2 1.2 3.34903 1.2 6Z"
128
137
  fill="currentColor"
@@ -130,7 +139,8 @@ const colorValue = computed(() => {
130
139
  :animate="{ opacity: [0.5, 1, 0.5] }"
131
140
  :transition="{ duration: 2.2, ease: 'easeOut', repeat: Infinity }"
132
141
  />
133
- <Motion
142
+ <component
143
+ :is="isAnimated ? Motion : 'rect'"
134
144
  as="rect"
135
145
  x="3.5"
136
146
  y="5"
@@ -145,12 +155,15 @@ const colorValue = computed(() => {
145
155
  </template>
146
156
 
147
157
  <template v-if="name === 'check'">
148
- <Motion as="g" :animate="{ opacity: [0.4, 1] }" :transition="{ duration: 0.3, ease: [0.645, 0.045, 0.355, 1] }" tabindex="-1">
158
+ <component
159
+ :is="isAnimated ? Motion : 'g'" as="g" :animate="{ opacity: [0.4, 1] }" :transition="{ duration: 0.3, ease: [0.645, 0.045, 0.355, 1] }" tabindex="-1"
160
+ >
149
161
  <path
150
162
  d="M12 6C12 9.31371 9.31371 12 6 12C2.68629 12 0 9.31371 0 6C0 2.68629 2.68629 0 6 0C9.31371 0 12 2.68629 12 6ZM1.2 6C1.2 8.65097 3.34903 10.8 6 10.8C8.65097 10.8 10.8 8.65097 10.8 6C10.8 3.34903 8.65097 1.2 6 1.2C3.34903 1.2 1.2 3.34903 1.2 6Z"
151
163
  fill="currentColor"
152
164
  />
153
- <Motion
165
+ <component
166
+ :is="isAnimated ? Motion : 'path'"
154
167
  as="path"
155
168
  d="M3.5 6 L5.5 8 L8.5 4"
156
169
  stroke="currentColor"
@@ -160,10 +173,10 @@ const colorValue = computed(() => {
160
173
  stroke-linejoin="round"
161
174
  tabindex="-1"
162
175
  :animate="{ pathLength: [0, 1] }"
163
- :style="{ rotate: 4, scale: 0.85 }"
176
+ :style="{ rotate: isAnimated ? 4 : undefined, scale: isAnimated ? 0.85 : undefined }"
164
177
  :transition="{ duration: 0.3, ease: [0.645, 0.045, 0.355, 1] }"
165
178
  />
166
- </Motion>
179
+ </component>
167
180
  </template>
168
181
 
169
182
  <template v-if="name === 'stop'">
@@ -194,13 +207,15 @@ const colorValue = computed(() => {
194
207
  </template>
195
208
 
196
209
  <template v-if="name === 'warning'">
197
- <Motion
210
+ <component
211
+ :is="isAnimated ? Motion : 'g'"
198
212
  as="g"
199
213
  tabindex="-1"
200
214
  :animate="{ opacity: [0.5, 1, 0.5] }"
201
215
  :transition="{ duration: 1.4, ease: 'easeOut', repeat: Infinity }"
202
216
  >
203
- <Motion
217
+ <component
218
+ :is="isAnimated ? Motion : 'path'"
204
219
  as="path"
205
220
  d="M11.4 6C11.7314 6 12.0031 5.73069 11.97 5.40098C11.858 4.28448 11.4345 3.21734 10.7422 2.32414C9.92681 1.27227 8.78487 0.521339 7.49603 0.189501C6.20719 -0.142337 4.84459 -0.0362459 3.62264 0.491083C2.40069 1.01841 1.38875 1.93705 0.746034 3.10244C0.103318 4.26784 -0.133688 5.61385 0.0723077 6.92869C0.278303 8.24353 0.915607 9.45256 1.88394 10.3656C2.85228 11.2786 4.09668 11.8437 5.42136 11.972C6.5462 12.081 7.67471 11.8698 8.67877 11.3688C8.97528 11.2209 9.05806 10.8474 8.88125 10.5671C8.70444 10.2868 8.3351 10.2068 8.03498 10.3473C7.25926 10.7104 6.39684 10.8609 5.53709 10.7776C4.47735 10.6749 3.48182 10.2228 2.70715 9.49244C1.93249 8.76205 1.42264 7.79482 1.25785 6.74295C1.09305 5.69108 1.28265 4.61427 1.79683 3.68195C2.311 2.74964 3.12055 2.01473 4.09811 1.59287C5.07567 1.171 6.16575 1.08613 7.19682 1.3516C8.2279 1.61707 9.14145 2.21782 9.79373 3.05932C10.3229 3.74201 10.6558 4.55173 10.7626 5.40155C10.8039 5.73033 11.0686 6 11.4 6Z"
206
221
  fill="currentColor"
@@ -208,7 +223,8 @@ const colorValue = computed(() => {
208
223
  :animate="{ rotate: [0, 360] }"
209
224
  :transition="{ duration: 0.7, ease: 'easeOut', repeat: Infinity, repeatDelay: 0.1 }"
210
225
  />
211
- <Motion
226
+ <component
227
+ :is="isAnimated ? Motion : 'rect'"
212
228
  as="rect"
213
229
  x="5.25"
214
230
  y="3"
@@ -227,17 +243,20 @@ const colorValue = computed(() => {
227
243
  r="0.875"
228
244
  fill="currentColor"
229
245
  />
230
- </Motion>
246
+ </component>
231
247
  </template>
232
248
 
233
249
  <template v-if="name === 'check-dashed'">
234
- <Motion as="g" clip-path="url(#clip0_5125_104)" tabindex="-1" :animate="{ opacity: [0.4, 1] }" :transition="{ duration: 0.3, ease: [0.645, 0.045, 0.355, 1] }">
250
+ <component
251
+ :is="isAnimated ? Motion : 'g'" as="g" clip-path="url(#clip0_5125_104)" tabindex="-1" :animate="{ opacity: [0.4, 1] }" :transition="{ duration: 0.3, ease: [0.645, 0.045, 0.355, 1] }"
252
+ >
235
253
  <path d="M12 6C12 9.31371 9.31371 12 6 12C2.68629 12 0 9.31371 0 6C0 2.68629 2.68629 0 6 0C9.31371 0 12 2.68629 12 6ZM1.2 6C1.2 8.65097 3.34903 10.8 6 10.8C8.65097 10.8 10.8 8.65097 10.8 6C10.8 3.34903 8.65097 1.2 6 1.2C3.34903 1.2 1.2 3.34903 1.2 6Z" fill="currentColor" />
236
254
  <rect x="5.25" width="1.5" height="12" fill="#F0FDF6" />
237
255
  <rect x="12" y="5.25" width="1.5" height="12" transform="rotate(90 12 5.25)" fill="#F0FDF6" />
238
256
  <rect x="10.773" y="9.71228" width="1.5" height="12" transform="rotate(135 10.773 9.71228)" fill="#F0FDF6" />
239
257
  <rect x="2.28769" y="10.7729" width="1.5" height="12" transform="rotate(-135 2.28769 10.7729)" fill="#F0FDF6" />
240
- <Motion
258
+ <component
259
+ :is="isAnimated ? Motion : 'path'"
241
260
  as="path"
242
261
  d="M3.5 6 L5.5 8 L8.5 4"
243
262
  stroke="currentColor"
@@ -250,7 +269,7 @@ const colorValue = computed(() => {
250
269
  :style="{ rotate: 4, scale: 0.85 }"
251
270
  :transition="{ duration: 0.3, ease: [0.645, 0.045, 0.355, 1] }"
252
271
  />
253
- </Motion>
272
+ </component>
254
273
  <defs>
255
274
  <clipPath id="clip0_5125_104">
256
275
  <rect width="12" height="12" fill="white" />
@@ -275,7 +294,8 @@ const colorValue = computed(() => {
275
294
 
276
295
  <template v-if="name === 'arrow-down'">
277
296
  <path d="M12 6C12 9.31371 9.31371 12 6 12C2.68629 12 0 9.31371 0 6C0 2.68629 2.68629 0 6 0C9.31371 0 12 2.68629 12 6ZM1.2 6C1.2 8.65097 3.34903 10.8 6 10.8C8.65097 10.8 10.8 8.65097 10.8 6C10.8 3.34903 8.65097 1.2 6 1.2C3.34903 1.2 1.2 3.34903 1.2 6Z" fill="#A3A3A3" />
278
- <Motion
297
+ <component
298
+ :is="isAnimated ? Motion : 'path'"
279
299
  as="path"
280
300
  d="M6 4V8M6 8L7.75 6.53682M6 8L4.25 6.53682"
281
301
  stroke="currentColor"
@@ -138,7 +138,7 @@ defineExpose({
138
138
  @click="$el.querySelector(isTextarea ? 'textarea' : 'input')?.focus()"
139
139
  >
140
140
  <div v-if="icon" flex mr-6px>
141
- <TelaIcon :name="icon" size="sm" text="gray-600" />
141
+ <TelaIcon :name="icon" size="sm" text="gray-400" />
142
142
  </div>
143
143
  <div
144
144
  flex="~ col" grow align-center h-full
@@ -183,7 +183,15 @@ function handlePointerDownOutside(event: any) {
183
183
  background-color: rgba(2, 9, 19, 0.24);
184
184
  position: fixed;
185
185
  inset: 0;
186
- animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
186
+
187
+ &[data-state="open"] {
188
+ animation: overlayShow 0.12s ease-out forwards;
189
+ }
190
+
191
+ &[data-state="closed"] {
192
+ animation: overlayHide 0.12s ease-out forwards;
193
+ animation-delay: 0s;
194
+ }
187
195
  }
188
196
 
189
197
  .DialogContent {
@@ -191,7 +199,15 @@ function handlePointerDownOutside(event: any) {
191
199
  overflow-x: hidden;
192
200
  width: 400px;
193
201
  padding: 24px;
194
- animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
202
+
203
+ &[data-state="open"] {
204
+ animation: contentShow 0.12s ease-out forwards;
205
+ }
206
+
207
+ &[data-state="closed"] {
208
+ animation: contentHide 0.12s ease-out forwards;
209
+ animation-delay: 0s;
210
+ }
195
211
  }
196
212
 
197
213
  .DialogContent:focus {
@@ -229,6 +245,15 @@ function handlePointerDownOutside(event: any) {
229
245
  }
230
246
  }
231
247
 
248
+ @keyframes overlayHide {
249
+ from {
250
+ opacity: 1;
251
+ }
252
+ to {
253
+ opacity: 0;
254
+ }
255
+ }
256
+
232
257
  @keyframes contentShow {
233
258
  from {
234
259
  opacity: 0;
@@ -239,4 +264,15 @@ function handlePointerDownOutside(event: any) {
239
264
  transform: scale(1);
240
265
  }
241
266
  }
267
+
268
+ @keyframes contentHide {
269
+ from {
270
+ opacity: 1;
271
+ transform: scale(1);
272
+ }
273
+ to {
274
+ opacity: 0;
275
+ transform: scale(0.96);
276
+ }
277
+ }
242
278
  </style>
@@ -10,7 +10,13 @@ import { computed } from 'vue'
10
10
  import type { HTMLAttributes } from 'vue'
11
11
  import ScrollBar from './scroll-bar.vue'
12
12
 
13
- const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes['class'] }>()
13
+ const props = withDefaults(
14
+ defineProps<ScrollAreaRootProps
15
+ & { class?: HTMLAttributes['class'], orientation?: 'vertical' | 'horizontal' | 'both' }>(),
16
+ {
17
+ orientation: 'vertical',
18
+ },
19
+ )
14
20
 
15
21
  const delegatedProps = computed(() => {
16
22
  const { class: _, ...delegated } = props
@@ -24,7 +30,8 @@ const delegatedProps = computed(() => {
24
30
  <ScrollAreaViewport class="h-full w-full rounded-[inherit]">
25
31
  <slot />
26
32
  </ScrollAreaViewport>
27
- <ScrollBar />
33
+ <ScrollBar v-if="props.orientation === 'vertical' || props.orientation === 'both'" orientation="vertical" />
34
+ <ScrollBar v-if="props.orientation === 'horizontal' || props.orientation === 'both'" orientation="horizontal" />
28
35
  <ScrollAreaCorner />
29
36
  </ScrollAreaRoot>
30
37
  </template>
@@ -28,7 +28,7 @@ function selectTab(value: string) {
28
28
 
29
29
  <template>
30
30
  <div
31
- rounded-full flex items-center gap-2px select-none p-2px min-w-68px bg-gray-100
31
+ rounded-full flex items-center gap-2px select-none p-2px min-w-68px bg-gray-200
32
32
  :class="[
33
33
  props.size === 'small' ? 'min-h-24px' : 'min-h-28px',
34
34
  props.disabled && 'opacity-50 pointer-events-none cursor-not-allowed',
@@ -21,6 +21,10 @@ A status component that displays different workflow and task statuses with appro
21
21
 
22
22
  <Canvas of={StatusStories.AnimatedTransitions} />
23
23
 
24
+ ### Without Icon Animation
25
+
26
+ <Canvas of={StatusStories.WithoutIconAnimation} />
27
+
24
28
  ### Basic Usage
25
29
 
26
30
  ```vue
@@ -83,6 +87,7 @@ type Variant =
83
87
  type StatusProps = {
84
88
  variant?: Variant
85
89
  label?: string
90
+ isIconAnimated?: boolean
86
91
  }
87
92
  ```
88
93
 
@@ -158,3 +158,15 @@ export const AnimatedTransitions: Story = {
158
158
  layout: 'centered',
159
159
  },
160
160
  }
161
+
162
+ export const WithoutIconAnimation: Story = {
163
+ render: () => {
164
+ return {
165
+ components: { Status },
166
+ template: '<Status variant="pending-run" :is-icon-animated="false" />',
167
+ }
168
+ },
169
+ parameters: {
170
+ layout: 'centered',
171
+ },
172
+ }
@@ -14,8 +14,10 @@ type StatusConfig = {
14
14
  const props = withDefaults(defineProps<{
15
15
  variant?: TelaStatusVariant
16
16
  label?: string
17
+ isIconAnimated?: boolean
17
18
  }>(), {
18
19
  variant: 'running',
20
+ isIconAnimated: true,
19
21
  })
20
22
 
21
23
  const variantConfig: Record<TelaStatusVariant, StatusConfig> = {
@@ -385,7 +387,7 @@ const SPRING_CONFIG = {
385
387
  >
386
388
  <div :class="cn('relative z-1 p-[2px]', backgroundColor)">
387
389
  <Motion :key="`icon-${iconName}`" v-bind="iconVariants()">
388
- <TelaIconCustom :name="iconName" :color="iconColor" />
390
+ <TelaIconCustom :name="iconName" :color="iconColor" :is-animated="isIconAnimated" />
389
391
  </Motion>
390
392
  </div>
391
393
  <Motion
@@ -403,18 +405,3 @@ const SPRING_CONFIG = {
403
405
  </div>
404
406
  </MotionConfig>
405
407
  </template>
406
-
407
- <style scoped>
408
- .animate-shine {
409
- animation: shine 6s 0.05s linear infinite;
410
- }
411
-
412
- @keyframes shine {
413
- 0% {
414
- background-position: 350% 0;
415
- }
416
- 100% {
417
- background-position: -350% 0;
418
- }
419
- }
420
- </style>
@@ -0,0 +1,130 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+
4
+ interface Todo {
5
+ id: string
6
+ content: string
7
+ status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
8
+ }
9
+
10
+ const props = withDefaults(defineProps<{
11
+ todos: Todo[]
12
+ title?: string
13
+ completedText?: string
14
+ showMoreText?: string
15
+ showLessText?: string
16
+ class?: HTMLAttributes['class']
17
+ contentClass?: HTMLAttributes['class']
18
+ footerClass?: HTMLAttributes['class']
19
+ }>(), {
20
+ title: 'To-do-list',
21
+ completedText: 'completed',
22
+ showMoreText: 'Show more',
23
+ showLessText: 'Show less',
24
+ })
25
+
26
+ const isExpanded = ref(false)
27
+
28
+ const contentMeasure = useTemplateRef<HTMLElement>('contentMeasure')
29
+ const { height: heightContent } = useElementSize(contentMeasure)
30
+
31
+ const computedHeightContent = computed(() => {
32
+ if (heightContent.value > 0) {
33
+ return `${heightContent.value}px`
34
+ }
35
+ return undefined
36
+ })
37
+
38
+ const completedCount = computed(() => props.todos.filter(t => t.status === 'completed').length)
39
+ const completedPercent = computed(() => Math.round((completedCount.value / props.todos.length) * 100))
40
+
41
+ const sortedTodos = computed(() => {
42
+ const order: Record<Todo['status'], number> = { in_progress: 0, completed: 1, pending: 2, cancelled: 3 }
43
+ const sorted = [...props.todos].sort((a, b) => order[a.status] - order[b.status])
44
+
45
+ if (!isExpanded.value && props.todos.length > 3) {
46
+ return sorted.slice(0, 3)
47
+ }
48
+
49
+ return sorted
50
+ })
51
+
52
+ function getIconName(status: Todo['status']) {
53
+ if (status === 'completed')
54
+ return 'check'
55
+
56
+ if (status === 'in_progress')
57
+ return 'circle-notch'
58
+
59
+ if (status === 'cancelled')
60
+ return 'close'
61
+
62
+ return 'circle'
63
+ }
64
+
65
+ function getIconColor(status: Todo['status']) {
66
+ if (status === 'completed')
67
+ return 'gray-700'
68
+
69
+ return 'neutral-400'
70
+ }
71
+
72
+ const showMoreTextWithCount = computed(() => {
73
+ const parts = props.showMoreText.split(' ')
74
+ const count = (props.todos.length - sortedTodos.value.length).toString()
75
+
76
+ return parts.join(` ${count} `)
77
+ })
78
+ </script>
79
+
80
+ <template>
81
+ <div
82
+ px-12px pt-12px bg-background rounded-16px overflow-hidden
83
+ :class="cn(!isExpanded && todos.length <= 3 && 'pb-10px', props.class)"
84
+ >
85
+ <div
86
+ transition-height duration-200 ease-out
87
+ :style="computedHeightContent ? { height: computedHeightContent } : {}"
88
+ >
89
+ <div ref="contentMeasure">
90
+ <div flex="~ col" gap-10px :class="props.contentClass">
91
+ <div flex items-center justify-between pl-4px pb-10px border-b-0.5px border>
92
+ <h5 heading-h5-semibold text-primary>
93
+ {{ $t('workflow.agent.tools.todoList') }}
94
+ </h5>
95
+ <p body-14-medium text-secondary>
96
+ <TelaAnimatedNumber :value="completedPercent" />% {{ $t('workflow.agent.tools.completed') }}
97
+ </p>
98
+ </div>
99
+ <div flex="~ col" gap-7px px-1px>
100
+ <div v-for="todo in sortedTodos" :key="todo.id" relative flex items-center gap-8px py-1px>
101
+ <TelaIconCustom :name="getIconName(todo.status)" :color="getIconColor(todo.status)" shrink-0 :is-animated="false" />
102
+ <span
103
+ body-14-medium truncate w="90%"
104
+ :class="{
105
+ 'text-primary': (todo.status === 'completed' || todo.status === 'in_progress'),
106
+ 'text-tertiary': todo.status === 'pending',
107
+ 'text-tertiary line-through': todo.status === 'cancelled',
108
+ }"
109
+ >
110
+ {{ todo.content }}
111
+ </span>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ <div flex="~ col" :class="props.footerClass">
118
+ <button
119
+ v-if="todos.length > 3"
120
+ type="button"
121
+ relative z-10 items-center justify-center bg-background h-32px pb-4px mx--12px w="calc(100% + 24px)"
122
+ @click="isExpanded = !isExpanded"
123
+ >
124
+ <span body-12-semibold text-primary>
125
+ {{ isExpanded ? props.showLessText : showMoreTextWithCount }}
126
+ </span>
127
+ </button>
128
+ </div>
129
+ </div>
130
+ </template>
@@ -0,0 +1,182 @@
1
+ <script setup lang="ts">
2
+ interface ToolUse {
3
+ name: string
4
+ input?: Record<string, any>
5
+ description?: string
6
+ }
7
+
8
+ interface ToolResult {
9
+ id: string
10
+ output: string
11
+ isError: boolean
12
+ }
13
+
14
+ interface ToolDetailsModalProps {
15
+ isOpen: boolean
16
+ toolUse: ToolUse
17
+ toolResult: ToolResult
18
+ }
19
+
20
+ const props = defineProps<ToolDetailsModalProps>()
21
+
22
+ const emit = defineEmits<{
23
+ close: []
24
+ }>()
25
+
26
+ const expanded = reactive<Record<string, boolean>>({})
27
+
28
+ const input = computed(() => {
29
+ return props.toolUse.input
30
+ })
31
+
32
+ function toggle(key: string) {
33
+ expanded[key] = !expanded[key]
34
+ }
35
+
36
+ function textForValue(value: any) {
37
+ if (typeof value === 'string')
38
+ return value
39
+ try {
40
+ return JSON.stringify(value, null, 2)
41
+ }
42
+ catch {
43
+ return String(value)
44
+ }
45
+ }
46
+
47
+ function needsToggle(value: any) {
48
+ return (textForValue(value) ?? '').length > 500
49
+ }
50
+
51
+ function closeDialog() {
52
+ emit('close')
53
+ }
54
+
55
+ function capitalize(string: string) {
56
+ return string.replace(/_/g, ' ').charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1)
57
+ }
58
+ </script>
59
+
60
+ <template>
61
+ <TelaModal
62
+ :model-value="isOpen"
63
+ compact
64
+ hide-dividers
65
+ :style="{
66
+ width: '480px',
67
+ maxHeight: '90vh',
68
+ overflow: 'hidden',
69
+ padding: '24px',
70
+ }"
71
+ :is-close-icon="false"
72
+ @close="closeDialog"
73
+ >
74
+ <div flex="~ col" w-full h-full gap-24px>
75
+ <!-- Header -->
76
+ <div flex="~ col">
77
+ <div flex="~ row justify-between" items-start>
78
+ <h3 heading-h3-semibold text-primary>
79
+ {{ toolUse.name === 'Grep' ? `${$t('workflow.codeExecution')} ·` : '' }} {{ toolUse.name }}
80
+ </h3>
81
+ <TelaIconButton
82
+ icon="i-ph-x"
83
+ size="md"
84
+ color="secondary"
85
+ outline-none
86
+ p-8px mt--16px mr--16px
87
+ @click="closeDialog"
88
+ />
89
+ </div>
90
+ <p body-14-regular text-secondary mt-4px w="90%" class="[text-wrap:pretty]">
91
+ {{ $t('dashboard.modals.toolDetailsModal.description') }}
92
+ </p>
93
+ </div>
94
+
95
+ <!-- Tool Input Parameters -->
96
+ <div flex="~ col" gap-20px>
97
+ <div v-if="input && Object.keys(input).length > 0" text-secondary flex="~ col" gap-20px>
98
+ <div v-for="(value, key) in input" :key="key" flex="~ col" gap-6px>
99
+ <span body-12-medium text-primary>{{ capitalize(key) }}</span>
100
+
101
+ <div v-if="typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number'" border-0.5px border-gray-200 rounded-10px pl-2 py-2>
102
+ <TelaScrollArea>
103
+ <div max-h-300px pr-4>
104
+ <pre v-if="typeof value === 'string'" body-12-regular font-mono whitespace-pre-wrap break-words overflow-wrap-anywhere m-0>{{ value }}</pre>
105
+ <span v-else-if="typeof value === 'boolean'" block body-12-regular font-mono>{{ value ? 'true' : 'false' }}</span>
106
+ <span v-else-if="typeof value === 'number'" block body-12-regular font-mono>{{ value }}</span>
107
+ </div>
108
+ </TelaScrollArea>
109
+ </div>
110
+
111
+ <!-- Array value -->
112
+ <div v-else-if="Array.isArray(value)" flex="~ col" gap-4px>
113
+ <span text-xs text-gray-500>
114
+ {{ $t('reasoningSteps.itemsCount', { count: value.length }) }}
115
+ </span>
116
+ <div bg-gray-50 rounded-xl border-0.5px border-gray-200 p-3>
117
+ <div v-for="(item, idx) in value" :key="idx" text-sm py-0.5>
118
+ <span text-gray-400 mr-2 font-mono>{{ idx }}:</span>
119
+ <span font-mono>{{ typeof item === 'string' ? item : JSON.stringify(item) }}</span>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Object value -->
125
+ <div v-else-if="typeof value === 'object' && value !== null" flex="~ col" gap-4px>
126
+ <span text-xs text-gray-500>
127
+ {{ $t('reasoningSteps.propertiesCount', { count: Object.keys(value).length }) }}
128
+ </span>
129
+ <div bg-gray-50 rounded-xl border-0.5px border-gray-200 p-3>
130
+ <div
131
+ :style="{ maxHeight: expanded[String(key)] ? 'none' : '400px', overflow: expanded[String(key)] ? 'visible' : 'hidden' }"
132
+ text-sm font-mono whitespace-pre-wrap break-words overflow-wrap-anywhere
133
+ >
134
+ <pre m-0 break-words overflow-wrap-anywhere>{{ textForValue(value) }}</pre>
135
+ </div>
136
+ <div v-if="needsToggle(value)" mt-2 flex justify-center>
137
+ <button type="button" text-sm text-primary-600 hover:opacity-80 flex items-center gap-2 @click.prevent="toggle(String(key))">
138
+ {{ expanded[String(key)] ? $t('reasoningSteps.showLess') : $t('reasoningSteps.showMore') }}
139
+ <i class="i-ph-caret-down" :style="{ transform: expanded[String(key)] ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }" />
140
+ </button>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <!-- Null value -->
146
+ <div v-else-if="value === null" bg-gray-50 rounded-xl border-0.5px border-gray-200 p-3>
147
+ <span text-sm font-mono text-gray-500>null</span>
148
+ </div>
149
+
150
+ <!-- Fallback for other types -->
151
+ <div v-else bg-gray-50 rounded-xl border-0.5px border-gray-200 p-3>
152
+ <div :style="{ maxHeight: expanded[String(key)] ? 'none' : '400px', overflow: expanded[String(key)] ? 'visible' : 'hidden' }">
153
+ <pre text-sm font-mono>{{ textForValue(value) }}</pre>
154
+ </div>
155
+ <div v-if="needsToggle(value)" mt-2 flex justify-center>
156
+ <button type="button" text-sm text-primary-600 hover:opacity-80 flex items-center gap-2 @click.prevent="toggle(String(key))">
157
+ {{ expanded[String(key)] ? $t('reasoningSteps.showLess') : $t('reasoningSteps.showMore') }}
158
+ <i class="i-ph-caret-down" :style="{ transform: expanded[String(key)] ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }" />
159
+ </button>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ <div flex="~ col" gap-6px>
165
+ <span body-12-medium text-primary>{{ $t('workflow.result') }}</span>
166
+ <div v-if="toolResult" border-0.5px border-gray-200 rounded-10px pl-2 py-2>
167
+ <TelaScrollArea>
168
+ <div max-h-300px pr-4>
169
+ <span block body-12-regular text-secondary font-mono>
170
+ {{ toolResult.output }}
171
+ </span>
172
+ </div>
173
+ </TelaScrollArea>
174
+ </div>
175
+ <div v-else rounded-10px overflow-hidden>
176
+ <TelaSkeleton w-full h-30px rounded-none bg-gray-200 />
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </TelaModal>
182
+ </template>
@@ -0,0 +1,145 @@
1
+ <script setup lang="ts">
2
+ import ToolUseDisplayDetailsModal from './tool-use-display-details-modal.vue'
3
+
4
+ interface ToolUse {
5
+ name: string
6
+ input?: Record<string, any>
7
+ description?: string
8
+ }
9
+
10
+ interface ToolResult {
11
+ id: string
12
+ output: string
13
+ isError: boolean
14
+ }
15
+
16
+ const props = withDefaults(defineProps<{
17
+ toolUse: ToolUse
18
+ toolResult: ToolResult
19
+ isRunning?: boolean
20
+ detailsButtonLabel?: string
21
+ toolRunningLabel?: string
22
+ toolExecutedLabel?: string
23
+ showMoreLabel?: string
24
+ showLessLabel?: string
25
+ codeExecutionLabel?: string
26
+ itemsLabel?: string
27
+ propertiesLabel?: string
28
+ }>(), {
29
+ toolRunningLabel: 'Tool running',
30
+ toolExecutedLabel: 'Tool executed',
31
+ showMoreLabel: 'Show more',
32
+ showLessLabel: 'Show less',
33
+ codeExecutionLabel: 'Code execution',
34
+ detailsButtonLabel: 'Details',
35
+ itemsLabel: 'items',
36
+ propertiesLabel: 'properties',
37
+ })
38
+
39
+ const isToolDetailsModalOpen = ref(false)
40
+
41
+ const input = computed(() => {
42
+ const rawInput = props.toolUse.input ?? {}
43
+
44
+ return Object.fromEntries(
45
+ Object.entries(rawInput).filter(([key]) => key !== 'description' && key !== 'content').slice(0, 1),
46
+ )
47
+ })
48
+
49
+ function textForValue(value: any) {
50
+ if (typeof value === 'string')
51
+ return value
52
+ try {
53
+ return JSON.stringify(value, null, 2)
54
+ }
55
+ catch {
56
+ return String(value)
57
+ }
58
+ }
59
+
60
+ function openDetailsModal() {
61
+ isToolDetailsModalOpen.value = true
62
+ }
63
+
64
+ function closeDetailsModal() {
65
+ isToolDetailsModalOpen.value = false
66
+ }
67
+ </script>
68
+
69
+ <template>
70
+ <div v-if="isRunning || (input && Object.keys(input).length > 0)" px-14px py-12px bg-background rounded-16px>
71
+ <div pb-12px border-b-0.5px border>
72
+ <div flex items-center gap-6px>
73
+ <TelaIconCustom v-if="isRunning" name="circle-notch" size="12px" :is-animated="true" />
74
+ <TelaIconCustom v-else name="check" size="12px" :is-animated="false" />
75
+ <p body-12-regular text-black-700>
76
+ {{ isRunning ? toolRunningLabel : toolExecutedLabel }}
77
+ </p>
78
+ </div>
79
+ </div>
80
+ <div v-if="input && Object.keys(input).length > 0" mt-12px>
81
+ <div v-for="(value, key) in input" :key="key" flex items-center gap-8px>
82
+ <div flex items-center justify-center shrink-0 w-24px h-24px bg-lowered rounded-7px>
83
+ <TelaIcon name="i-ph-code-bold" color="gray-500" />
84
+ </div>
85
+
86
+ <div flex-1 min-w-0 flex items-center justify-between gap-8px>
87
+ <div flex="~ col" min-w-0 flex-1>
88
+ <p body-12-medium text-primary>
89
+ {{ toolUse.name === 'Grep' ? `${codeExecutionLabel} ·` : '' }} {{ toolUse.name }}
90
+ </p>
91
+
92
+ <p v-if="typeof value === 'string'" body-12-regular text-secondary line-clamp-2>
93
+ {{ value }}
94
+ </p>
95
+
96
+ <span v-else-if="typeof value === 'boolean'" block text-sm font-mono text-gray-700>
97
+ {{ value ? 'true' : 'false' }}
98
+ </span>
99
+
100
+ <span v-else-if="typeof value === 'number'" block text-sm font-mono text-gray-700>
101
+ {{ value }}
102
+ </span>
103
+
104
+ <div v-else-if="Array.isArray(value)">
105
+ <div block text-xs text-gray-500 mb-1>
106
+ {{ value.length }} {{ itemsLabel }}
107
+ </div>
108
+ <div bg-gray-50 rounded-xl border-0.5px border-gray-200 p-3>
109
+ <div v-for="(item, idx) in value" :key="idx" text-sm text-gray-700 py-0.5>
110
+ <span text-gray-400 mr-2 font-mono>{{ idx }}:</span>
111
+ <span font-mono>{{ typeof item === 'string' ? item : JSON.stringify(item) }}</span>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <div v-else-if="typeof value === 'object' && value !== null">
117
+ <div block text-xs text-gray-500 mb-1>
118
+ {{ Object.keys(value).length }} {{ propertiesLabel }}
119
+ </div>
120
+ <div bg-gray-50 rounded-xl border-0.5px border-gray-200 p-3>
121
+ <pre text-sm text-gray-700 font-mono whitespace-pre-wrap break-words overflow-wrap-anywhere>{{ textForValue(value) }}</pre>
122
+ </div>
123
+ </div>
124
+
125
+ <span v-else-if="value === null" block text-sm font-mono text-gray-700>null</span>
126
+
127
+ <div v-else bg-gray-50 rounded-xl border-0.5px border-gray-200 p-3>
128
+ <pre text-sm text-gray-700 font-mono>{{ textForValue(value) }}</pre>
129
+ </div>
130
+ </div>
131
+ <TelaButton variant="secondary" size="sm" shrink-0 @click="openDetailsModal">
132
+ {{ detailsButtonLabel }}
133
+ </TelaButton>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <ToolUseDisplayDetailsModal
140
+ :is-open="isToolDetailsModalOpen"
141
+ :tool-use="toolUse"
142
+ :tool-result="toolResult"
143
+ @close="closeDetailsModal"
144
+ />
145
+ </template>
package/css/text.css CHANGED
@@ -20,3 +20,16 @@ h5 {
20
20
  h6 {
21
21
  --at-apply: 'text-h6 text-gray-950';
22
22
  }
23
+
24
+ .animate-shine {
25
+ animation: shine 6s 0.05s linear infinite;
26
+ }
27
+
28
+ @keyframes shine {
29
+ 0% {
30
+ background-position: 350% 0;
31
+ }
32
+ 100% {
33
+ background-position: -350% 0;
34
+ }
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",