@meistrari/tela-build 1.52.1 → 1.53.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.
@@ -25,40 +25,50 @@ const resolvedVariant = computed(() =>
25
25
  || 'primary',
26
26
  )
27
27
 
28
+ const slots = useSlots()
29
+
30
+ const hasLeadingIcon = computed(() => !!((props.loading && props.leading) || slots.leading || (props.icon && props.leading)))
31
+ const hasTrailingIcon = computed(() => !!((props.loading && !props.leading) || slots.trailing || (props.icon && !props.leading)))
32
+
28
33
  const sizeStyle = computed(() => (({
29
- sm: 'text-12px leading-16px font-580 tracking-normal px-10px py-4px rounded-9px',
30
- md: 'text-14px leading-18px font-580 -tracking-0.15px px-12px py-7px rounded-11px [&>svg]:text-10px',
31
- lg: 'text-16px leading-20px font-580 -tracking-0.15px px-16px py-10px rounded-13px [&>svg]:text-12px',
34
+ sm: '[--padding-x:10px] text-[12px] leading-[16px] tracking-normal py-[4px] rounded-[9px]',
35
+ md: '[--padding-x:12px] text-[14px] leading-[18px] -tracking-[0.15px] py-[7px] rounded-[11px]',
36
+ lg: '[--padding-x:16px] text-[16px] leading-[20px] -tracking-[0.15px] py-[10px] rounded-[13px]',
32
37
  }) as Record<ButtonSize, string>)[resolvedSize.value] ?? '')
33
38
 
39
+ const paddingStyle = computed(() => [
40
+ hasLeadingIcon.value ? 'pl-[calc(var(--padding-x)_-_2px)]' : 'pl-[var(--padding-x)]',
41
+ hasTrailingIcon.value ? 'pr-[calc(var(--padding-x)_-_2px)]' : 'pr-[var(--padding-x)]',
42
+ ].join(' '))
43
+
34
44
  const variantStyle = computed(() => (({
35
45
  'primary': fold`
36
- bg-neutral-900 text-white
37
- hover:bg-neutral-800
38
- active:bg-neutral-700
46
+ bg-btn-primary text-inverse
47
+ hover:bg-btn-primary-hover
48
+ active:bg-btn-primary-active
39
49
  focus-visible:ring-0.5px focus-visible:ring-cyan-600
40
50
  `,
41
51
  'secondary': fold`
42
- bg-white text-neutral-900 border border-0.5px
52
+ bg-btn-secondary text-contrast border border-0.5px
43
53
  [box-shadow:0_1px_6px_0_rgba(103,127,148,0.05)]
44
- hover:bg-subtle hover:border-strong
45
- active:bg-muted active:border-neutral-400/60
54
+ hover:bg-btn-secondary-hover hover:border-strong
55
+ active:bg-btn-secondary-active active:border-neutral-400/60
46
56
  focus-visible:ring-0.5px focus-visible:ring-cyan-600
47
57
  `,
48
58
  'ghost': fold`
49
- bg-transparent text-gray-900
50
- hover:bg-gray-100
59
+ bg-btn-ghost text-contrast
60
+ hover:bg-btn-ghost-hover
51
61
  focus-visible:ring-0.5px focus-visible:ring-cyan-600
52
62
  `,
53
63
  'disabled': fold`
54
- bg-gray-100 text-gray-400 !cursor-not-allowed
64
+ bg-btn-disabled text-tertiary !cursor-not-allowed
55
65
  `,
56
66
  'loading': fold`
57
- bg-gray-200 text-gray-600
67
+ bg-btn-loading text-neutral-600
58
68
  `,
59
69
  'danger': fold`
60
- bg-red-100 text-red-500
61
- hover:bg-red-100/80
70
+ bg-btn-danger text-red-500
71
+ hover:bg-btn-danger-hover
62
72
  focus:outline-red-600
63
73
  `,
64
74
  'ghost-danger': fold`
@@ -67,14 +77,26 @@ const variantStyle = computed(() => (({
67
77
  focus:outline-red-600
68
78
  `,
69
79
  'dark': fold`
70
- bg-white/4 text-white border-0.5px border-white/16
71
- hover:bg-white/8 active:bg-white/10
80
+ bg-btn-dark text-white border-0.5px border-white/16
81
+ hover:bg-btn-dark-hover active:bg-btn-dark-active
72
82
  focus-visible:ring-0.5px focus-visible:ring-cyan-600
73
83
  `,
74
84
  }) as Record<ButtonVariant | 'disabled' | 'loading', string>)[resolvedVariant.value] ?? '')
75
85
 
86
+ const iconColorStyle = computed(() => (({
87
+ 'primary': 'icon-inverse',
88
+ 'secondary': 'icon-secondary',
89
+ 'ghost': 'icon-secondary',
90
+ 'disabled': 'icon-subtle',
91
+ 'loading': 'icon-tertiary',
92
+ 'danger': 'red-500',
93
+ 'ghost-danger': 'red-700',
94
+ 'dark': 'icon-subtle',
95
+ }) as Record<ButtonVariant | 'disabled' | 'loading', string>)[resolvedVariant.value] ?? '')
96
+
76
97
  const resolvedStyle = computed(() => [
77
98
  sizeStyle.value,
99
+ paddingStyle.value,
78
100
  variantStyle.value,
79
101
  ].join(' '))
80
102
 
