@meistrari/tela-build 1.74.2 → 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 +6 -0
- package/components/tela/select-menu/select-menu-content.vue +2 -5
- package/components/tela/tags/tags-select.mdx +2 -2
- package/components/tela/tags/tags-select.vue +71 -54
- package/components/tela/tags/tags.vue +17 -11
- package/docs/interfaces.md +33 -11
- package/package.json +1 -1
- package/unocss.config.ts +1 -0
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.
|
|
@@ -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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
<
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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="
|
|
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-
|
|
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-[
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
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"
|
|
584
|
-
<button v-for="tag in allCreatedTags" :key="`${tag.color}-${tag.name}`" class="group/tag" flex items-center justify-between hover:bg-
|
|
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-
|
|
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
|
-
<
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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-[
|
|
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-
|
|
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
|
|
658
|
+
color="white"
|
|
638
659
|
/>
|
|
639
660
|
</div>
|
|
640
661
|
</button>
|
|
641
662
|
</div>
|
|
642
|
-
<
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
663
|
-
|
|
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
|
|
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
|
-
|
|
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"
|
|
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('
|
|
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-
|
|
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"
|
|
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
|
|
145
|
-
<TelaIcon name="i-ph-x-bold" size="12px" color="
|
|
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>
|
package/docs/interfaces.md
CHANGED
|
@@ -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
|
|
187
|
-
|
|
|
188
|
-
| `TelaHome` (`home.vue`)
|
|
189
|
-
| `TelaDetails` (`details.vue`) | Full-screen detail / record pages and fullscreen modals.
|
|
190
|
-
| `TelaRfc` (`rfc.vue`)
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
482
|
+
- **Exception:** icons are allowed inside `<TelaButton />` via the `leading` prop
|
package/package.json
CHANGED
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]) => {
|