@meistrari/tela-build 1.29.2 → 1.30.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.
@@ -41,8 +41,8 @@ const variantStyle = computed(() => (({
41
41
  'secondary': fold`
42
42
  bg-white text-gray-900 border border-0.5px
43
43
  [box-shadow:0_1px_6px_0_rgba(103,127,148,0.05)]
44
- hover:bg-gray-50 hover:border-gray-300
45
- active:bg-gray-100 active:border-gray-400/60
44
+ hover:bg-subtle hover:border-gray-300
45
+ active:bg-muted active:border-gray-400/60
46
46
  focus-visible:ring-0.5px focus-visible:ring-cyan-600
47
47
  `,
48
48
  'ghost': fold`
@@ -21,7 +21,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
21
21
  <template>
22
22
  <ComboboxItem
23
23
  v-bind="forwarded"
24
- :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-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
24
+ :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)"
25
25
  >
26
26
  <slot />
27
27
  </ComboboxItem>
@@ -41,7 +41,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
41
41
  <ComboboxPortal v-else>
42
42
  <ComboboxContent
43
43
  v-bind="forwarded"
44
- :class="cn('ComboboxContent z-50 shadow-lg outline-none rounded-xl bg-white-1000 border-[0.5px] border-gray-200 pointer-events-auto', props.class)"
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)"
45
45
  >
46
46
  <ComboboxViewport>
47
47
  <slot />
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed } from 'vue'
3
- import type { Component, Ref } from 'vue'
3
+ import type { Component, ComponentPublicInstance, Ref } from 'vue'
4
4
  import { useMagicKeys } from '@vueuse/core'
5
5
 
6
6
  import ComboboxRoot from './combobox-root.vue'
@@ -29,6 +29,8 @@ interface ComboboxOption {
29
29
  isMultiModal?: boolean
30
30
  features?: Array<{ text: string }>
31
31
  cost?: number
32
+ children?: ComboboxOption[]
33
+ triggerLabel?: string
32
34
  }
33
35
 
34
36
  type ComboboxOptions = ComboboxOption[]
@@ -111,9 +113,21 @@ const currentOption = computed(() => {
111
113
  return { label: props.placeholder, value: '' }
112
114
  }
113
115
 
114
- const found = props.options.find(option => option.value === innerValue.value || option.value === props.modelValue)
116
+ const rawInner = innerValue.value
117
+ const value = (rawInner && typeof rawInner === 'object' ? (rawInner as any).value : rawInner) || props.modelValue
118
+ const found = props.options.find(option => option.value === value)
119
+ if (found)
120
+ return found
121
+
122
+ for (const option of props.options) {
123
+ if (option.children) {
124
+ const child = option.children.find(c => c.value === value)
125
+ if (child)
126
+ return child
127
+ }
128
+ }
115
129
 
116
- return found ?? props.options[0] ?? { label: props.placeholder || props.labelSelect, value: '' }
130
+ return props.options[0] ?? { label: props.placeholder || props.labelSelect, value: '' }
117
131
  })
118
132
 
119
133
  const tabs = computed(() => {
@@ -219,23 +233,89 @@ function renderCostIndicator(cost: number | undefined) {
219
233
  return { blackSymbols, graySymbols }
220
234
  }
221
235
 
236
+ const expandedItem = ref<string | null>(null)
237
+ const submenuStyle = ref<Record<string, string>>({})
238
+ const nestedItemRefs = ref<Record<string, HTMLElement>>({})
239
+
240
+ function setNestedItemRef(value: string, el: Element | ComponentPublicInstance | null) {
241
+ if (!el)
242
+ return
243
+ const htmlEl = '$el' in el ? el.$el as HTMLElement : el as HTMLElement
244
+ nestedItemRefs.value[value] = htmlEl
245
+ }
246
+
247
+ function updateSubmenuPosition(itemValue: string) {
248
+ const el = nestedItemRefs.value[itemValue]
249
+ if (!el)
250
+ return
251
+
252
+ const rect = el.getBoundingClientRect()
253
+ const item = props.options.find(o => o.value === itemValue)
254
+ const childCount = item?.children?.length ?? 0
255
+ const estimatedHeight = childCount * 52 + 8
256
+ const viewportHeight = window.innerHeight
257
+ let top = rect.top
258
+ if (top + estimatedHeight > viewportHeight - 8) {
259
+ top = Math.max(8, viewportHeight - estimatedHeight - 8)
260
+ }
261
+ submenuStyle.value = {
262
+ position: 'fixed',
263
+ left: `${rect.right + 2}px`,
264
+ top: `${top}px`,
265
+ zIndex: '9999',
266
+ }
267
+ }
268
+
222
269
  function onOpenChange(open: boolean) {
223
270
  isOpen.value = open
271
+ if (!open) {
272
+ expandedItem.value = null
273
+ }
274
+ }
275
+
276
+ function handleItemMouseEnter(item: ComboboxOption) {
277
+ if (item.children?.length) {
278
+ expandedItem.value = item.value
279
+ updateSubmenuPosition(item.value)
280
+ }
281
+ else {
282
+ expandedItem.value = null
283
+ }
284
+ }
285
+
286
+ function handleChildSelect(child: ComboboxOption, event: Event) {
287
+ event.preventDefault()
288
+ event.stopPropagation()
289
+ expandedItem.value = null
290
+ isOpen.value = false
291
+ emit('select', child.value)
292
+ emit('update:modelValue', child.value)
224
293
  }
225
294
 
226
295
  function handleSelect(option: any) {
227
- emit('select', option.value)
228
- emit('update:modelValue', option.value)
296
+ const value = typeof option === 'string' ? option : option?.value
297
+ const found = props.options.find(o => o.value === value)
298
+ if (found?.children?.length)
299
+ return
300
+ emit('select', value)
301
+ emit('update:modelValue', value)
229
302
  }
230
303
 
231
- watch(innerValue, (value) => {
304
+ watch(innerValue, (raw) => {
305
+ if (!raw)
306
+ return
307
+
308
+ const value = typeof raw === 'string' ? raw : (raw as any)?.value
232
309
  if (!value)
233
310
  return
234
311
 
235
- const option = props.options.find(option => option.value === value)
312
+ const option = props.options.find(o => o.value === value)
236
313
  if (!option)
237
314
  return
238
315
 
316
+ if (option.children?.length)
317
+ return
318
+
239
319
  innerValue.value = ''
240
320
 
241
321
  emit('select', value)
@@ -253,7 +333,8 @@ watch(innerValue, (value) => {
253
333
  as="button"
254
334
  tabindex="0"
255
335
  :class="cn(
256
- 'group select-none flex items-center justify-between gap-3 rounded-10px px-8px py-7px bg-white border-[0.5px] border-gray-300 transition hover:bg-gray-100 data-[state=open]:bg-gray-100',
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)]',
257
338
  !compact && 'w-full !py-2.5 pl-3 pr-3',
258
339
  props.triggerClass,
259
340
  )"
@@ -273,16 +354,16 @@ watch(innerValue, (value) => {
273
354
  </div>
274
355
  </div>
275
356
  <span
276
- :class="cn('truncate max-w-120px text-gray-900 group-data-[state=open]:text-gray-500',
357
+ :class="cn('truncate text-primary group-data-[state=open]:text-secondary',
277
358
  compact ? 'text-sm font-580' : 'text-body-14-regular', props.labelClass)"
278
359
  >
279
- {{ currentOption?.label }}
360
+ {{ currentOption?.triggerLabel || currentOption?.label }}
280
361
  </span>
281
362
  </div>
282
- <TelaIcon name="i-ph-caret-down" :class="cn('op-60 transition-all ease-out duration-150', isOpen && 'rotate-180')" size="12px" />
363
+ <TelaIcon name="i-ph-caret-down-bold" color="icon-secondary" :class="cn('transition-all ease-out duration-150', isOpen && 'rotate-180')" size="15px" />
283
364
  </ComboboxTrigger>
284
365
  </ComboboxAnchor>
285
- <ComboboxList :class="cn('z-999', compact ? 'w-440px' : 'w-327px!', props.contentClass)" :disable-portal="props.disablePortal" align="start">
366
+ <ComboboxList :class="cn('z-999', compact ? 'w-440px' : 'w-327px', props.contentClass)" :disable-portal="props.disablePortal" align="start">
286
367
  <div v-if="hasSearchbar" class="relative">
287
368
  <span class="absolute inset-y-0 start-0 flex items-center justify-center px-2.5">
288
369
  <TelaIcon name="i-ph-magnifying-glass" :size="compact ? '12px' : '14px'" class="text-gray-400" />
@@ -440,65 +521,113 @@ watch(innerValue, (value) => {
440
521
  <ComboboxLabel v-if="!search.trim() && hasGroupingLabels">
441
522
  {{ group.heading }}
442
523
  </ComboboxLabel>
443
- <ComboboxItem v-for="item in group.children" :key="item.value" :value="item" :class="cn('group py-2', !compact && '!px-1.5')">
444
- <div class="flex items-center">
445
- <div :class="cn('flex items-center w-250px', compact ? 'gap-2' : 'gap-1.5')">
446
- <div v-if="item.icon" class="w-5 h-5 shrink-0 flex items-center justify-center">
447
- <TelaIcon
448
- v-if="typeof item.icon === 'string'" :name="item.icon" :color="colorIcon" :size="iconWithBackground?.size"
449
- :background-class="iconWithBackground?.backgroundClass"
450
- />
451
- <Component
452
- :is="item.icon"
453
- v-else
454
- :class="cn((item?.label.startsWith('DeepSeek') || item?.label.startsWith('Gemini')) && 'scale-75')"
455
- />
456
- </div>
457
- <div v-if="!item.icon && item?.externalIconSrc">
458
- <img :src="item.externalIconSrc" :alt="item.label" class="w-5 h-5">
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()"
531
+ >
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-gray-100" 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-black-900">{{ renderCostIndicator(item.cost)?.blackSymbols }}</span>
556
+ <span class="text-gray-300">{{ renderCostIndicator(item.cost)?.graySymbols }}</span>
557
+ </span>
558
+ </div>
559
+ <span v-if="item.description" :class="cn('font-normal text-sm leading-none text-gray-500', descriptionClass)">
560
+ {{ item.description }}
561
+ </span>
562
+ <slot name="tags" :option="item" />
563
+ </div>
459
564
  </div>
460
- <div class="flex flex-col">
461
- <div class="flex items-center gap-1">
462
- <span :class="cn('font-medium truncate max-w-140px', compact ? 'text-sm font-580' : 'text-body-14-regular', labelItemClass)">{{ item.label }}</span>
463
- <TelaBadge v-if="item.isMultiModal" variant="filled" class="py-[2px] bg-gray-100" text-class="leading-none">
464
- {{ labelMultimodal }}
465
- </TelaBadge>
466
- <span v-if="item.cost !== undefined" class="text-10px font-580 ml-1px">
467
- <span class="text-black-900">{{ renderCostIndicator(item.cost)?.blackSymbols }}</span>
468
- <span class="text-gray-300">{{ renderCostIndicator(item.cost)?.graySymbols }}</span>
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-gray-400 leading-none text-9px uppercase tracking-wider font-semibold">
568
+ {{ labelInputMax }}
569
+ </span>
570
+ <span class="text-gray-400 leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-gray-700">
571
+ {{ handleFormatNumber(item.maxInputTokens) }}
572
+ </span>
573
+ </div>
574
+ <div class="flex flex-col gap-1.5">
575
+ <span class="text-gray-400 leading-none text-9px uppercase tracking-wider font-semibold">
576
+ {{ labelOutputMax }}
577
+ </span>
578
+ <span class="text-gray-400 leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-gray-700">
579
+ {{ handleFormatNumber(item.maxOutputTokens) }}
469
580
  </span>
470
581
  </div>
471
- <span v-if="item.description" :class="cn('font-normal text-sm leading-none text-gray-500', descriptionClass)">
472
- {{ item.description }}
473
- </span>
474
- <slot name="tags" :option="item" />
475
582
  </div>
476
583
  </div>
477
- <div v-if="item.maxInputTokens || item.maxOutputTokens" class="flex gap-3">
478
- <div class="flex flex-col gap-1.5">
479
- <span class="text-gray-400 leading-none text-9px uppercase tracking-wider font-semibold">
480
- {{ labelInputMax }}
481
- </span>
482
- <span class="text-gray-400 leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-gray-700">
483
- {{ handleFormatNumber(item.maxInputTokens) }}
484
- </span>
485
- </div>
486
- <div class="flex flex-col gap-1.5">
487
- <span class="text-gray-400 leading-none text-9px uppercase tracking-wider font-semibold">
488
- {{ labelOutputMax }}
489
- </span>
490
- <span class="text-gray-400 leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-gray-700">
491
- {{ handleFormatNumber(item.maxOutputTokens) }}
492
- </span>
584
+ <div class="flex gap-2 items-center">
585
+ <TelaIcon v-if="item.children?.length" name="i-ph-caret-right" size="14px" class="text-gray-400" />
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-gray-900 outline-none hover:bg-gray-100',
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-gray-500', descriptionClass)">
621
+ {{ child.description }}
622
+ </span>
623
+ </div>
624
+ </div>
625
+ <TelaIcon v-if="child.value === (modelValue || innerValue)" name="i-ph-check" size="14px" />
626
+ </div>
493
627
  </div>
494
628
  </div>
495
- </div>
496
- <div class="flex gap-2 items-center">
497
- <ComboboxItemIndicator>
498
- <TelaIcon name="i-ph-check" size="14px" />
499
- </ComboboxItemIndicator>
500
- </div>
501
- </ComboboxItem>
629
+ </Teleport>
630
+ </template>
502
631
  </ComboboxGroup>
503
632
  </template>
504
633
  </div>
@@ -521,3 +650,16 @@ watch(innerValue, (value) => {
521
650
  }
522
651
  }
523
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
+ </style>
@@ -786,11 +786,55 @@ function scrollToPage(page: number) {
786
786
  }
787
787
  }
788
788
 
789
- function download(url: string) {
789
+ async function download(url: string) {
790
790
  if (!url)
791
791
  return
792
792
 
793
- window.open(url, '_blank')
793
+ const filename = props.file.fileName || 'download'
794
+
795
+ function triggerAnchor(href: string, revokeAfter?: string) {
796
+ const a = document.createElement('a')
797
+ a.href = href
798
+ a.download = filename
799
+ document.body.appendChild(a)
800
+ a.click()
801
+ document.body.removeChild(a)
802
+ if (revokeAfter) {
803
+ setTimeout(() => URL.revokeObjectURL(revokeAfter), 10_000)
804
+ }
805
+ }
806
+
807
+ if (url.startsWith('data:')) {
808
+ try {
809
+ const response = await fetch(url)
810
+ const blob = await response.blob()
811
+ const blobUrl = URL.createObjectURL(blob)
812
+ triggerAnchor(blobUrl, blobUrl)
813
+ }
814
+ catch {
815
+ triggerAnchor(url)
816
+ }
817
+ return
818
+ }
819
+
820
+ try {
821
+ const response = await fetch(url)
822
+ if (!response.ok)
823
+ throw new Error(`HTTP ${response.status}`)
824
+ const blob = await response.blob()
825
+ const blobUrl = URL.createObjectURL(blob)
826
+ triggerAnchor(blobUrl, blobUrl)
827
+ }
828
+ catch {
829
+ const a = document.createElement('a')
830
+ a.href = url
831
+ a.download = filename
832
+ a.target = '_blank'
833
+ a.rel = 'noopener noreferrer'
834
+ document.body.appendChild(a)
835
+ a.click()
836
+ document.body.removeChild(a)
837
+ }
794
838
  }
795
839
 
796
840
  const isContainerHovered = ref(false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.29.2",
3
+ "version": "1.30.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",