@@ -100,19 +122,20 @@ const iconSize = computed(() => {
100
122
  <component
101
123
  :is="tag"
102
124
  :to="to"
103
- :class="resolvedStyle" ease-out duration-40 flex items-center justify-center gap-6px
125
+ :class="cn(
126
+ 'box-border font-540 select-none flex items-center justify-center gap-6px ease-out duration-40',
127
+ disabled ? 'cursor-not-allowed' : 'cursor-pointer',
128
+ resolvedStyle,
129
+ )"
104
130
  :disabled="disabled"
105
- :cursor="disabled ? 'not-allowed' : 'pointer'"
106
131
  :target="target"
107
- border-box
108
- select-none
109
132
  >
110
- <TelaIconSpinner v-if="loading && leading" animate-spin />
133
+ <TelaIconSpinner v-if="loading && leading" class="animate-spin" />
111
134
  <slot v-else-if="$slots.leading" name="leading" />
112
- <TelaIcon v-else-if="icon && leading" :name="icon" :size="iconSize" min-h-14px min-w-14px />
135
+ <TelaIcon v-else-if="icon && leading" :name="icon" :size="iconSize" :color="iconColorStyle" class="min-h-14px min-w-14px" />
113
136
  <slot />
114
- <TelaIconSpinner v-if="loading && !leading" animate-spin ml-0.5 />
137
+ <TelaIconSpinner v-if="loading && !leading" class="animate-spin min-h-14px min-w-14px" />
115
138
  <slot v-else-if="$slots.trailing" name="trailing" />
116
- <TelaIcon v-else-if="icon && !leading" :name="icon" :size="iconSize" />
139
+ <TelaIcon v-else-if="icon && !leading" :name="icon" :size="iconSize" :color="iconColorStyle" class="min-h-14px min-w-14px" />
117
140
  </component>
118
141
  </template>
@@ -15,7 +15,7 @@ const delegatedProps = reactiveOmit(props, 'class')
15
15
  <template>
16
16
  <ComboboxGroup
17
17
  v-bind="delegatedProps"
18
- :class="cn('overflow-hidden py-4px px-4px', props.class)"
18
+ :class="cn('overflow-hidden p-[4px]', props.class)"
19
19
  >
20
20
  <slot />
21
21
  </ComboboxGroup>
@@ -13,7 +13,7 @@ const delegatedProps = reactiveOmit(props, 'class')
13
13
  <template>
14
14
  <ComboboxItemIndicator
15
15
  v-bind="delegatedProps"
16
- :class="cn('text-gray-500', props.class)"
16
+ :class="cn('text-icon-secondary', props.class)"
17
17
  >
18
18
  <slot />
19
19
  </ComboboxItemIndicator>
@@ -8,6 +8,8 @@ import {
8
8
  } from 'reka-ui'
9
9
  import type { ComboboxInputEmits, ComboboxInputProps } from 'reka-ui'
10
10
 
11
+ defineOptions({ inheritAttrs: false })
12
+
11
13
  const props = defineProps<ComboboxInputProps & {
12
14
  class?: HTMLAttributes['class']
13
15
  }>()
@@ -20,10 +22,18 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
20
22
  </script>
21
23
 
22
24
  <template>
23
- <ComboboxInput
24
- v-bind="forwarded"
25
- :class="cn('flex h-10 w-full bg border-b-[0.5px] border-border rounded-xl px-3 py-1 body-14-regular text-primary transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-tertiary focus-visible:bg focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50', props.class)"
26
- >
27
- <slot />
28
- </ComboboxInput>
25
+ <div class="relative">
26
+ <span class="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center pl-[13px]">
27
+ <TelaIcon name="i-ph-magnifying-glass-bold" size="12px" class="text-tertiary" />
28
+ </span>
29
+ <ComboboxInput
30
+ v-bind="{ ...forwarded, ...$attrs }"
31
+ :class="cn('flex h-[36px] w-full bg border-b-[0.5px] border-border pl-32px pr-3 body-14-regular text-primary transition-colors placeholder:text-tertiary focus-visible:bg focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50', $slots.deleteSearchValue && 'pr-30px', props.class)"
32
+ >
33
+ <slot />
34
+ </ComboboxInput>
35
+ <div v-if="$slots.deleteSearchValue" class="absolute inset-y-0 end-0 flex items-center justify-center px-2.5">
36
+ <slot name="deleteSearchValue" />
37
+ </div>
38
+ </div>
29
39
  </template>
@@ -19,7 +19,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
19
19
  <template>
20
20
  <ComboboxItem
21
21
  v-bind="forwarded"
22
- :class="cn('relative flex cursor-pointer gap-2 select-none justify-between items-center rounded-lg px-2 py-1.5 text-sm font-medium outline-none data-[highlighted]:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
22
+ :class="cn('relative flex cursor-pointer gap-2 select-none justify-between items-center rounded-[12px] pl-[10px] pr-[8px] py-[7px] body-14-medium outline-none data-[highlighted]:bg-muted data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
23
23
  >
24
24
  <slot />
25
25
  </ComboboxItem>
@@ -15,7 +15,7 @@ const forwarded = useForwardProps(delegatedProps)
15
15
  <template>
16
16
  <ComboboxLabel
17
17
  v-bind="forwarded"
18
- :class="cn('px-2 py-1 body-12-semibold text-secondary', props.class)"
18
+ :class="cn('py-[4px] pl-[10px] body-12-semibold text-secondary', props.class)"
19
19
  >
20
20
  <slot />
21
21
  </ComboboxLabel>
@@ -28,20 +28,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
28
28
  </script>
29
29
 
30
30
  <template>
31
- <template v-if="props.disablePortal">
31
+ <ComboboxPortal :disabled="props.disablePortal">
32
32
  <ComboboxContent
33
33
  v-bind="forwarded"
34
- :class="cn('ComboboxContent z-[9999] shadow-lg outline-none rounded-xl bg-white-1000 border-[0.5px] border-gray-200 pointer-events-auto', props.class)"
35
- >
36
- <ComboboxViewport>
37
- <slot />
38
- </ComboboxViewport>
39
- </ComboboxContent>
40
- </template>
41
- <ComboboxPortal v-else>
42
- <ComboboxContent
43
- v-bind="forwarded"
44
- :class="cn('ComboboxContent z-[9999] shadow-lg outline-none rounded-xl bg-white-1000 border-[0.5px] border-gray-200 pointer-events-auto', props.class)"
34
+ :class="cn('ComboboxContent z-[9999] [box-shadow:0_4px_16px_0_rgba(85,91,98,0.14)] overflow-hidden outline-none rounded-[16px] bg border-0.5px border pointer-events-auto', props.class)"
45
35
  >
46
36
  <ComboboxViewport>
47
37
  <slot />
@@ -263,24 +263,14 @@ watch(innerValue, (value) => {
263
263
  @pointer-down-outside="closeOnOutsideClick ? undefined : $event.preventDefault()"
264
264
  @escape-key-down="closeOnEscape ? undefined : $event.preventDefault()"
265
265
  >
266
- <div class="relative">
267
- <span class="absolute inset-y-0 start-0 flex items-center justify-center px-2.5">
268
- <TelaIcon name="i-ph-magnifying-glass" size="12px" class="text-gray-400" />
269
- </span>
270
- <ComboboxInput
271
- v-model="search" class="pl-7 rounded-b-none"
272
- :placeholder="t('models.labels.search')"
273
- />
274
- <div v-if="search.length > 0" class="absolute inset-y-0 end-0 flex items-center justify-center px-2.5">
275
- <button class="flex items-center justify-center" @click="handleDeleteSearchValue">
276
- <TelaIcon name="i-ph-x-circle" size="16px" class="text-gray-400" />
277
- </button>
278
- </div>
279
- </div>
266
+ <ComboboxInput
267
+ v-model="search"
268
+ :placeholder="t('models.labels.search')"
269
+ />
280
270
  <Tabs v-model="selectedTab" :default-value="selectedTab">
281
271
  <TabsList class="sticky z-10 top-0 shrink-0 flex gap-2 w-full">
282
272
  <TabsIndicator />
283
- <TabsTrigger v-for="tab in tabs" :key="tab.tab" :value="tab.tab">
273
+ <TabsTrigger v-for="tab in tabs" :key="tab.tab" :value="tab.tab" size="sm">
284
274
  {{ tab.label }}
285
275
  </TabsTrigger>
286
276
  </TabsList>
@@ -315,7 +305,7 @@ watch(innerValue, (value) => {
315
305
  </TelaButton>
316
306
  </ComboboxEmpty>
317
307
  <template v-for="group in groupedOptions" :key="group.heading">
318
- <ComboboxGroup :heading="group.heading" class="py-2">
308
+ <ComboboxGroup :heading="group.heading">
319
309
  <ComboboxLabel v-if="!search.trim()" class="py-0 tracking-normal">
320
310
  {{ group.heading }}
321
311
  </ComboboxLabel>
@@ -1,12 +1,30 @@
1
1
  <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { reactiveOmit } from '@vueuse/core'
2
4
  import { ComboboxTrigger } from 'reka-ui'
3
5
  import type { ComboboxTriggerProps } from 'reka-ui'
4
6
 
5
- const props = defineProps<ComboboxTriggerProps>()
7
+ const props = defineProps<ComboboxTriggerProps & { class?: HTMLAttributes['class'] }>()
8
+
9
+ const delegatedProps = reactiveOmit(props, 'class')
6
10
  </script>
7
11
 
8
12
  <template>
9
- <ComboboxTrigger v-bind="props">
13
+ <ComboboxTrigger
14
+ v-bind="delegatedProps"
15
+ :class="cn(
16
+ 'group select-none flex items-center justify-between body-14-semibold gap-3 rounded-10px pl-11px pr-10px py-7px bg-white border-0.5px border transition hover:border-strong hover:bg-subtle',
17
+ 'data-[state=open]:text-secondary data-[state=open]:border-strong data-[state=open]:bg-subtle',
18
+ '[box-shadow:0_1px_6px_0_rgba(103,127,148,0.05)]',
19
+ props.class,
20
+ )"
21
+ >
10
22
  <slot />
23
+ <TelaIcon
24
+ name="i-ph-caret-down-bold"
25
+ size="15px"
26
+ color="icon-secondary"
27
+ class="shrink-0 transition-transform ease-out duration-150 group-data-[state=open]:rotate-180"
28
+ />
11
29
  </ComboboxTrigger>
12
30
  </template>
@@ -93,7 +93,7 @@ const meta: Meta<typeof Combobox> = {
93
93
 
94
94
  export default meta
95
95
 
96
- export const Default: StoryObj<{ options: Array<{ value: string; label: string }> }> = {
96
+ export const Default: StoryObj<{ options: Array<{ value: string, label: string }> }> = {
97
97
  render: args => ({
98
98
  components: { ComboboxRoot, ComboboxAnchor, ComboboxTrigger, TelaIcon, TelaButton, ComboboxInput, ComboboxList, ComboboxEmpty, ComboboxGroup, ComboboxItem, ComboboxItemIndicator, ComboboxLabel },
99
99
  setup() {
@@ -105,14 +105,14 @@ export const Default: StoryObj<{ options: Array<{ value: string; label: string }
105
105
  return selectedValue.value
106
106
  }
107
107
 
108
- return args.options.find((option: any) => option.value === selectedValue.value)
108
+ return args.options.find(option => option.value === selectedValue.value)
109
109
  })
110
110
 
111
111
  const filteredOptions = computed(() => {
112
112
  if (!search.value.trim()) {
113
113
  return args.options
114
114
  }
115
- return args.options.filter((option: any) =>
115
+ return args.options.filter(option =>
116
116
  option.label.toLowerCase().includes(search.value.toLowerCase()),
117
117
  )
118
118
  })
@@ -1,19 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { ref, computed } from 'vue'
3
2
  import type { Component, ComponentPublicInstance, Ref } from 'vue'
4
3
  import { useMagicKeys } from '@vueuse/core'
5
4
 
6
- import ComboboxRoot from './combobox-root.vue'
7
- import ComboboxAnchor from './combobox-anchor.vue'
8
- import ComboboxEmpty from './combobox-empty.vue'
9
- import ComboboxGroup from './combobox-group.vue'
10
- import ComboboxInput from './combobox-input.vue'
11
- import ComboboxItem from './combobox-item.vue'
12
- import ComboboxItemIndicator from './combobox-indicator.vue'
13
- import ComboboxList from './combobox-list.vue'
14
- import ComboboxLabel from './combobox-label.vue'
15
- import ComboboxTrigger from './combobox-trigger.vue'
16
-
17
5
  interface ComboboxOption {
18
6
  value: string
19
7
  label: string
@@ -200,6 +188,8 @@ const groupedOptions = computed(() => {
200
188
  })).filter(group => group.children.length > 0)
201
189
  })
202
190
 
191
+ const isEmpty = computed(() => groupedOptions.value.every(group => group.children.length === 0))
192
+
203
193
  const handleDeleteSearchValue = () => search.value = ''
204
194
 
205
195
  const keys = useMagicKeys()
@@ -324,17 +314,15 @@ watch(innerValue, (raw) => {
324
314
  </script>
325
315
 
326
316
  <template>
327
- <ComboboxRoot
317
+ <TelaComboboxRoot
328
318
  v-model="innerValue" by="value" :open="isOpen" :disabled="readOnly" @update:open="onOpenChange"
329
319
  @update:model-value="handleSelect"
330
320
  >
331
- <ComboboxAnchor>
332
- <ComboboxTrigger
321
+ <TelaComboboxAnchor>
322
+ <TelaComboboxTrigger
333
323
  as="button"
334
324
  tabindex="0"
335
325
  :class="cn(
336
- 'group select-none flex items-center justify-between gap-3 rounded-10px px-8px py-7px bg-white border-0.5px border transition hover:border-strong hover:bg-subtle data-[state=open]:border-strong data-[state=open]:bg-subtle',
337
- '[box-shadow:0_1px_6px_0_rgba(103,127,148,0.05)]',
338
326
  !compact && 'w-full !py-2.5 pl-3 pr-3',
339
327
  props.triggerClass,
340
328
  )"
@@ -342,324 +330,188 @@ watch(innerValue, (raw) => {
342
330
  <div :class="cn('flex items-center', !compact ? 'gap-2' : 'gap-1')">
343
331
  <div v-if="currentOption?.icon && labelIconVisible" :class="cn('group-data-[state=open]:opacity-70', props.iconClass)">
344
332
  <div v-if="currentOption?.icon && !(currentOption?.value === '')">
345
- <TelaIcon v-if="typeof currentOption.icon === 'string'" :name="currentOption.icon" :color="colorIcon" />
333
+ <TelaIcon v-if="typeof currentOption.icon === 'string'" :name="currentOption.icon" :color="colorIcon" class="shrink-0" />
346
334
  <Component
347
335
  :is="currentOption.icon"
348
336
  v-else
349
- :class="cn((currentOption?.label.startsWith('DeepSeek') || currentOption?.label.startsWith('Gemini')) && 'scale-75')"
337
+ :class="cn('shrink-0', (currentOption?.label.startsWith('DeepSeek') || currentOption?.label.startsWith('Gemini')) && 'scale-75')"
350
338
  />
351
339
  </div>
352
340
  <div v-if="!currentOption?.icon && currentOption?.externalIconSrc">
353
- <img :src="currentOption.externalIconSrc" :alt="labelModelIconAlt || currentOption?.label" w-16px h-16px>
341
+ <img :src="currentOption.externalIconSrc" :alt="labelModelIconAlt || currentOption?.label" class="w-16px h-16px">
354
342
  </div>
355
343
  </div>
356
- <span
357
- :class="cn('truncate text-primary group-data-[state=open]:text-secondary',
358
- compact ? 'text-sm font-580' : 'text-body-14-regular', props.labelClass)"
359
- >
344
+ <span :class="cn('truncate text-primary group-data-[state=open]:text-secondary', props.labelClass)">
360
345
  {{ currentOption?.triggerLabel || currentOption?.label }}
361
346
  </span>
362
347
  </div>
363
- <TelaIcon name="i-ph-caret-down-bold" color="icon-secondary" :class="cn('transition-all ease-out duration-150', isOpen && 'rotate-180')" size="15px" />
364
- </ComboboxTrigger>
365
- </ComboboxAnchor>
366
- <ComboboxList :class="cn('z-999', compact ? 'w-440px' : 'w-327px', props.contentClass)" :disable-portal="props.disablePortal" align="start">
367
- <div v-if="hasSearchbar" class="relative">
368
- <span class="absolute inset-y-0 start-0 flex items-center justify-center px-2.5">
369
- <TelaIcon name="i-ph-magnifying-glass" :size="compact ? '12px' : '14px'" class="text-tertiary" />
370
- </span>
371
- <ComboboxInput
372
- v-model="search" :class="cn('rounded-b-none', compact ? 'pl-7' : 'pl-8')"
373
- :placeholder="inputPlaceholder || labelSearchPlaceholder"
374
- />
375
- <div v-if="search.length > 0" class="absolute inset-y-0 end-0 flex items-center justify-center px-2.5">
376
- <button class="flex items-center justify-center" @click="handleDeleteSearchValue">
377
- <TelaIcon name="i-ph-x-circle" size="16px" class="text-tertiary" />
378
- </button>
379
- </div>
380
- </div>
381
- <template v-if="hasTabs">
382
- <TelaTabsRoot v-model="selectedTab" :default-value="selectedTab">
383
- <TelaTabsList class="sticky z-10 top-0 shrink-0 flex gap-2 w-full">
384
- <TelaTabsIndicator />
385
- <TelaTabsTrigger v-for="tab in tabs" :key="tab.tab" :value="tab.tab">
386
- {{ tab.label }}
387
- </TelaTabsTrigger>
388
- </TelaTabsList>
389
- <TelaTabsContent :value="selectedTab" class="max-h-262px overflow-y-auto no-scrollbar">
390
- <ComboboxEmpty
391
- v-if="groupedOptions.length === 0 || groupedOptions.every(g => g.children.length === 0)"
392
- class="flex flex-col items-center justify-center gap-5 px-7"
393
- >
394
- <div class="flex flex-col items-center justify-center gap-3">
395
- <TelaIcon name="i-ph-smiley-sad" size="24px" class="text-tertiary" />
396
- <span class="text-xl font-medium leading-none">
397
- {{ search.trim() && labelNoResultsWithSearch ? labelNoResultsWithSearch(search) : labelNoResults }}
398
- </span>
399
- <span v-if="search.trim()" class="inline-flex items-center gap-1.5 text-md text-secondary">
400
- {{ labelPress }}
401
- <div class="flex items-center gap-1">
402
- <div class="p-2px bg-lowered rounded">
403
- <TelaIcon name="i-ph-command" size="16px" />
404
- </div>
405
- <div class="p-2px bg-lowered rounded">
406
- <TelaIcon name="i-ph-backspace" size="16px" />
407
- </div>
408
- </div>
409
- {{ labelToClear }}
410
- </span>
348
+ </TelaComboboxTrigger>
349
+ </TelaComboboxAnchor>
350
+ <TelaComboboxList :class="cn('z-999', compact ? 'w-440px' : 'w-327px', props.contentClass)" :disable-portal="props.disablePortal" align="start">
351
+ <TelaComboboxInput
352
+ v-if="hasSearchbar"
353
+ v-model="search"
354
+ :placeholder="inputPlaceholder || labelSearchPlaceholder"
355
+ />
356
+ <TelaTabsRoot v-if="hasTabs" v-model="selectedTab">
357
+ <TelaTabsList class="sticky z-10 top-0 shrink-0 flex gap-2 w-full">
358
+ <TelaTabsIndicator />
359
+ <TelaTabsTrigger v-for="tab in tabs" :key="tab.tab" :value="tab.tab" size="sm">
360
+ {{ tab.label }}
361
+ </TelaTabsTrigger>
362
+ </TelaTabsList>
363
+ </TelaTabsRoot>
364
+ <div :class="cn('max-h-262px overflow-y-auto no-scrollbar', hasTabs && 'pt-2')">
365
+ <TelaComboboxEmpty
366
+ v-if="isEmpty"
367
+ class="flex flex-col items-center justify-center gap-5 px-7"
368
+ >
369
+ <div class="flex flex-col items-center justify-center gap-3">
370
+ <TelaIcon name="i-ph-smiley-sad" size="24px" class="text-tertiary" />
371
+ <span class="text-xl font-medium leading-none">
372
+ {{ search.trim() && labelNoResultsWithSearch ? labelNoResultsWithSearch(search) : labelNoResults }}
373
+ </span>
374
+ <span v-if="search.trim()" class="inline-flex items-center gap-1.5 text-md text-secondary">
375
+ {{ labelPress }}
376
+ <div class="flex items-center gap-1">
377
+ <div class="p-2px bg-lowered rounded">
378
+ <TelaIcon name="i-ph-command" size="16px" />
379
+ </div>
380
+ <div class="p-2px bg-lowered rounded">
381
+ <TelaIcon name="i-ph-backspace" size="16px" />
382
+ </div>
411
383
  </div>
412
- <TelaButton
413
- v-if="search.trim()" variant="secondary" size="sm"
414
- @click="handleDeleteSearchValue"
384
+ {{ labelToClear }}
385
+ </span>
386
+ </div>
387
+ <TelaButton
388
+ v-if="search.trim()" variant="secondary" size="sm"
389
+ @click="handleDeleteSearchValue"
390
+ >
391
+ {{ labelClear }}
392
+ </TelaButton>
393
+ </TelaComboboxEmpty>
394
+ <template v-for="group in groupedOptions" :key="group.heading">
395
+ <TelaComboboxGroup :heading="group.heading">
396
+ <TelaComboboxLabel v-if="!search.trim() && hasGroupingLabels">
397
+ {{ group.heading }}
398
+ </TelaComboboxLabel>
399
+ <template v-for="item in group.children" :key="item.value">
400
+ <TelaComboboxItem
401
+ :ref="(el) => item.children?.length && setNestedItemRef(item.value, el)"
402
+ :value="item"
403
+ :class="cn('group py-2', !compact && '!px-1.5')"
404
+ @mouseenter="handleItemMouseEnter(item)"
405
+ @select="(e) => item.children?.length && e.preventDefault()"
415
406
  >
416
- {{ labelClear }}
417
- </TelaButton>
418
- </ComboboxEmpty>
419
- <template v-for="group in groupedOptions" :key="group.heading">
420
- <ComboboxGroup :heading="group.heading">
421
- <ComboboxLabel v-if="!search.trim() && hasGroupingLabels">
422
- {{ group.heading }}
423
- </ComboboxLabel>
424
- <ComboboxItem v-for="item in group.children" :key="item.value" :value="item" :class="cn('group py-2', !compact && '!px-1.5')">
425
- <div class="flex items-center">
426
- <div :class="cn('flex items-center w-270px', compact ? 'gap-2' : 'gap-1.5')">
427
- <div v-if="item.icon" class="w-5 h-5 shrink-0 flex items-center justify-center">
428
- <TelaIcon
429
- v-if="typeof item.icon === 'string'" :name="item.icon" :color="colorIcon" :size="iconWithBackground?.size"
430
- :background-class="iconWithBackground?.backgroundClass"
431
- />
432
- <Component
433
- :is="item.icon"
434
- v-else
435
- :class="cn((item?.label.startsWith('DeepSeek') || item?.label.startsWith('Gemini')) && 'scale-75')"
436
- />
437
- </div>
438
- <div v-if="!item.icon && item?.externalIconSrc">
439
- <img :src="item.externalIconSrc" :alt="item.label" class="w-5 h-5">
440
- </div>
441
- <div class="flex flex-col">
442
- <div class="flex items-center gap-1">
443
- <span :class="cn('font-medium truncate max-w-140px', compact ? 'text-sm font-580' : 'text-body-14-regular', labelItemClass)">{{ item.label }}</span>
444
- <TelaBadge v-if="item.isMultiModal" variant="filled" class="py-[2px] bg-muted" text-class="leading-none">
445
- {{ labelMultimodal }}
446
- </TelaBadge>
447
- <span v-if="item.cost !== undefined" class="text-10px font-580 ml-1px">
448
- <span class="text-primary">{{ renderCostIndicator(item.cost)?.blackSymbols }}</span>
449
- <span class="text-neutral-300">{{ renderCostIndicator(item.cost)?.graySymbols }}</span>
450
- </span>
451
- </div>
452
- <span v-if="item.description" :class="cn('font-normal text-sm text-balance leading-none text-secondary', descriptionClass)">
453
- {{ item.description }}
454
- </span>
455
- <slot name="tags" :option="item" />
456
- </div>
407
+ <div class="flex items-center">
408
+ <div :class="cn('flex items-center w-270px', compact ? 'gap-2' : 'gap-1.5')">
409
+ <div v-if="item.icon" class="w-5 h-5 shrink-0 flex items-center justify-center">
410
+ <TelaIcon
411
+ v-if="typeof item.icon === 'string'" :name="item.icon" :color="colorIcon" :size="iconWithBackground?.size"
412
+ :background-class="iconWithBackground?.backgroundClass"
413
+ />
414
+ <Component
415
+ :is="item.icon"
416
+ v-else
417
+ :class="cn((item?.label.startsWith('DeepSeek') || item?.label.startsWith('Gemini')) && 'scale-75')"
418
+ />
457
419
  </div>
458
- <div class="flex gap-3">
459
- <div v-if="item.maxInputTokens" class="flex flex-col gap-1.5">
460
- <span class="text-tertiary leading-none text-9px uppercase tracking-wider font-semibold">
461
- {{ labelInputMax }}
462
- </span>
463
- <span class="text-tertiary leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-neutral-700">
464
- {{ handleFormatNumber(item.maxInputTokens) }}
465
- </span>
466
- </div>
467
- <div v-if="item.maxOutputTokens" class="flex flex-col gap-1.5">
468
- <span class="text-tertiary leading-none text-9px uppercase tracking-wider font-semibold">
469
- {{ labelOutputMax }}
470
- </span>
471
- <span class="text-tertiary leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-neutral-700">
472
- {{ handleFormatNumber(item.maxOutputTokens) }}
420
+ <div v-if="!item.icon && item?.externalIconSrc">
421
+ <img :src="item.externalIconSrc" :alt="item.label" class="w-5 h-5">
422
+ </div>
423
+ <div class="flex flex-col">
424
+ <div class="flex items-center gap-1">
425
+ <span :class="cn('font-medium truncate max-w-140px', compact ? 'text-sm font-580' : 'text-body-14-regular', labelItemClass)">{{ item.label }}</span>
426
+ <TelaBadge v-if="item.isMultiModal" variant="filled" class="py-[2px] bg-muted" text-class="leading-none">
427
+ {{ labelMultimodal }}
428
+ </TelaBadge>
429
+ <span v-if="item.cost !== undefined" class="text-10px font-580 ml-1px">
430
+ <span class="text-primary">{{ renderCostIndicator(item.cost)?.blackSymbols }}</span>
431
+ <span class="text-neutral-300">{{ renderCostIndicator(item.cost)?.graySymbols }}</span>
473
432
  </span>
474
433
  </div>
434
+ <span v-if="item.description" :class="cn('font-normal text-sm text-balance leading-none text-secondary', descriptionClass)">
435
+ {{ item.description }}
436
+ </span>
437
+ <slot name="tags" :option="item" />
475
438
  </div>
476
439
  </div>
477
- <div class="flex gap-2 items-center">
478
- <ComboboxItemIndicator>
479
- <TelaIcon name="i-ph-check" size="14px" />
480
- </ComboboxItemIndicator>
481
- </div>
482
- </ComboboxItem>
483
- </ComboboxGroup>
484
- </template>
485
- </TelaTabsContent>
486
- </TelaTabsRoot>
487
- </template>
488
- <template v-else>
489
- <div class="max-h-262px overflow-y-auto no-scrollbar">
490
- <ComboboxEmpty
491
- v-if="groupedOptions.length === 0 || groupedOptions.every(g => g.children.length === 0)"
492
- class="flex flex-col items-center justify-center gap-5 px-7"
493
- >
494
- <div class="flex flex-col items-center justify-center gap-3">
495
- <TelaIcon name="i-ph-smiley-sad" size="24px" class="text-tertiary" />
496
- <span class="text-xl font-medium leading-none">
497
- {{ search.trim() && labelNoResultsWithSearch ? labelNoResultsWithSearch(search) : labelNoResults }}
498
- </span>
499
- <span v-if="search.trim()" class="inline-flex gap-1.5 text-md text-secondary">
500
- {{ labelPress }}
501
- <div class="flex items-center gap-1">
502
- <div class="p-2px bg-lowered rounded">
503
- <TelaIcon name="i-ph-command" size="16px" />
504
- </div>
505
- <div class="p-2px bg-lowered rounded">
506
- <TelaIcon name="i-ph-backspace" size="16px" />
440
+ <div v-if="item.maxInputTokens || item.maxOutputTokens" class="flex gap-3">
441
+ <div v-if="item.maxInputTokens" class="flex flex-col gap-1.5">
442
+ <span class="text-tertiary leading-none text-9px uppercase tracking-wider font-semibold">
443
+ {{ labelInputMax }}
444
+ </span>
445
+ <span class="text-tertiary leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-neutral-700">
446
+ {{ handleFormatNumber(item.maxInputTokens) }}
447
+ </span>
448
+ </div>
449
+ <div v-if="item.maxOutputTokens" class="flex flex-col gap-1.5">
450
+ <span class="text-tertiary leading-none text-9px uppercase tracking-wider font-semibold">
451
+ {{ labelOutputMax }}
452
+ </span>
453
+ <span class="text-tertiary leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-neutral-700">
454
+ {{ handleFormatNumber(item.maxOutputTokens) }}
455
+ </span>
456
+ </div>
507
457
  </div>
508
458
  </div>
509
- {{ labelToClear }}
510
- </span>
511
- </div>
512
- <TelaButton
513
- v-if="search.trim()" variant="secondary" size="sm" class="rounded-lg"
514
- @click="handleDeleteSearchValue"
515
- >
516
- {{ labelClear }}
517
- </TelaButton>
518
- </ComboboxEmpty>
519
- <template v-for="group in groupedOptions" :key="group.heading">
520
- <ComboboxGroup :heading="group.heading">
521
- <ComboboxLabel v-if="!search.trim() && hasGroupingLabels">
522
- {{ group.heading }}
523
- </ComboboxLabel>
524
- <template v-for="item in group.children" :key="item.value">
525
- <ComboboxItem
526
- :ref="(el) => item.children?.length && setNestedItemRef(item.value, el)"
527
- :value="item"
528
- :class="cn('group/item py-2', !compact && '!px-1.5')"
529
- @mouseenter="handleItemMouseEnter(item)"
530
- @select="(e) => item.children?.length && e.preventDefault()"
459
+ <div class="flex gap-2 items-center">
460
+ <TelaIcon v-if="item.children?.length" name="i-ph-caret-right" size="14px" color="icon-tertiary" />
461
+ <TelaComboboxIndicator v-else>
462
+ <TelaIcon name="i-ph-check" size="14px" />
463
+ </TelaComboboxIndicator>
464
+ </div>
465
+ </TelaComboboxItem>
466
+ <!-- Nested submenu -->
467
+ <Teleport v-if="item.children?.length" to="body">
468
+ <div
469
+ v-if="expandedItem === item.value"
470
+ :style="submenuStyle"
471
+ class="min-w-200px bg border-0.5px border rounded-12px shadow-[0_10px_15px_-3px_rgba(0,0,0,0.1),0_4px_6px_-4px_rgba(0,0,0,0.1)] pointer-events-auto"
472
+ @mouseenter="expandedItem = item.value"
473
+ @pointerdown.stop
531
474
  >
532
- <div class="flex items-center">
533
- <div :class="cn('flex items-center w-250px', compact ? 'gap-2' : 'gap-1.5')">
534
- <div v-if="item.icon" class="w-5 h-5 shrink-0 flex items-center justify-center">
535
- <TelaIcon
536
- v-if="typeof item.icon === 'string'" :name="item.icon" :color="colorIcon" :size="iconWithBackground?.size"
537
- :background-class="iconWithBackground?.backgroundClass"
538
- />
539
- <Component
540
- :is="item.icon"
541
- v-else
542
- :class="cn((item?.label.startsWith('DeepSeek') || item?.label.startsWith('Gemini')) && 'scale-75')"
543
- />
544
- </div>
545
- <div v-if="!item.icon && item?.externalIconSrc">
546
- <img :src="item.externalIconSrc" :alt="item.label" class="w-5 h-5">
547
- </div>
548
- <div class="flex flex-col">
549
- <div class="flex items-center gap-1">
550
- <span :class="cn('font-medium truncate max-w-140px', compact ? 'text-sm font-580' : 'text-body-14-regular', labelItemClass)">{{ item.label }}</span>
551
- <TelaBadge v-if="item.isMultiModal" variant="filled" class="py-[2px] bg-muted" text-class="leading-none">
552
- {{ labelMultimodal }}
553
- </TelaBadge>
554
- <span v-if="item.cost !== undefined" class="text-10px font-580 ml-1px">
555
- <span class="text-primary">{{ renderCostIndicator(item.cost)?.blackSymbols }}</span>
556
- <span class="text-neutral-300">{{ renderCostIndicator(item.cost)?.graySymbols }}</span>
557
- </span>
475
+ <div class="p-4px">
476
+ <div
477
+ v-for="child in item.children"
478
+ :key="child.value"
479
+ :class="cn(
480
+ 'relative flex cursor-pointer select-none justify-between items-center rounded-lg px-2 py-2 text-sm font-medium text-primary outline-none hover:bg-muted',
481
+ !compact && '!px-1.5',
482
+ )"
483
+ @click.stop="handleChildSelect(child, $event)"
484
+ >
485
+ <div :class="cn('flex items-center', compact ? 'gap-2' : 'gap-1.5')">
486
+ <div v-if="child.icon" class="w-5 h-5 shrink-0 flex items-center justify-center">
487
+ <TelaIcon
488
+ v-if="typeof child.icon === 'string'" :name="child.icon" :color="colorIcon" :size="iconWithBackground?.size"
489
+ :background-class="iconWithBackground?.backgroundClass"
490
+ />
491
+ <Component :is="child.icon" v-else />
558
492
  </div>
559
- <span v-if="item.description" :class="cn('font-normal text-sm leading-none text-secondary', descriptionClass)">
560
- {{ item.description }}
561
- </span>
562
- <slot name="tags" :option="item" />
563
- </div>
564
- </div>
565
- <div v-if="item.maxInputTokens || item.maxOutputTokens" class="flex gap-3">
566
- <div class="flex flex-col gap-1.5">
567
- <span class="text-tertiary leading-none text-9px uppercase tracking-wider font-semibold">
568
- {{ labelInputMax }}
569
- </span>
570
- <span class="text-tertiary leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-neutral-700">
571
- {{ handleFormatNumber(item.maxInputTokens) }}
572
- </span>
573
- </div>
574
- <div class="flex flex-col gap-1.5">
575
- <span class="text-tertiary leading-none text-9px uppercase tracking-wider font-semibold">
576
- {{ labelOutputMax }}
577
- </span>
578
- <span class="text-tertiary leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-neutral-700">
579
- {{ handleFormatNumber(item.maxOutputTokens) }}
580
- </span>
581
- </div>
582
- </div>
583
- </div>
584
- <div class="flex gap-2 items-center">
585
- <TelaIcon v-if="item.children?.length" name="i-ph-caret-right" size="14px" color="icon-tertiary" />
586
- <ComboboxItemIndicator v-else>
587
- <TelaIcon name="i-ph-check" size="14px" />
588
- </ComboboxItemIndicator>
589
- </div>
590
- </ComboboxItem>
591
- <!-- Nested submenu -->
592
- <Teleport v-if="item.children?.length" to="body">
593
- <div
594
- v-if="expandedItem === item.value"
595
- :style="submenuStyle"
596
- class="min-w-200px bg-white border border-strong rounded-12px shadow-[0_10px_15px_-3px_rgba(0,0,0,0.1),0_4px_6px_-4px_rgba(0,0,0,0.1)] pointer-events-auto"
597
- @mouseenter="expandedItem = item.value"
598
- @pointerdown.stop
599
- >
600
- <div p-4px>
601
- <div
602
- v-for="child in item.children"
603
- :key="child.value"
604
- :class="cn(
605
- 'relative flex cursor-pointer select-none justify-between items-center rounded-lg px-2 py-2 text-sm font-medium text-primary outline-none hover:bg-muted',
606
- !compact && '!px-1.5',
607
- )"
608
- @click.stop="handleChildSelect(child, $event)"
609
- >
610
- <div :class="cn('flex items-center', compact ? 'gap-2' : 'gap-1.5')">
611
- <div v-if="child.icon" class="w-5 h-5 shrink-0 flex items-center justify-center">
612
- <TelaIcon
613
- v-if="typeof child.icon === 'string'" :name="child.icon" :color="colorIcon" :size="iconWithBackground?.size"
614
- :background-class="iconWithBackground?.backgroundClass"
615
- />
616
- <Component :is="child.icon" v-else />
617
- </div>
618
- <div class="flex flex-col">
619
- <span :class="cn('font-medium truncate max-w-140px', compact ? 'text-sm font-580' : 'text-body-14-regular', labelItemClass)">{{ child.label }}</span>
620
- <span v-if="child.description" :class="cn('font-normal text-sm leading-none text-secondary', descriptionClass)">
621
- {{ child.description }}
622
- </span>
623
- </div>
493
+ <div class="flex flex-col">
494
+ <span :class="cn('font-medium truncate max-w-140px', compact ? 'text-sm font-580' : 'text-body-14-regular', labelItemClass)">{{ child.label }}</span>
495
+ <span v-if="child.description" :class="cn('font-normal text-sm leading-none text-secondary', descriptionClass)">
496
+ {{ child.description }}
497
+ </span>
624
498
  </div>
625
- <TelaIcon v-if="child.value === (modelValue || innerValue)" name="i-ph-check" size="14px" />
626
499
  </div>
500
+ <TelaIcon v-if="child.value === (modelValue || innerValue)" name="i-ph-check" size="14px" />
627
501
  </div>
628
502
  </div>
629
- </Teleport>
630
- </template>
631
- </ComboboxGroup>
632
- </template>
633
- </div>
634
- </template>
635
- </ComboboxList>
636
- </ComboboxRoot>
503
+ </div>
504
+ </Teleport>
505
+ </template>
506
+ </TelaComboboxGroup>
507
+ </template>
508
+ </div>
509
+ </TelaComboboxList>
510
+ </TelaComboboxRoot>
637
511
  </template>
638
512
 
639
513
  <style scoped>
640
514
  .nuxt-icon {
641
515
  margin-bottom: 0;
642
516
  }
643
-
644
- .no-scrollbar {
645
- -ms-overflow-style: none;
646
- scrollbar-width: none;
647
-
648
- &::-webkit-scrollbar {
649
- display: none;
650
- }
651
- }
652
- </style>
653
-
654
- <style>
655
- @keyframes combobox-submenuFadeIn {
656
- from {
657
- opacity: 0;
658
- transform: translateX(-4px);
659
- }
660
- to {
661
- opacity: 1;
662
- transform: translateX(0);
663
- }
664
- }
665
517
  </style>
@@ -10,6 +10,7 @@ const props = defineProps<{
10
10
  cancelButtonText?: string
11
11
  loading?: boolean
12
12
  confirmButtonTestId?: string
13
+ overlayClass?: string
13
14
  }>()
14
15
 
15
16
  const emit = defineEmits<{
@@ -29,6 +30,7 @@ const confirmButtonVariant = computed(() => {
29
30
  v-model:model-value="open"
30
31
  :compact="props.compact ?? true"
31
32
  hide-dividers
33
+ :overlay-class="props.overlayClass"
32
34
  :is-close-icon="false"
33
35
  :style="{ paddingBlock: '20px' }"
34
36
  @close="emit('cancel')"
@@ -29,28 +29,28 @@ const emit = defineEmits<{
29
29
  }>()
30
30
 
31
31
  const style = computed(() => {
32
- const base = 'cursor-pointer flex items-center justify-center rounded-lg p-0.5em ease-out duration-40'
32
+ const base = 'cursor-pointer flex items-center justify-center p-0.5em ease-out duration-40'
33
33
  const size = ({
34
- '3xs': 'text-6px p-1',
35
- '2xs': 'text-8px p-4px', // 16px
36
- 'xs': 'text-12px p-6px', // 24px
37
- 'sm': 'text-16px p-0.4em', // 32px
38
- 'md': 'text-20px p-0.4em', // 44px
34
+ '3xs': 'text-6px p-1 rounded-8px',
35
+ '2xs': 'text-8px p-4px rounded-4px', // 16px
36
+ 'xs': 'text-12px p-6px rounded-6px', // 24px
37
+ 'sm': 'text-16px p-0.4em rounded-8px', // 32px
38
+ 'md': 'text-20px p-0.4em rounded-10px', // 44px
39
39
  } as Record<typeof props.size, string>)[props.size]
40
40
 
41
41
  const color = ({
42
- primary: 'bg-neutral-900 !text-white hover:bg-neutral-800',
42
+ primary: 'bg-btn-primary !text-inverse hover:bg-btn-primary-hover active:bg-btn-primary-active',
43
43
  secondary: 'bg-transparent text-icon-tertiary hover:bg-muted hover:text-primary active:text-primary disabled:bg-muted disabled:!text-subtle',
44
44
  } as Record<typeof props.color, string>)[props.color]
45
45
 
46
46
  const disabled = props.disabled && 'cursor-not-allowed !bg-muted !text-subtle !b-0'
47
47
 
48
48
  const outline = props.outline && ({
49
- primary: 'b-0.7px !b-#011D21',
50
- secondary: 'b-0.7px !b-gray-200',
49
+ primary: 'border-0.5px !border-neutral-800',
50
+ secondary: 'border-0.5px !border',
51
51
  } as Record<typeof props.color, string>)[props.color]
52
52
 
53
- const active = props.active && '!bg-gray-100'
53
+ const active = props.active && '!bg-muted'
54
54
 
55
55
  return [
56
56
  base,
@@ -164,32 +164,20 @@ function iconSrc(option: PreviewVariableOption): string {
164
164
  </p>
165
165
  </div>
166
166
  </div>
167
- <div flex-shrink-0>
168
- <TelaIcon
169
- name="i-ph-caret-down-bold"
170
- size="16px"
171
- color="icon-secondary"
172
- :class="cn('ease transition-transform duration-200', isOpen && 'rotate-180')"
173
- />
174
- </div>
175
167
  </TelaComboboxTrigger>
176
168
  </TelaComboboxAnchor>
177
169
  <TelaComboboxList :class="cn('z-999 w-full max-w-[var(--reka-combobox-trigger-width)]', popoverClass)" :disable-portal="true" align="center">
178
- <div class="relative">
179
- <div class="absolute inset-y-0 flex items-center justify-center px-2.5">
180
- <TelaIcon name="i-ph-magnifying-glass-bold" size="13px" color="neutral-300" />
181
- </div>
182
- <TelaComboboxInput
183
- v-model="search"
184
- :class="cn('rounded-b-none pl-30px')"
185
- :placeholder="labels.searchPlaceholder"
186
- />
187
- <div v-if="search.length > 0" class="absolute inset-y-0 end-0 flex items-center justify-center px-3">
170
+ <TelaComboboxInput
171
+ v-model="search"
172
+ class="rounded-b-none"
173
+ :placeholder="labels.searchPlaceholder"
174
+ >
175
+ <template v-if="search.length > 0" #deleteSearchValue>
188
176
  <button class="flex items-center justify-center" @mousedown.prevent @click.stop="handleDeleteSearchValue">
189
177
  <TelaIcon name="i-ph-x-circle" size="16px" class="text-gray-400" />
190
178
  </button>
191
- </div>
192
- </div>
179
+ </template>
180
+ </TelaComboboxInput>
193
181
  <div class="max-h-262px overflow-y-auto no-scrollbar">
194
182
  <TelaComboboxEmpty
195
183
  v-if="groupedOptions.length === 0 || groupedOptions.every(g => g.children.length === 0)"
@@ -248,9 +236,9 @@ function iconSrc(option: PreviewVariableOption): string {
248
236
  </p>
249
237
  </div>
250
238
  <div v-if="props.modelValue === item.value" class="flex gap-2 items-center">
251
- <TelaComboboxItemIndicator>
239
+ <TelaComboboxIndicator>
252
240
  <TelaIcon name="i-ph-check-bold" size="14px" color="icon-secondary" />
253
- </TelaComboboxItemIndicator>
241
+ </TelaComboboxIndicator>
254
242
  </div>
255
243
  </div>
256
244
  </TelaComboboxItem>
@@ -4,18 +4,28 @@ import { reactiveOmit } from '@vueuse/core'
4
4
  import { TabsTrigger, useForwardProps } from 'reka-ui'
5
5
  import type { TabsTriggerProps } from 'reka-ui'
6
6
 
7
- const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes['class'], textClass?: HTMLAttributes['class'] }>()
7
+ const props = withDefaults(defineProps<TabsTriggerProps & {
8
+ class?: HTMLAttributes['class']
9
+ textClass?: HTMLAttributes['class']
10
+ size?: 'sm' | 'md'
11
+ }>(), { size: 'md' })
8
12
 
9
- const delegatedProps = reactiveOmit(props, 'class', 'textClass')
13
+ const delegatedProps = reactiveOmit(props, 'class', 'textClass', 'size')
10
14
 
11
15
  const forwardedProps = useForwardProps(delegatedProps)
16
+
17
+ const sizeStyle = computed(() => (({
18
+ sm: 'px-[6px] py-1.5 body-12-semibold',
19
+ md: 'px-[6px] py-2 body-14-semibold',
20
+ }) as Record<'sm' | 'md', string>)[props.size])
12
21
  </script>
13
22
 
14
23
  <template>
15
24
  <TabsTrigger
16
25
  v-bind="forwardedProps"
17
26
  :class="cn(
18
- 'inline-flex cursor-pointer items-center justify-center whitespace-nowrap px-[6px] py-2 body-14-semibold transition-all focus-visible:outline-none disabled:pointer-events-none text-gray-400 hover:text-black disabled:opacity-50 data-[state=active]:text-black data-[state=active]:shadow-sm',
27
+ 'inline-flex cursor-pointer items-center justify-center whitespace-nowrap transition-all focus-visible:outline-none disabled:pointer-events-none text-tertiary hover:text-primary disabled:opacity-50 data-[state=active]:text-primary',
28
+ sizeStyle,
19
29
  props.class,
20
30
  )"
21
31
  >
@@ -111,6 +111,19 @@ type TabsProps = {
111
111
 
112
112
  - `content` - Tab panel content area
113
113
 
114
+ ## Sizes
115
+
116
+ `TelaTabsTrigger` supports two sizes via the `size` prop:
117
+
118
+ - `sm` — use in small components, e.g. inside a card or a table
119
+ - `md` (default) — use in bigger layouts, e.g. `TelaDetails`
120
+
121
+ ```vue
122
+ <TelaTabsTrigger value="overview" size="sm">
123
+ Overview
124
+ </TelaTabsTrigger>
125
+ ```
126
+
114
127
  ## Components
115
128
 
116
129
  The Tabs system consists of these reka-ui components:
package/docs/tokens.md CHANGED
@@ -52,6 +52,12 @@ Surfaces stack. Use the right background token for each layer — not by feel, b
52
52
  - **`bg-subtle`** — One level up. Secondary panels, sidebars. (`DT.colors.background.subtle`)
53
53
  - **`bg-muted`** — Inputs, cards, contained areas. (`DT.colors.background.muted`)
54
54
  - **`bg-lowered`** — Below the base. Subtle separators, recessed areas. (`DT.colors.background.lowered`)
55
+ - **`bg-btn-primary`** / **`-hover`** / **`-active`** — Primary button background at rest, hover, pressed. (`DT.colors.background['btn-primary*']`)
56
+ - **`bg-btn-secondary`** / **`-hover`** / **`-active`** — Secondary button background states. (`DT.colors.background['btn-secondary*']`)
57
+ - **`bg-btn-ghost`** / **`-hover`** — Ghost button background states. (`DT.colors.background['btn-ghost*']`)
58
+ - **`bg-btn-danger`** / **`-hover`** — Danger button background states. (`DT.colors.background['btn-danger*']`)
59
+ - **`bg-btn-dark`** / **`-hover`** / **`-active`** — Dark (inverse surface) button background states. (`DT.colors.background['btn-dark*']`)
60
+ - **`bg-btn-disabled`**, **`bg-btn-loading`** — Button backgrounds for disabled and loading states. (`DT.colors.background['btn-disabled']`, `['btn-loading']`)
55
61
  - **`bg-success`** — Success state backgrounds in badges and alerts. (`DT.colors.background.success`)
56
62
  - **`bg-error`** — Error state backgrounds in badges and alerts. (`DT.colors.background.error`)
57
63
  - **`bg-warning`** — Warning state backgrounds in badges and alerts. (`DT.colors.background.warning`)
@@ -64,7 +70,8 @@ For typography, default to `text-primary` (content) and `text-secondary` (suppor
64
70
  - **`text-secondary`** — Supporting text, metadata, labels. (`DT.colors.text.secondary`)
65
71
  - **`text-tertiary`** — Placeholders, disabled states — not "quieter body text". (`DT.colors.text.tertiary`)
66
72
  - **`text-subtle`** — Very low emphasis, over muted surfaces only. (`DT.colors.text.subtle`)
67
- - **`text-contrast`** — Reserved; primary is already the default heading color. (`DT.colors.text.contrast`)
73
+ - **`text-contrast`** — Darkest text on light surfaces — e.g. labels on secondary/ghost buttons. Not for copy; primary is already the default heading color. (`DT.colors.text.contrast`)
74
+ - **`text-inverse`** — Text on dark/inverted surfaces — e.g. labels on primary/dark buttons. (`DT.colors.text.inverse`)
68
75
  - **`text-icon`** — Icon color applied via text utility. (`DT.colors.text.icon`)
69
76
  - **`text-success`** — Success messaging and status indicators. (`DT.colors.text.success`)
70
77
  - **`text-error`** — Error messaging, destructive action labels. (`DT.colors.text.error`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.52.1",
3
+ "version": "1.53.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",
package/unocss.config.ts CHANGED
@@ -77,11 +77,11 @@ export default defineConfig({
77
77
 
78
78
  return `${sizeStyle} font-${weight} ${isCaption ? 'uppercase tracking-0.05em!' : ''}`
79
79
  }],
80
- // Catch for `text-(primary|secondary|tertiary|subtle|contrast|success|error|warning|pending)`
81
- [/text-(primary|secondary|tertiary|subtle|contrast|success|error|warning|pending)/, ([_, v]) => `text-text-${v}`],
82
- // Catch for `bg-(subtle|muted|lowered|success|error|warning)`
80
+ // Catch for `text-(primary|secondary|tertiary|subtle|contrast|inverse|success|error|warning|pending)`
81
+ [/text-(primary|secondary|tertiary|subtle|contrast|inverse|success|error|warning|pending)/, ([_, v]) => `text-text-${v}`],
82
+ // Catch for `bg-(subtle|muted|lowered|btn-*|success|error|warning)`
83
83
  // Handle bg, bg-subtle, etc. Use bg-background if just 'bg'
84
- [/^bg(?:-(subtle|muted|lowered|success|error|warning))?$/, ([_, v]) => v ? `bg-background-${v}` : 'bg-background'],
84
+ [/^bg(?:-(subtle|muted|lowered|btn(?:-[a-z]+)*|success|error|warning))?$/, ([_, v]) => v ? `bg-background-${v}` : 'bg-background'],
85
85
  // Catch for `border-(subtle|strong)`
86
86
  [/^border(?:-(subtle|strong))?$/, ([_, v]) => v ? `border-border-${v.toLowerCase()}` : 'border-border'],
87
87
  ],
@@ -290,44 +290,76 @@ const LEGACY_COLORS = {
290
290
  },
291
291
  }
292
292
 
293
+ const BTN_COLORS = {
294
+ 'btn-primary': BASE_COLORS.neutral[900],
295
+ 'btn-primary-hover': BASE_COLORS.neutral[800],
296
+ 'btn-primary-active': BASE_COLORS.neutral[700],
297
+ 'btn-secondary': BASE_COLORS.white.DEFAULT,
298
+ 'btn-secondary-hover': BASE_COLORS.neutral[50],
299
+ 'btn-secondary-active': BASE_COLORS.neutral[100],
300
+ 'btn-ghost': BASE_COLORS.transparent,
301
+ 'btn-ghost-hover': BASE_COLORS.neutral[100],
302
+ 'btn-danger': BASE_COLORS.red[100],
303
+ 'btn-danger-hover': BASE_COLORS.red[50],
304
+ 'btn-disabled': BASE_COLORS.neutral[100],
305
+ 'btn-loading': BASE_COLORS.neutral[200],
306
+ 'btn-dark': 'rgba(255, 255, 255, 0.04)',
307
+ 'btn-dark-hover': 'rgba(255, 255, 255, 0.08)',
308
+ 'btn-dark-active': 'rgba(255, 255, 255, 0.1)',
309
+ } as const
310
+
311
+ const BACKGROUND_COLORS = {
312
+ DEFAULT: BASE_COLORS.white.DEFAULT,
313
+ subtle: BASE_COLORS.neutral[50],
314
+ muted: BASE_COLORS.neutral[100],
315
+ lowered: BASE_COLORS.neutral[200],
316
+ success: BASE_COLORS.green[200],
317
+ error: BASE_COLORS.red[200],
318
+ warning: BASE_COLORS.amber[200],
319
+ ...BTN_COLORS,
320
+ } as const
321
+
322
+ const TEXT_COLORS = {
323
+ primary: BASE_COLORS.neutral[800],
324
+ secondary: BASE_COLORS.neutral[500],
325
+ tertiary: BASE_COLORS.neutral[400],
326
+ subtle: BASE_COLORS.neutral[300],
327
+ contrast: BASE_COLORS.neutral[900],
328
+ inverse: BASE_COLORS.white.DEFAULT,
329
+ success: BASE_COLORS.green[600],
330
+ error: BASE_COLORS.red[600],
331
+ warning: BASE_COLORS.amber[600],
332
+ pending: BASE_COLORS.blue[600],
333
+ } as const
334
+
335
+ const BORDER_COLORS = {
336
+ DEFAULT: BASE_COLORS.neutral[200],
337
+ subtle: BASE_COLORS.neutral[100],
338
+ strong: BASE_COLORS.neutral[300],
339
+ accent: BASE_COLORS.neutral[400],
340
+ inverse: BASE_COLORS.white.DEFAULT,
341
+ } as const
342
+
343
+ const ICON_COLORS = {
344
+ DEFAULT: BASE_COLORS.neutral[700],
345
+ secondary: BASE_COLORS.neutral[500],
346
+ tertiary: BASE_COLORS.neutral[400],
347
+ subtle: BASE_COLORS.neutral[300],
348
+ inverse: BASE_COLORS.white.DEFAULT,
349
+ } as const
350
+
351
+ const COLORS = {
352
+ ...BASE_COLORS,
353
+ ...LEGACY_COLORS,
354
+ background: BACKGROUND_COLORS,
355
+ text: TEXT_COLORS,
356
+ border: BORDER_COLORS,
357
+ icon: ICON_COLORS,
358
+ } as const
359
+
293
360
  export const DT = {
294
361
  colors: {
295
- ...BASE_COLORS,
296
- ...LEGACY_COLORS,
297
- background: {
298
- DEFAULT: BASE_COLORS.white.DEFAULT,
299
- subtle: BASE_COLORS.neutral[50],
300
- muted: BASE_COLORS.neutral[100],
301
- lowered: BASE_COLORS.neutral[200],
302
- success: BASE_COLORS.green[200],
303
- error: BASE_COLORS.red[200],
304
- warning: BASE_COLORS.amber[200],
305
- },
306
- text: {
307
- primary: BASE_COLORS.neutral[800],
308
- secondary: BASE_COLORS.neutral[500],
309
- tertiary: BASE_COLORS.neutral[400],
310
- subtle: BASE_COLORS.neutral[300],
311
- contrast: BASE_COLORS.neutral[900],
312
- success: BASE_COLORS.green[600],
313
- error: BASE_COLORS.red[600],
314
- warning: BASE_COLORS.amber[600],
315
- pending: BASE_COLORS.blue[600],
316
- },
317
- border: {
318
- DEFAULT: BASE_COLORS.neutral[200],
319
- subtle: BASE_COLORS.neutral[100],
320
- strong: BASE_COLORS.neutral[300],
321
- accent: BASE_COLORS.neutral[400],
322
- inverse: BASE_COLORS.white.DEFAULT,
323
- },
324
- icon: {
325
- DEFAULT: BASE_COLORS.neutral[700],
326
- secondary: BASE_COLORS.neutral[500],
327
- tertiary: BASE_COLORS.neutral[400],
328
- subtle: BASE_COLORS.neutral[300],
329
- inverse: BASE_COLORS.white.DEFAULT,
330
- },
362
+ ...COLORS,
331
363
  },
332
364
  fontSize: {
333
365
  // headers (you should use <h.> tags for these sizes)