@meistrari/tela-build 1.74.1 → 1.74.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,12 @@ Latest updates and announcements.
12
12
 
13
13
  ## September 17, 2026
14
14
 
15
+ ### Tags
16
+
17
+ - **Delete is an icon button.** In `TelaTagsSelect`'s edit view, delete is now a trash icon button in the header instead of a labeled button, so the `deleteTagLabel` prop was removed. See [Tags Select](/components/tags-select).
18
+ - **Motion and polish.** The trigger dots of `TelaTagsSelect` animate in and out as tags change, and `TelaTags` stacks its dots with a mask cutout and gives the dismiss button a larger hit area. See [Tags](/components/tags).
19
+ - `TelaSelectMenuContent` uses the `bg` and `border` theme tokens instead of fixed white and gray. See [Select Menu](/components/select-menu).
20
+
15
21
  - [Chat Answer](/templates/chat#answer) option descriptions now use the single-line tooltip. It wraps the option label and opens to the label's right on hover (0.5s) or keyboard focus, instead of above it.
16
22
 
17
23
  - [Tooltip](/components/tooltip): new `asChild` prop merges the trigger into the slotted element instead of wrapping it in a button, and `reference` anchors the tooltip to another element. An `open` value set after mount now takes effect; leaving it `undefined` keeps hover and focus in charge.
@@ -41,7 +41,7 @@ const props = withDefaults(defineProps<{
41
41
  iconSide: 'left',
42
42
  })
43
43
 
44
- defineEmits(['click'])
44
+ const emit = defineEmits(['click', 'update:open'])
45
45
 
46
46
  const search = ref('')
47
47
  const searchInputEl = ref<HTMLInputElement>()
@@ -58,6 +58,7 @@ const groups = computed(() => filteredItems.value.reduce((acc, item) => {
58
58
  }, {} as Record<string, typeof props.items[number][]>))
59
59
 
60
60
  function onToggle(open: boolean) {
61
+ emit('update:open', open)
61
62
  if (!open)
62
63
  return
63
64
 
@@ -24,6 +24,16 @@ Use `TelaDropdownMenu` when clicking triggers a side effect. Use `TelaSelectMenu
24
24
 
25
25
  **The test:** If clicking triggers a side effect → `TelaDropdownMenu`. If it updates a bound value → `TelaSelectMenu`.
26
26
 
27
+ ## Loading menu data on open
28
+
29
+ `update:open` emits a boolean whenever the menu opens or closes, including keyboard activation. Use it to fetch menu data only when needed:
30
+
31
+ ```vue
32
+ <TelaDropdownMenu :items="items" @update:open="open => open && loadItems()">
33
+ <TelaButton>Open menu</TelaButton>
34
+ </TelaDropdownMenu>
35
+ ```
36
+
27
37
  ## Examples
28
38
 
29
39
  ### Basic Usage
@@ -10,9 +10,6 @@ import type { SelectContentEmits, SelectContentProps } from 'reka-ui'
10
10
  import type { HTMLAttributes } from 'vue'
11
11
  import { reactiveOmit } from '@vueuse/core'
12
12
 
13
- import SelectMenuDownButton from './select-menu-down-button.vue'
14
- import SelectMenuUpButton from './select-menu-up-button.vue'
15
-
16
13
  defineOptions({
17
14
  inheritAttrs: false,
18
15
  })
@@ -36,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
36
33
  <SelectContent
37
34
  v-bind="{ ...forwarded, ...$attrs }"
38
35
  :class="cn(
39
- 'SelectContent popover-enter-exit relative z-999 max-h-80 overflow-hidden rounded-[10px] border-[0.5px] border-gray-300 bg-white-1000',
36
+ 'SelectContent popover-enter-exit relative z-999 max-h-80 overflow-hidden rounded-[10px] border-[0.5px] border bg',
40
37
  props.class,
41
38
  )"
42
39
  :side-offset="4"
@@ -52,7 +49,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
52
49
  v-else
53
50
  v-bind="{ ...forwarded, ...$attrs }"
54
51
  :class="cn(
55
- 'SelectContent popover-enter-exit relative z-999 max-h-80 overflow-hidden rounded-[16px] border-[0.5px] border-gray-200 bg-white-1000',
52
+ 'SelectContent popover-enter-exit relative z-999 max-h-80 overflow-hidden rounded-[16px] border-[0.5px] border bg',
56
53
  props.class,
57
54
  )"
58
55
  :side-offset="4"
@@ -260,8 +260,8 @@ The component intelligently merges `modelValue` and `options` to create a comple
260
260
  ### Dot Mask Effect
261
261
  The overlapping dot effect is achieved using CSS mask:
262
262
  ```css
263
- .dot-mask:not(:last-child) {
264
- mask-image: radial-gradient(circle 6px at right center, transparent 6px, #fff 6px);
263
+ .tags-select-dot-mask:not(:last-child) {
264
+ mask-image: radial-gradient(circle 8px at calc(100% + 2px) center, transparent 8px, #fff 8px);
265
265
  }
266
266
  ```
267
267
 
@@ -18,7 +18,6 @@ const props = withDefaults(defineProps<{
18
18
  addNewNamePlaceholder?: string
19
19
  createButtonLabel?: string
20
20
  saveChangesButtonLabel?: string
21
- deleteTagLabel?: string
22
21
  tagSingular?: string
23
22
  tagPlural?: string
24
23
  assignLabel?: string
@@ -37,7 +36,6 @@ const props = withDefaults(defineProps<{
37
36
  addNewNamePlaceholder: 'Add new name',
38
37
  createButtonLabel: 'Create',
39
38
  saveChangesButtonLabel: 'Save changes',
40
- deleteTagLabel: 'Delete',
41
39
  tagSingular: 'tag',
42
40
  tagPlural: 'tags',
43
41
  showLabel: true,
@@ -65,6 +63,7 @@ export interface Tag {
65
63
  }
66
64
 
67
65
  interface Dot {
66
+ key: string
68
67
  color: string
69
68
  lightColor?: boolean
70
69
  class?: HTMLAttributes['class']
@@ -73,9 +72,9 @@ interface Dot {
73
72
  }
74
73
 
75
74
  const EMPTY_DOTS: Dot[] = [
76
- { color: 'neutral', lightColor: true },
77
- { color: 'neutral', lightColor: true },
78
- { color: 'neutral', icon: 'i-ph-plus-bold' },
75
+ { key: 'empty-0', color: 'neutral', lightColor: true },
76
+ { key: 'empty-1', color: 'neutral', lightColor: true },
77
+ { key: 'empty-2', color: 'neutral', icon: 'i-ph-plus-bold' },
79
78
  ]
80
79
 
81
80
  function getTagKey(tag: Tag): string {
@@ -186,15 +185,17 @@ const tags = computed({
186
185
 
187
186
  const hasTags = computed(() => allCreatedTags.value.length > 0)
188
187
 
188
+ const hasSelectedDots = computed(() => tags.value.length > 0 && !props.alwaysShowEmpty)
189
+
189
190
  const triggerDots = computed<Dot[]>(() => {
190
191
  if (props.alwaysShowEmpty) {
191
192
  return EMPTY_DOTS
192
193
  }
193
194
  if (tags.value.length > 0) {
194
- return tags.value.slice(0, 3).map(tag => ({ color: tag.color }))
195
+ return tags.value.slice(0, 3).map(tag => ({ key: `selected-${tag.id}`, color: tag.color }))
195
196
  }
196
197
  if (hasTags.value) {
197
- return allCreatedTags.value.slice(0, 3).map(tag => ({ color: tag.color }))
198
+ return allCreatedTags.value.slice(0, 3).map(tag => ({ key: `all-${tag.id}`, color: tag.color }))
198
199
  }
199
200
  return EMPTY_DOTS
200
201
  })
@@ -489,31 +490,39 @@ updateAllCreatedTags()
489
490
  as="button"
490
491
  :disabled="props.disabled"
491
492
  :class="cn(
492
- 'group w-fit select-none flex items-center gap-[6px] px-[12px] py-[7px] rounded-[10px] bg-background border-0.5px border',
493
- 'transition ease-in-out duration-80 hover:border-strong hover:bg-subtle data-[state=open]:border-strong data-[state=open]:bg-subtle',
493
+ 'group w-fit select-none flex items-center gap-[6px] h-[32px] px-[12px] py-[7px] rounded-[10px] bg-background border-0.5px border',
494
+ 'transition ease-out duration-40 hover:border-strong hover:bg-subtle data-[state=open]:border-strong data-[state=open]:bg-subtle',
494
495
  'active:bg-muted active:border-gray-400/60 [box-shadow:0_1px_6px_0_rgba(103,127,148,0.05)]',
495
496
  props.disabled && 'opacity-50 cursor-not-allowed hover:bg-background',
496
497
  props.class,
497
498
  )"
498
499
  >
499
- <div class="flex items-center">
500
- <div
501
- v-for="(dot, index) in triggerDots"
502
- :key="index"
503
- :class="cn(
504
- 'flex items-center justify-center w-[12px] h-[12px] rounded-full dot-mask [&:not(:first-child)]:-ml-[4px]',
505
- resolveColor(dot.color, dot.lightColor),
506
- )"
507
- >
508
- <TelaIcon v-if="dot.icon" :name="dot.icon" size="7px" :color="dot.iconColor || 'white-1000'" />
509
- </div>
500
+ <div class="relative flex items-center">
501
+ <AnimatePresence :initial="false" mode="popLayout">
502
+ <Motion
503
+ v-for="(dot, index) in triggerDots"
504
+ :key="index"
505
+ :initial="hasSelectedDots ? { x: -8, scale: 0, opacity: 0 } : { opacity: 0 }"
506
+ :animate="{ x: 0, scale: 1, opacity: 1 }"
507
+ :exit="{ x: -8, opacity: 0 }"
508
+ :transition="{ duration: 0.16, type: 'spring', bounce: 0.1 }"
509
+ :style="{ zIndex: 30 - index }"
510
+ :class="cn(
511
+ 'relative flex items-center justify-center w-[12px] h-[12px] rounded-full [&:not(:first-child)]:-ml-[4px] transition-colors duration-80 ease-out',
512
+ index < triggerDots.length - 1 && 'tags-select-dot-mask',
513
+ resolveColor(dot.color, dot.lightColor),
514
+ )"
515
+ >
516
+ <TelaIcon v-if="dot.icon" :name="dot.icon" size="7px" :color="dot.iconColor || 'white'" />
517
+ </Motion>
518
+ </AnimatePresence>
510
519
  </div>
511
520
  <span v-if="showLabel" :class="cn('body-14-semibold text-text-primary', props.labelClass)">
512
521
  {{ labelText }}
513
522
  </span>
514
523
  </PopoverTrigger>
515
524
  <PopoverPortal>
516
- <PopoverContent class="TagsSelectContent popover-enter-exit z-[99999]" :side-offset="8" w-220px pt-12px px-16px bg-background border-0.5px border-border shadow-lg rounded-12px overflow-hidden>
525
+ <PopoverContent class="TagsSelectContent popover-enter-exit z-[99999] relative [box-shadow:0_4px_16px_0_rgba(85,91,98,0.14)]" :side-offset="4" w-220px pt-12px px-16px bg border-0.5px border rounded-12px overflow-hidden>
517
526
  <div
518
527
  class="[transition:height_0.25s_ease-out]"
519
528
  :style="computedHeightContent ? { height: computedHeightContent } : {}"
@@ -531,7 +540,7 @@ updateAllCreatedTags()
531
540
  <div flex="~ col" gap-4px>
532
541
  <button
533
542
  v-if="allCreatedTags.length > 0"
534
- flex items-center justify-center w-24px h-24px rounded-6px hover:bg-background-muted ml--6px mt--4px
543
+ flex items-center justify-center w-24px h-24px rounded-6px hover:bg-muted ml--6px mt--4px duration-40 ease-out
535
544
  @click="handleBackToTags"
536
545
  >
537
546
  <TelaIcon name="i-ph-arrow-left" size="16px" color="icon" />
@@ -543,7 +552,7 @@ updateAllCreatedTags()
543
552
  <TelaInput
544
553
  v-model="newTagName"
545
554
  :placeholder="props.defineNamePlaceholder"
546
- input-class="px-[6px]! pt-[2px]! pb-[3px]! rounded-[6px]!"
555
+ input-class="px-[8px]! pt-[2px]! pb-[3px]! rounded-[8px]!"
547
556
  input-font-class="body-12-regular!"
548
557
  autofocus
549
558
  @keydown.enter="createNewTag"
@@ -552,17 +561,15 @@ updateAllCreatedTags()
552
561
  <button
553
562
  v-for="color in ALL_COLORS"
554
563
  :key="color"
555
- w-20px h-20px rounded-4px flex items-center justify-center hover:bg-background-lowered
564
+ w-20px h-20px rounded-6px flex items-center justify-center hover:bg-lowered
556
565
  @click="handleSelectColor(color)"
557
566
  >
558
- <div
559
- :class="cn('flex items-center justify-center w-12px h-12px rounded-full', resolveColor(color))"
560
- >
567
+ <div :class="cn('flex items-center justify-center w-12px h-12px rounded-full', resolveColor(color))">
561
568
  <TelaIcon
562
569
  v-if="selectedColorsForNewTag === color"
563
570
  name="i-ph-check-bold"
564
571
  size="8px"
565
- color="white-1000"
572
+ color="white"
566
573
  />
567
574
  </div>
568
575
  </button>
@@ -571,17 +578,21 @@ updateAllCreatedTags()
571
578
  {{ props.createButtonLabel }}
572
579
  </TelaButton>
573
580
  </div>
574
- <div v-if="config.type === 'existing-tag'" flex="~ col" pb-10px>
575
- <div flex items-center justify-between>
581
+ <div v-if="config.type === 'existing-tag'" flex="~ col" pb-8px mt--4px>
582
+ <div flex items-center justify-between h-24px>
576
583
  <h5 heading-h5-semibold>
577
584
  {{ allCreatedTags.length }} {{ allCreatedTags.length > 1 ? props.tagPlural : props.tagSingular }}
578
585
  </h5>
579
- <button v-if="props.isEditable" flex items-center justify-center w-24px h-24px rounded-6px hover:bg-background-muted mr--4px @click="handleCreateMore">
586
+ <button
587
+ v-if="props.isEditable"
588
+ flex items-center justify-center w-24px h-24px rounded-6px hover:bg-muted mr--4px duration-40 ease-out
589
+ @click="handleCreateMore"
590
+ >
580
591
  <TelaIcon name="i-ph-plus" size="16px" color="icon" />
581
592
  </button>
582
593
  </div>
583
- <div flex="~ col" mt-4px>
584
- <button v-for="tag in allCreatedTags" :key="`${tag.color}-${tag.name}`" class="group/tag" flex items-center justify-between hover:bg-background-muted px-8px mx--8px py-4px rounded-8px @click="toggleTagSelection(tag, !isTagSelected(tag))">
594
+ <div flex="~ col">
595
+ <button v-for="tag in allCreatedTags" :key="`${tag.color}-${tag.name}`" class="group/tag" flex items-center justify-between hover:bg-muted px-8px mx--8px py-4px rounded-8px @click="toggleTagSelection(tag, !isTagSelected(tag))">
585
596
  <div flex items-center gap-6px min-w-0>
586
597
  <div :class="cn('w-[8px] h-[8px] rounded-full flex-shrink-0', resolveColor(tag.color))" />
587
598
  <span body-14-medium text-text-primary capitalize truncate text-start>
@@ -591,7 +602,7 @@ updateAllCreatedTags()
591
602
  <div flex items-center gap-4px>
592
603
  <button
593
604
  v-if="props.isEditable"
594
- class="group opacity-0 group-hover/tag:opacity-100" flex items-center justify-center w-17px h-17px rounded-5px hover:bg-background-lowered
605
+ class="group opacity-0 group-hover/tag:opacity-100" flex items-center justify-center w-17px h-17px rounded-5px hover:bg-lowered
595
606
  @click.stop="handleEditTag(tag)"
596
607
  >
597
608
  <TelaIcon name="i-ph-pencil-simple" size="12px" color="icon-subtle group-hover:icon" />
@@ -601,14 +612,24 @@ updateAllCreatedTags()
601
612
  </button>
602
613
  </div>
603
614
  </div>
604
- <div v-if="config.type === 'edit-tag'" flex="~ col" gap-12px pb-16px>
615
+ <div v-if="config.type === 'edit-tag'" flex="~ col" gap-12px pb-16px mt--4px>
605
616
  <div flex="~ col" gap-4px>
606
- <button
607
- flex items-center justify-center w-24px h-24px rounded-6px hover:bg-background-muted ml--6px
608
- @click="cancelEditTag"
609
- >
610
- <TelaIcon name="i-ph-arrow-left" size="16px" color="icon" />
611
- </button>
617
+ <div flex items-center justify-between ml--6px>
618
+ <button
619
+ flex items-center justify-center w-24px h-24px rounded-6px hover:bg-muted duration-40 ease-out
620
+ @click="cancelEditTag"
621
+ >
622
+ <TelaIcon name="i-ph-arrow-left" size="16px" color="icon" />
623
+ </button>
624
+
625
+ <button
626
+ class="group"
627
+ flex items-center justify-center w-24px h-24px rounded-6px hover:bg-btn-danger duration-40 ease-out mb--1px mr--5px
628
+ @click="deleteTag"
629
+ >
630
+ <TelaIcon name="i-ph-trash" size="16px" color="icon-subtle group-hover:red-700" />
631
+ </button>
632
+ </div>
612
633
  <h5 heading-h5-semibold>
613
634
  {{ props.editTagLabel }}
614
635
  </h5>
@@ -616,7 +637,7 @@ updateAllCreatedTags()
616
637
  <TelaInput
617
638
  v-model="editingTagName"
618
639
  :placeholder="props.addNewNamePlaceholder"
619
- input-class="px-[6px]! pt-[2px]! pb-[3px]! rounded-[6px]!"
640
+ input-class="px-[8px]! pt-[2px]! pb-[3px]! rounded-[8px]!"
620
641
  input-font-class="body-12-regular!"
621
642
  @keydown.enter="saveEditedTag"
622
643
  />
@@ -624,7 +645,7 @@ updateAllCreatedTags()
624
645
  <button
625
646
  v-for="color in ALL_COLORS"
626
647
  :key="color"
627
- w-20px h-20px rounded-4px flex items-center justify-center hover:bg-background-lowered
648
+ w-20px h-20px rounded-6px flex items-center justify-center hover:bg-lowered
628
649
  @click="handleSelectColorForEdit(color)"
629
650
  >
630
651
  <div
@@ -634,19 +655,14 @@ updateAllCreatedTags()
634
655
  v-if="editingTagColor === color"
635
656
  name="i-ph-check-bold"
636
657
  size="8px"
637
- color="white-1000"
658
+ color="white"
638
659
  />
639
660
  </div>
640
661
  </button>
641
662
  </div>
642
- <div flex="~ col" gap-4px w-full>
643
- <TelaButton size="sm" :disabled="!editingTagName || editingTagName.trim() === '' || !editingTagColor" @click="saveEditedTag">
644
- {{ props.saveChangesButtonLabel }}
645
- </TelaButton>
646
- <TelaButton size="sm" variant="danger" @click="deleteTag">
647
- {{ props.deleteTagLabel }}
648
- </TelaButton>
649
- </div>
663
+ <TelaButton size="sm" :disabled="!editingTagName || editingTagName.trim() === '' || !editingTagColor" @click="saveEditedTag">
664
+ {{ props.saveChangesButtonLabel }}
665
+ </TelaButton>
650
666
  </div>
651
667
  </Motion>
652
668
  </AnimatePresence>
@@ -659,8 +675,9 @@ updateAllCreatedTags()
659
675
  </template>
660
676
 
661
677
  <style>
662
- .dot-mask:not(:last-child) {
663
- mask-image: radial-gradient(circle 6px at right center, transparent 6px, #fff 6px);
678
+ /* 12px dots overlapping by 4px: the next dot's center sits 2px past this dot's right edge */
679
+ .tags-select-dot-mask:not(:last-child) {
680
+ mask-image: radial-gradient(circle 8px at calc(100% + 2px) center, transparent 8px, #fff 8px);
664
681
  }
665
682
 
666
683
  .TagsSelectContent {
@@ -98,9 +98,10 @@ function handleClose() {
98
98
  >
99
99
  <div
100
100
  :class="cn(
101
- 'inline-flex w-fit items-center pl-[8px] pr-[10px] py-[3px] bg-background border-0.5px border rounded-[8px] select-none transition-all duration-150',
101
+ 'inline-flex w-fit items-center pl-[8px] pr-[10px] py-[3px] bg border-0.5px border rounded-[8px] select-none transition-all duration-150',
102
102
  '[box-shadow:0_1px_6px_0_rgba(103,127,148,0.05)]',
103
- ((isHovered && hasModelValue) || count) && 'pr-[6px] hover:border-strong hover:bg-subtle active:bg-gray-100 active:border-gray-400/60',
103
+ hasMultipleTags ? 'cursor-pointer' : 'cursor-default',
104
+ ((isHovered && hasModelValue) || count) && 'pr-[6px]',
104
105
  props.class,
105
106
  )"
106
107
  @mouseenter="isHovered = true"
@@ -108,20 +109,19 @@ function handleClose() {
108
109
  >
109
110
  <div class="relative z-[1] flex items-center gap-[6px] bg-[inherit]">
110
111
  <!-- Multiple tags mode: stacked dots -->
111
- <div v-if="hasMultipleTags" class="relative flex items-center shrink-0" :style="{ width: `${8 + (visibleTags.length - 1) * 4}px`, height: '8px' }">
112
+ <div v-if="hasMultipleTags" class="relative flex items-center shrink-0">
112
113
  <div
113
114
  v-for="(tag, index) in visibleTags"
114
115
  :key="index"
115
- :class="cn('absolute w-[8px] h-[8px] rounded-full border-[0.5px] border-background', resolveColor(tag.color, dotLightColor))"
116
- :style="{ left: `${index * 4}px`, zIndex: visibleTags.length + index }"
116
+ :class="cn('w-[8px] h-[8px] [&:not(:first-child)]:-ml-[2px] rounded-full dot-mask', resolveColor(tag.color, dotLightColor))"
117
117
  />
118
118
  </div>
119
119
  <!-- Single tag mode: single dot -->
120
120
  <div v-else :class="cn('w-[8px] h-[8px] rounded-full shrink-0', resolveColor(dotColor, dotLightColor))" />
121
121
 
122
- <span :class="cn('body-14-medium text-text-primary line-clamp-1 text-left', textClass)">
122
+ <span :class="cn('body-14-medium text-primary line-clamp-1 text-left', textClass)">
123
123
  <slot />
124
- <span v-if="hasMultipleTags && remainingTagsCount > 0" class="text-text-secondary ml-[2px]">
124
+ <span v-if="hasMultipleTags && remainingTagsCount > 0">
125
125
  +{{ remainingTagsCount }}
126
126
  </span>
127
127
  </span>
@@ -135,14 +135,14 @@ function handleClose() {
135
135
  v-if="isHovered"
136
136
  as="button"
137
137
  class="ml-[4px]"
138
- :initial="{ width: 0, x: -20, scale: 0, opacity: 0 }"
138
+ :initial="{ width: 0, x: -20, scale: 0.4, opacity: 0 }"
139
139
  :animate="{ width: 'auto', x: 0, scale: 1, opacity: 1 }"
140
- :exit="{ width: 0, x: -20, scale: 0, opacity: 0 }"
140
+ :exit="{ width: 0, x: -20, scale: 0.4, opacity: 0 }"
141
141
  :transition="{ duration: 0.15, ease: 'easeOut' }"
142
142
  @click="handleClose"
143
143
  >
144
- <div class="p-[2px] hover:bg-background-lowered rounded-[5px]">
145
- <TelaIcon name="i-ph-x-bold" size="12px" color="text-secondary" />
144
+ <div p-2px hover:bg-lowered rounded-5px touch-hitbox duration-40 will-change-transform active:scale-93>
145
+ <TelaIcon name="i-ph-x-bold" size="12px" color="icon-secondary" />
146
146
  </div>
147
147
  </Motion>
148
148
  </AnimatePresence>
@@ -166,3 +166,9 @@ function handleClose() {
166
166
  </template>
167
167
  </TelaTooltip>
168
168
  </template>
169
+
170
+ <style>
171
+ .dot-mask:not(:last-child) {
172
+ mask-image: radial-gradient(circle 4px at right center, transparent 4px, #fff 4px);
173
+ }
174
+ </style>
@@ -1,10 +1,12 @@
1
- import * as pdfjsLib from 'pdfjs-dist'
2
1
  import type { PDFDocumentProxy, PDFPageProxy, RenderTask } from 'pdfjs-dist'
3
2
  import type { Ref } from 'vue'
4
3
  import { markRaw, onUnmounted, reactive, watch } from 'vue'
5
4
 
6
- if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
7
- pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`
5
+ async function loadPdfLibrary() {
6
+ const pdfjsLib = await import('pdfjs-dist')
7
+ if (!pdfjsLib.GlobalWorkerOptions.workerSrc)
8
+ pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`
9
+ return pdfjsLib
8
10
  }
9
11
 
10
12
  function normalizeForMatch(text: string): string {
@@ -70,7 +72,11 @@ export function usePdf(url: Ref<string>) {
70
72
  state.loadError = null
71
73
 
72
74
  try {
73
- const loadingTask = pdfjsLib.getDocument(url.value)
75
+ const requestedUrl = url.value
76
+ const pdfjsLib = await loadPdfLibrary()
77
+ if (isUnmounted)
78
+ return null
79
+ const loadingTask = pdfjsLib.getDocument(requestedUrl)
74
80
  const doc = await loadingTask.promise
75
81
 
76
82
  if (isUnmounted) {
@@ -304,6 +310,7 @@ export function usePdf(url: Ref<string>) {
304
310
  }
305
311
  }
306
312
 
313
+ const pdfjsLib = await loadPdfLibrary()
307
314
  for (let itemIndex = 0; itemIndex < textItems.length; itemIndex++) {
308
315
  if (isUnmounted)
309
316
  return
@@ -183,11 +183,11 @@ Root pages get the sidebar for navigation and context. Details pages get the hea
183
183
 
184
184
  Before building a new page from scratch, check for an existing **layout template** and use it if one fits. Layouts package the entire page structure — scroll model, header, columns, footer — so surfaces stay consistent and you don't re-assemble (or re-debug) that scaffolding by hand.
185
185
 
186
- | Layout | Use for | Docs |
187
- | --- | --- | --- |
188
- | `TelaHome` (`home.vue`) | Root index / list / dashboard pages with a sidebar rail. | `components/tela/home/home.mdx` |
189
- | `TelaDetails` (`details.vue`) | Full-screen detail / record pages and fullscreen modals. | `components/tela/details/details.mdx` |
190
- | `TelaRfc` (`rfc.vue`) | Full-screen technical proposals rendered from Markdown, with generated section navigation and diagram support. | `components/tela/rfc/rfc.mdx` |
186
+ | Layout | Use for | Docs |
187
+ | ----------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
188
+ | `TelaHome` (`home.vue`) | Root index / list / dashboard pages with a sidebar rail. | `components/tela/home/home.mdx` |
189
+ | `TelaDetails` (`details.vue`) | Full-screen detail / record pages and fullscreen modals. | `components/tela/details/details.mdx` |
190
+ | `TelaRfc` (`rfc.vue`) | Full-screen technical proposals rendered from Markdown, with generated section navigation and diagram support. | `components/tela/rfc/rfc.mdx` |
191
191
 
192
192
  **`TelaHome`** — a flex-row shell: a sticky `TelaSidebar` beside a scrolling `TelaHomeContent` column that stacks a page title, a metric-card row (`TelaHomeMetrics`), a filter toolbar (`TelaHomeToolbar`), and a data table. It does **not** own scroll — the sidebar pins itself (`sticky top-0 h-screen`) and the page scrolls as one, no `overflow` container or height hack. Expandable detail rows are an opt-in enhancement.
193
193
 
@@ -288,11 +288,11 @@ Always use `TelaModal`. `TelaDialog` is deprecated — never use it.
288
288
 
289
289
  ```vue
290
290
  <TelaModal
291
- v-model="isOpen"
292
- modal-width="md"
293
- :compact="true"
294
- :hide-dividers="true"
295
- :is-close-icon="false"
291
+ v-model="isOpen"
292
+ modal-width="md"
293
+ :compact="true"
294
+ :hide-dividers="true"
295
+ :is-close-icon="false"
296
296
  >
297
297
  <div flex="~ col" w-full gap-16px>
298
298
  <!-- Header -->
@@ -432,6 +432,27 @@ Never duplicate attributes on the same element:
432
432
  </div>
433
433
  ```
434
434
 
435
+ ### Touch Hitbox for Extra-Small Buttons
436
+
437
+ **Always** add the `touch-hitbox` utility (defined in `packages/build/unocss.config.ts`) to extra-small interactive targets — buttons smaller than ~20px, like inline icon buttons, dismiss ×'s on chips/tags (e.g. the close button in `tags.vue`), or the 17px icon buttons inside popovers. It expands the clickable area to at least 32×32px (and 140% of the width) via an invisible centered `::before`, without changing the visual size:
438
+
439
+ ```vue
440
+ <button
441
+ class="touch-hitbox"
442
+ flex
443
+ items-center
444
+ justify-center
445
+ w-17px
446
+ h-17px
447
+ rounded-5px
448
+ hover:bg-background-lowered
449
+ >
450
+ <TelaIcon name="i-ph-x-bold" size="12px" />
451
+ </button>
452
+ ```
453
+
454
+ The element becomes `position: relative`, so don't combine it with a conflicting `absolute`/`fixed` on the same element.
455
+
435
456
  ## Do / Don't
436
457
 
437
458
  **Do:**
@@ -444,6 +465,7 @@ Never duplicate attributes on the same element:
444
465
  - Use `<TelaStatus />` for all status indicators
445
466
  - Use `text-primary` or `text-secondary` for all text content
446
467
  - Use `TelaSelectMenu` for filters with multiple options
468
+ - Add `touch-hitbox` to extra-small (<20px) interactive targets
447
469
 
448
470
  **Don't:**
449
471
 
@@ -457,4 +479,4 @@ Never duplicate attributes on the same element:
457
479
  - Build custom status indicators
458
480
  - Use `text-success` or `text-error` on text — reserved for `<TelaStatus />`
459
481
  - Use standalone icons in interfaces — text and labels are always sufficient
460
- - **Exception:** icons are allowed inside `<TelaButton />` via the `leading` prop
482
+ - **Exception:** icons are allowed inside `<TelaButton />` via the `leading` prop
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.74.1",
3
+ "version": "1.74.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",
package/unocss.config.ts CHANGED
@@ -61,6 +61,7 @@ export default defineConfig({
61
61
  shortcuts: [
62
62
  ['debug', 'ring-2 ring-red ring-inset'],
63
63
  ['clickable', 'cursor-pointer transition hover:translate-y--1px active:translate-y-1px'],
64
+ ['touch-hitbox', 'relative before:absolute before:left-1/2 before:top-1/2 before:block before:h-full before:min-h-44px before:w-full before:min-w-44px before:content-empty before:translate-x--50% before:translate-y--50%'],
64
65
  [/u-ring-([\d.]+)/, ([_, v]) => `ring-${v}`],
65
66
  // typography
66
67
  [/(body|heading)(-caption)?-(h[1-6]|\d+)-(thin|regular|medium|semibold|bold)/i, ([_, tag, type, size, weight]) => {