negodesign 0.0.26 → 0.0.27
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/dist/components/core/breadcrumb/breadcrumb-list.svelte +31 -31
- package/dist/components/core/button/ButtonBack.svelte +18 -18
- package/dist/components/core/button/ButtonLogin.svelte +15 -15
- package/dist/components/core/button/ButtonLoginWith.svelte +57 -57
- package/dist/components/core/button/ButtonSend.svelte +15 -15
- package/dist/components/core/card/highlight/01/CardHighlight01.svelte +91 -91
- package/dist/components/core/card/highlight/02/CardHighlight02.svelte +93 -93
- package/dist/components/core/card/highlight/CardHighlight.svelte +22 -22
- package/dist/components/core/card/media/01/CardMedia01.svelte +111 -111
- package/dist/components/core/card/media/02/CardMedia02.svelte +110 -110
- package/dist/components/core/card/media/CardMedia.svelte +17 -17
- package/dist/components/core/card/profile/01/CardProfile01.svelte +98 -98
- package/dist/components/core/card/profile/02/CardProfile02.svelte +98 -98
- package/dist/components/core/card/profile/CardProfile.svelte +17 -17
- package/dist/components/core/card/promotion/CardPromotion.svelte +112 -112
- package/dist/components/core/card/shared/CardButton.svelte +30 -30
- package/dist/components/core/card/shared/CardCart.svelte +36 -36
- package/dist/components/core/card/shared/CardDescription.svelte +38 -38
- package/dist/components/core/card/shared/CardFavorite.svelte +33 -33
- package/dist/components/core/card/shared/CardStarRating.svelte +20 -20
- package/dist/components/core/card/shared/CardStartPrice.svelte +34 -34
- package/dist/components/core/card/shared/CardTags.svelte +73 -73
- package/dist/components/core/card/shared/CardThumbnailVideo.svelte +169 -169
- package/dist/components/core/card/shared/TruncatableText.svelte +55 -55
- package/dist/components/core/carousel/badge/ui/CarouselBadge.svelte +82 -83
- package/dist/components/core/carousel/hero/CarouselHero.svelte +158 -158
- package/dist/components/core/carousel/highlights/ui/CarouselHighlights.svelte +63 -63
- package/dist/components/core/carousel/promotion/ui/CarouselPromotion.svelte +64 -64
- package/dist/components/core/carousel/shared/ui/CarouselHeader.svelte +55 -55
- package/dist/components/core/datatable/ui/DataTableActions.svelte +28 -28
- package/dist/components/core/datatable/ui/DataTableCheckbox.svelte +22 -22
- package/dist/components/core/datatable/ui/DataTableContent.svelte +59 -59
- package/dist/components/core/datatable/ui/DataTableCore.svelte +151 -151
- package/dist/components/core/datatable/ui/DataTableDragHandle.svelte +8 -8
- package/dist/components/core/datatable/ui/DataTableListFilter.svelte +59 -59
- package/dist/components/core/datatable/ui/DataTableListPagination.svelte +35 -35
- package/dist/components/core/form/ui/input-base.svelte +81 -81
- package/dist/components/core/form/ui/input-email-or-phone.svelte +23 -23
- package/dist/components/core/form/ui/input-email.svelte +23 -23
- package/dist/components/core/form/ui/input-password-confirm.svelte +23 -23
- package/dist/components/core/form/ui/input-password.svelte +23 -23
- package/dist/components/core/form/ui/input-phone.svelte +33 -33
- package/dist/components/core/form/ui/input-username.svelte +22 -22
- package/dist/components/core/hero/SimpleHero.svelte +65 -65
- package/dist/components/core/label/LabelOr.svelte +25 -25
- package/dist/components/core/link/link-forget-password.svelte +12 -12
- package/dist/components/core/link/link-privacy-polity.svelte +12 -12
- package/dist/components/core/link/link-terms-of-service.svelte +12 -12
- package/dist/components/core/nav/ui/menu-links.svelte +27 -27
- package/dist/components/core/nav/ui/mobile/nav-menu-drawer.svelte +85 -85
- package/dist/components/core/nav/ui/nav-menu-bottom-sidebar.svelte +33 -33
- package/dist/components/core/nav/ui/nav-menu-sidebar.svelte +97 -97
- package/dist/components/core/nav/ui/nav-menu-top-bar-sidebar.svelte +103 -103
- package/dist/components/core/nav/ui/nav-menu.svelte +106 -106
- package/dist/components/core/panel/CarouselSlot.svelte +63 -63
- package/dist/components/core/panel/LeftHero.svelte +116 -116
- package/dist/components/core/search/SearchInput.svelte +32 -32
- package/dist/components/core/search/shared/SearchInputCore.svelte +66 -66
- package/dist/components/core/sidebar/MenuBarSidebar.svelte +70 -70
- package/dist/components/core/tabs/ui/tab-underline.svelte +43 -43
- package/dist/components/pages/admin/01/ui/AdminContent.svelte +14 -14
- package/dist/components/pages/admin/01/ui/AdminPanel.svelte +46 -46
- package/dist/components/pages/security/login/01/ui/FormRegister.svelte +28 -28
- package/dist/components/pages/security/login/01/ui/LoginCard.svelte +107 -107
- package/dist/components/pages/security/login/PageLogin.svelte +22 -22
- package/dist/components/ui/badge/badge.svelte +49 -49
- package/dist/components/ui/button/button.svelte +82 -82
- package/dist/components/ui/chart/chart-container.svelte +80 -80
- package/dist/components/ui/chart/chart-style.svelte +37 -37
- package/dist/components/ui/chart/chart-tooltip.svelte +184 -184
- package/dist/components/ui/checkbox/checkbox.svelte +40 -40
- package/dist/components/ui/data-table/flex-render.svelte +40 -40
- package/dist/components/ui/drawer/drawer-close.svelte +7 -7
- package/dist/components/ui/drawer/drawer-content.svelte +33 -33
- package/dist/components/ui/drawer/drawer-description.svelte +17 -17
- package/dist/components/ui/drawer/drawer-footer.svelte +20 -20
- package/dist/components/ui/drawer/drawer-header.svelte +20 -20
- package/dist/components/ui/drawer/drawer-nested.svelte +12 -12
- package/dist/components/ui/drawer/drawer-overlay.svelte +17 -17
- package/dist/components/ui/drawer/drawer-portal.svelte +7 -7
- package/dist/components/ui/drawer/drawer-title.svelte +17 -17
- package/dist/components/ui/drawer/drawer-trigger.svelte +7 -7
- package/dist/components/ui/drawer/drawer.svelte +12 -12
- package/dist/components/ui/select/select-content.svelte +45 -45
- package/dist/components/ui/select/select-group-heading.svelte +21 -21
- package/dist/components/ui/select/select-group.svelte +17 -17
- package/dist/components/ui/select/select-item.svelte +41 -41
- package/dist/components/ui/select/select-label.svelte +20 -20
- package/dist/components/ui/select/select-portal.svelte +7 -7
- package/dist/components/ui/select/select-scroll-down-button.svelte +21 -21
- package/dist/components/ui/select/select-scroll-up-button.svelte +21 -21
- package/dist/components/ui/select/select-separator.svelte +18 -18
- package/dist/components/ui/select/select-trigger.svelte +30 -30
- package/dist/components/ui/select/select.svelte +11 -11
- package/dist/components/ui/sidebar/sidebar-menu-sub-button.svelte +39 -39
- package/dist/components/ui/star-rating/star-rating-star.svelte +41 -41
- package/dist/components/ui/star-rating/star-rating.svelte +20 -20
- package/dist/components/ui/table/table-body.svelte +15 -15
- package/dist/components/ui/table/table-caption.svelte +20 -20
- package/dist/components/ui/table/table-cell.svelte +15 -15
- package/dist/components/ui/table/table-footer.svelte +20 -20
- package/dist/components/ui/table/table-head.svelte +15 -15
- package/dist/components/ui/table/table-header.svelte +20 -20
- package/dist/components/ui/table/table-row.svelte +15 -15
- package/dist/components/ui/table/table.svelte +17 -17
- package/dist/components/ui/tabs/tabs-content.svelte +17 -17
- package/dist/components/ui/tabs/tabs-list.svelte +40 -40
- package/dist/components/ui/tabs/tabs-trigger.svelte +23 -23
- package/dist/components/ui/tabs/tabs.svelte +19 -19
- package/dist/globals.css +3 -0
- package/dist/styles/globals.css +149 -149
- package/package.json +113 -113
|
@@ -1,170 +1,170 @@
|
|
|
1
|
-
<!-- CardMedia.svelte -->
|
|
2
|
-
<script lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* Image thumbnail with hover-to-play video, lazy loading,
|
|
5
|
-
* duration badge, video indicator, and optional maximize overlay.
|
|
6
|
-
* @component
|
|
7
|
-
*/
|
|
8
|
-
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
9
|
-
import { Maximize01Icon, Cancel01Icon, VideoReplayIcon } from "@hugeicons/core-free-icons";
|
|
10
|
-
|
|
11
|
-
type Props = {
|
|
12
|
-
imageUrl?: string;
|
|
13
|
-
videoUrl?: string;
|
|
14
|
-
class?: string;
|
|
15
|
-
isImageButtonMaximized?: boolean;
|
|
16
|
-
isVideoButtonMaximized?: boolean;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
let {
|
|
20
|
-
imageUrl,
|
|
21
|
-
videoUrl,
|
|
22
|
-
class: className = "w-64 h-64",
|
|
23
|
-
isImageButtonMaximized = false,
|
|
24
|
-
isVideoButtonMaximized = false,
|
|
25
|
-
}: Props = $props();
|
|
26
|
-
|
|
27
|
-
let containerEl: HTMLDivElement | undefined = $state();
|
|
28
|
-
let videoEl: HTMLVideoElement | undefined = $state();
|
|
29
|
-
|
|
30
|
-
let isHovering = $state(false);
|
|
31
|
-
let isVisible = $state(false);
|
|
32
|
-
let isMaximized = $state(false);
|
|
33
|
-
let duration = $state("");
|
|
34
|
-
|
|
35
|
-
let showVideo = $derived(!imageUrl || isHovering);
|
|
36
|
-
let showMaximize = $derived((imageUrl && isImageButtonMaximized) || (videoUrl && isVideoButtonMaximized));
|
|
37
|
-
let showVideoBadge = $derived(!!imageUrl && !!videoUrl && !isHovering);
|
|
38
|
-
|
|
39
|
-
function onLoadedMetadata() {
|
|
40
|
-
if (!videoEl) return;
|
|
41
|
-
const m = Math.floor(videoEl.duration / 60);
|
|
42
|
-
const s = Math.floor(videoEl.duration % 60);
|
|
43
|
-
duration = `${m}:${s.toString().padStart(2, "0")}`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function toggleHover(state: boolean) {
|
|
47
|
-
if (!videoUrl) return;
|
|
48
|
-
isHovering = state;
|
|
49
|
-
if (state) videoEl?.play();
|
|
50
|
-
else {
|
|
51
|
-
videoEl?.pause();
|
|
52
|
-
if (imageUrl && videoEl) videoEl.currentTime = 0;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
$effect(() => {
|
|
57
|
-
if (!containerEl) return;
|
|
58
|
-
const observer = new IntersectionObserver(
|
|
59
|
-
([entry]) => {
|
|
60
|
-
if (entry.isIntersecting) {
|
|
61
|
-
isVisible = true;
|
|
62
|
-
observer.disconnect();
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
{ rootMargin: "100px" }
|
|
66
|
-
);
|
|
67
|
-
observer.observe(containerEl);
|
|
68
|
-
return () => observer.disconnect();
|
|
69
|
-
});
|
|
70
|
-
</script>
|
|
71
|
-
|
|
72
|
-
<svelte:window onkeydown={isMaximized ? (e) => e.key === "Escape" && (isMaximized = false) : undefined} />
|
|
73
|
-
|
|
74
|
-
{#snippet maximizeButton()}
|
|
75
|
-
<button
|
|
76
|
-
type="button"
|
|
77
|
-
class="absolute top-2 right-2 z-10 rounded-full bg-black/50 p-1.5 text-white transition-colors hover:bg-black/70"
|
|
78
|
-
onclick={(e) => { e.stopPropagation(); isMaximized = true; videoEl?.pause(); }}
|
|
79
|
-
aria-label="Maximizar"
|
|
80
|
-
>
|
|
81
|
-
<HugeiconsIcon icon={Maximize01Icon} size={16} color="currentColor" strokeWidth={1.5} />
|
|
82
|
-
</button>
|
|
83
|
-
{/snippet}
|
|
84
|
-
|
|
85
|
-
{#if imageUrl || videoUrl}
|
|
86
|
-
<div
|
|
87
|
-
bind:this={containerEl}
|
|
88
|
-
class="relative w-full {className}"
|
|
89
|
-
role="button"
|
|
90
|
-
tabindex="0"
|
|
91
|
-
onmouseenter={() => toggleHover(true)}
|
|
92
|
-
onmouseleave={() => toggleHover(false)}
|
|
93
|
-
onfocus={() => toggleHover(true)}
|
|
94
|
-
onblur={() => toggleHover(false)}
|
|
95
|
-
onkeydown={(e) => (e.key === "Enter" || e.key === " ") && (e.preventDefault(), toggleHover(!isHovering))}
|
|
96
|
-
>
|
|
97
|
-
{#if imageUrl}
|
|
98
|
-
<img
|
|
99
|
-
src={isVisible ? imageUrl : undefined}
|
|
100
|
-
loading="lazy"
|
|
101
|
-
decoding="async"
|
|
102
|
-
alt={imageUrl}
|
|
103
|
-
class="absolute inset-0 w-full {className} rounded-md object-cover transition-opacity duration-500"
|
|
104
|
-
class:opacity-0={videoUrl && showVideo}
|
|
105
|
-
/>
|
|
106
|
-
{/if}
|
|
107
|
-
|
|
108
|
-
{#if videoUrl && isVisible}
|
|
109
|
-
<video
|
|
110
|
-
bind:this={videoEl}
|
|
111
|
-
src={videoUrl}
|
|
112
|
-
preload="metadata"
|
|
113
|
-
onloadedmetadata={onLoadedMetadata}
|
|
114
|
-
class="absolute inset-0 w-full {className} rounded-md object-cover transition-opacity duration-500"
|
|
115
|
-
class:opacity-0={imageUrl && !showVideo}
|
|
116
|
-
muted
|
|
117
|
-
loop
|
|
118
|
-
playsinline
|
|
119
|
-
></video>
|
|
120
|
-
{/if}
|
|
121
|
-
|
|
122
|
-
{#if showVideoBadge}
|
|
123
|
-
<div class="pointer-events-none absolute inset-0 flex items-center justify-center transition-opacity duration-300">
|
|
124
|
-
<div class="flex h-11 w-11 items-center justify-center rounded-full bg-black/50 backdrop-blur-sm">
|
|
125
|
-
<HugeiconsIcon icon={VideoReplayIcon} size={22} color="white" strokeWidth={1.5} />
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
{#if duration}
|
|
129
|
-
<span class="absolute bottom-2 right-2 z-10 rounded bg-black/60 px-1.5 py-0.5 text-xs font-medium text-white">
|
|
130
|
-
{duration}
|
|
131
|
-
</span>
|
|
132
|
-
{/if}
|
|
133
|
-
{/if}
|
|
134
|
-
|
|
135
|
-
{#if showMaximize}
|
|
136
|
-
{@render maximizeButton()}
|
|
137
|
-
{/if}
|
|
138
|
-
</div>
|
|
139
|
-
{/if}
|
|
140
|
-
|
|
141
|
-
{#if isMaximized}
|
|
142
|
-
<div class="fixed inset-0 z-50">
|
|
143
|
-
<button
|
|
144
|
-
type="button"
|
|
145
|
-
class="absolute inset-0 h-full w-full cursor-default bg-black/80"
|
|
146
|
-
onclick={() => (isMaximized = false)}
|
|
147
|
-
aria-label="Fechar modal"
|
|
148
|
-
></button>
|
|
149
|
-
|
|
150
|
-
<div class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
151
|
-
<div class="pointer-events-auto relative max-h-[90vh] max-w-4xl">
|
|
152
|
-
<button
|
|
153
|
-
type="button"
|
|
154
|
-
class="absolute top-3 right-3 rounded-full bg-red-900/50 p-2 text-white hover:bg-black/70"
|
|
155
|
-
onclick={() => (isMaximized = false)}
|
|
156
|
-
aria-label="Fechar"
|
|
157
|
-
>
|
|
158
|
-
<HugeiconsIcon icon={Cancel01Icon} size={20} color="currentColor" strokeWidth={1.5} />
|
|
159
|
-
</button>
|
|
160
|
-
|
|
161
|
-
{#if videoUrl && showVideo}
|
|
162
|
-
<!-- svelte-ignore a11y_media_has_caption -->
|
|
163
|
-
<video src={videoUrl} class="max-h-[90vh] max-w-full rounded-md" controls autoplay playsinline></video>
|
|
164
|
-
{:else if imageUrl}
|
|
165
|
-
<img src={imageUrl} alt={imageUrl} class="max-h-[90vh] max-w-full rounded-md object-contain" />
|
|
166
|
-
{/if}
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
1
|
+
<!-- CardMedia.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
/**
|
|
4
|
+
* Image thumbnail with hover-to-play video, lazy loading,
|
|
5
|
+
* duration badge, video indicator, and optional maximize overlay.
|
|
6
|
+
* @component
|
|
7
|
+
*/
|
|
8
|
+
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
9
|
+
import { Maximize01Icon, Cancel01Icon, VideoReplayIcon } from "@hugeicons/core-free-icons";
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
imageUrl?: string;
|
|
13
|
+
videoUrl?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
isImageButtonMaximized?: boolean;
|
|
16
|
+
isVideoButtonMaximized?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
imageUrl,
|
|
21
|
+
videoUrl,
|
|
22
|
+
class: className = "w-64 h-64",
|
|
23
|
+
isImageButtonMaximized = false,
|
|
24
|
+
isVideoButtonMaximized = false,
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
28
|
+
let videoEl: HTMLVideoElement | undefined = $state();
|
|
29
|
+
|
|
30
|
+
let isHovering = $state(false);
|
|
31
|
+
let isVisible = $state(false);
|
|
32
|
+
let isMaximized = $state(false);
|
|
33
|
+
let duration = $state("");
|
|
34
|
+
|
|
35
|
+
let showVideo = $derived(!imageUrl || isHovering);
|
|
36
|
+
let showMaximize = $derived((imageUrl && isImageButtonMaximized) || (videoUrl && isVideoButtonMaximized));
|
|
37
|
+
let showVideoBadge = $derived(!!imageUrl && !!videoUrl && !isHovering);
|
|
38
|
+
|
|
39
|
+
function onLoadedMetadata() {
|
|
40
|
+
if (!videoEl) return;
|
|
41
|
+
const m = Math.floor(videoEl.duration / 60);
|
|
42
|
+
const s = Math.floor(videoEl.duration % 60);
|
|
43
|
+
duration = `${m}:${s.toString().padStart(2, "0")}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toggleHover(state: boolean) {
|
|
47
|
+
if (!videoUrl) return;
|
|
48
|
+
isHovering = state;
|
|
49
|
+
if (state) videoEl?.play();
|
|
50
|
+
else {
|
|
51
|
+
videoEl?.pause();
|
|
52
|
+
if (imageUrl && videoEl) videoEl.currentTime = 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$effect(() => {
|
|
57
|
+
if (!containerEl) return;
|
|
58
|
+
const observer = new IntersectionObserver(
|
|
59
|
+
([entry]) => {
|
|
60
|
+
if (entry.isIntersecting) {
|
|
61
|
+
isVisible = true;
|
|
62
|
+
observer.disconnect();
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{ rootMargin: "100px" }
|
|
66
|
+
);
|
|
67
|
+
observer.observe(containerEl);
|
|
68
|
+
return () => observer.disconnect();
|
|
69
|
+
});
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<svelte:window onkeydown={isMaximized ? (e) => e.key === "Escape" && (isMaximized = false) : undefined} />
|
|
73
|
+
|
|
74
|
+
{#snippet maximizeButton()}
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
class="absolute top-2 right-2 z-10 rounded-full bg-black/50 p-1.5 text-white transition-colors hover:bg-black/70"
|
|
78
|
+
onclick={(e) => { e.stopPropagation(); isMaximized = true; videoEl?.pause(); }}
|
|
79
|
+
aria-label="Maximizar"
|
|
80
|
+
>
|
|
81
|
+
<HugeiconsIcon icon={Maximize01Icon} size={16} color="currentColor" strokeWidth={1.5} />
|
|
82
|
+
</button>
|
|
83
|
+
{/snippet}
|
|
84
|
+
|
|
85
|
+
{#if imageUrl || videoUrl}
|
|
86
|
+
<div
|
|
87
|
+
bind:this={containerEl}
|
|
88
|
+
class="relative w-full {className}"
|
|
89
|
+
role="button"
|
|
90
|
+
tabindex="0"
|
|
91
|
+
onmouseenter={() => toggleHover(true)}
|
|
92
|
+
onmouseleave={() => toggleHover(false)}
|
|
93
|
+
onfocus={() => toggleHover(true)}
|
|
94
|
+
onblur={() => toggleHover(false)}
|
|
95
|
+
onkeydown={(e) => (e.key === "Enter" || e.key === " ") && (e.preventDefault(), toggleHover(!isHovering))}
|
|
96
|
+
>
|
|
97
|
+
{#if imageUrl}
|
|
98
|
+
<img
|
|
99
|
+
src={isVisible ? imageUrl : undefined}
|
|
100
|
+
loading="lazy"
|
|
101
|
+
decoding="async"
|
|
102
|
+
alt={imageUrl}
|
|
103
|
+
class="absolute inset-0 w-full {className} rounded-md object-cover transition-opacity duration-500"
|
|
104
|
+
class:opacity-0={videoUrl && showVideo}
|
|
105
|
+
/>
|
|
106
|
+
{/if}
|
|
107
|
+
|
|
108
|
+
{#if videoUrl && isVisible}
|
|
109
|
+
<video
|
|
110
|
+
bind:this={videoEl}
|
|
111
|
+
src={videoUrl}
|
|
112
|
+
preload="metadata"
|
|
113
|
+
onloadedmetadata={onLoadedMetadata}
|
|
114
|
+
class="absolute inset-0 w-full {className} rounded-md object-cover transition-opacity duration-500"
|
|
115
|
+
class:opacity-0={imageUrl && !showVideo}
|
|
116
|
+
muted
|
|
117
|
+
loop
|
|
118
|
+
playsinline
|
|
119
|
+
></video>
|
|
120
|
+
{/if}
|
|
121
|
+
|
|
122
|
+
{#if showVideoBadge}
|
|
123
|
+
<div class="pointer-events-none absolute inset-0 flex items-center justify-center transition-opacity duration-300">
|
|
124
|
+
<div class="flex h-11 w-11 items-center justify-center rounded-full bg-black/50 backdrop-blur-sm">
|
|
125
|
+
<HugeiconsIcon icon={VideoReplayIcon} size={22} color="white" strokeWidth={1.5} />
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
{#if duration}
|
|
129
|
+
<span class="absolute bottom-2 right-2 z-10 rounded bg-black/60 px-1.5 py-0.5 text-xs font-medium text-white">
|
|
130
|
+
{duration}
|
|
131
|
+
</span>
|
|
132
|
+
{/if}
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
{#if showMaximize}
|
|
136
|
+
{@render maximizeButton()}
|
|
137
|
+
{/if}
|
|
138
|
+
</div>
|
|
139
|
+
{/if}
|
|
140
|
+
|
|
141
|
+
{#if isMaximized}
|
|
142
|
+
<div class="fixed inset-0 z-50">
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
class="absolute inset-0 h-full w-full cursor-default bg-black/80"
|
|
146
|
+
onclick={() => (isMaximized = false)}
|
|
147
|
+
aria-label="Fechar modal"
|
|
148
|
+
></button>
|
|
149
|
+
|
|
150
|
+
<div class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
151
|
+
<div class="pointer-events-auto relative max-h-[90vh] max-w-4xl">
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
class="absolute top-3 right-3 rounded-full bg-red-900/50 p-2 text-white hover:bg-black/70"
|
|
155
|
+
onclick={() => (isMaximized = false)}
|
|
156
|
+
aria-label="Fechar"
|
|
157
|
+
>
|
|
158
|
+
<HugeiconsIcon icon={Cancel01Icon} size={20} color="currentColor" strokeWidth={1.5} />
|
|
159
|
+
</button>
|
|
160
|
+
|
|
161
|
+
{#if videoUrl && showVideo}
|
|
162
|
+
<!-- svelte-ignore a11y_media_has_caption -->
|
|
163
|
+
<video src={videoUrl} class="max-h-[90vh] max-w-full rounded-md" controls autoplay playsinline></video>
|
|
164
|
+
{:else if imageUrl}
|
|
165
|
+
<img src={imageUrl} alt={imageUrl} class="max-h-[90vh] max-w-full rounded-md object-contain" />
|
|
166
|
+
{/if}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
170
|
{/if}
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
<!-- shared/TruncatableText.svelte -->
|
|
2
|
-
<script lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* Clamps text to N lines; if it overflows, shows an indicator
|
|
5
|
-
* ("Ler texto completo") positioned absolutely at the end of the
|
|
6
|
-
* text, that opens the full text in a Popover.
|
|
7
|
-
* @component
|
|
8
|
-
*/
|
|
9
|
-
import * as Popover from "../../../ui/popover";
|
|
10
|
-
import { t } from "../../../../i18n";
|
|
11
|
-
|
|
12
|
-
type Props = { text: string; class?: string; lines?: 2 | 3 | 4 };
|
|
13
|
-
let { text, class: className = "", lines = 2 }: Props = $props();
|
|
14
|
-
|
|
15
|
-
// Tailwind precisa de classes literais no código-fonte para as gerar —
|
|
16
|
-
// "line-clamp-{lines}" interpolado nunca é detectado pelo scanner.
|
|
17
|
-
const clampClasses = {
|
|
18
|
-
2: "line-clamp-2",
|
|
19
|
-
3: "line-clamp-3",
|
|
20
|
-
4: "line-clamp-4",
|
|
21
|
-
} as const;
|
|
22
|
-
|
|
23
|
-
let textEl: HTMLParagraphElement | undefined = $state();
|
|
24
|
-
let isTruncated = $state(false);
|
|
25
|
-
|
|
26
|
-
$effect(() => {
|
|
27
|
-
if (!textEl) return;
|
|
28
|
-
const check = () =>
|
|
29
|
-
(isTruncated = textEl!.scrollHeight > textEl!.clientHeight + 1);
|
|
30
|
-
// requestAnimationFrame garante que o layout com o clamp já foi aplicado
|
|
31
|
-
// antes de medirmos — medir no mesmo tick pode dar valores desatualizados.
|
|
32
|
-
requestAnimationFrame(check);
|
|
33
|
-
const ro = new ResizeObserver(check);
|
|
34
|
-
ro.observe(textEl);
|
|
35
|
-
return () => ro.disconnect();
|
|
36
|
-
});
|
|
37
|
-
</script>
|
|
38
|
-
|
|
39
|
-
<div class="relative">
|
|
40
|
-
<p bind:this={textEl} class="{clampClasses[lines]} {className}">
|
|
41
|
-
{text}
|
|
42
|
-
</p>
|
|
43
|
-
|
|
44
|
-
{#if isTruncated}
|
|
45
|
-
<Popover.Root>
|
|
46
|
-
<Popover.Trigger
|
|
47
|
-
class="absolute bottom-0 right-0 bg-background pl-1 text-[11px] font-medium text-primary underline underline-offset-2 cursor-pointer"
|
|
48
|
-
>
|
|
49
|
-
{$t("label.view.full")}
|
|
50
|
-
</Popover.Trigger>
|
|
51
|
-
<Popover.Content>
|
|
52
|
-
<p class="max-w-xs text-justify text-[12px]">{text}</p>
|
|
53
|
-
</Popover.Content>
|
|
54
|
-
</Popover.Root>
|
|
55
|
-
{/if}
|
|
1
|
+
<!-- shared/TruncatableText.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
/**
|
|
4
|
+
* Clamps text to N lines; if it overflows, shows an indicator
|
|
5
|
+
* ("Ler texto completo") positioned absolutely at the end of the
|
|
6
|
+
* text, that opens the full text in a Popover.
|
|
7
|
+
* @component
|
|
8
|
+
*/
|
|
9
|
+
import * as Popover from "../../../ui/popover";
|
|
10
|
+
import { t } from "../../../../i18n";
|
|
11
|
+
|
|
12
|
+
type Props = { text: string; class?: string; lines?: 2 | 3 | 4 };
|
|
13
|
+
let { text, class: className = "", lines = 2 }: Props = $props();
|
|
14
|
+
|
|
15
|
+
// Tailwind precisa de classes literais no código-fonte para as gerar —
|
|
16
|
+
// "line-clamp-{lines}" interpolado nunca é detectado pelo scanner.
|
|
17
|
+
const clampClasses = {
|
|
18
|
+
2: "line-clamp-2",
|
|
19
|
+
3: "line-clamp-3",
|
|
20
|
+
4: "line-clamp-4",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
let textEl: HTMLParagraphElement | undefined = $state();
|
|
24
|
+
let isTruncated = $state(false);
|
|
25
|
+
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (!textEl) return;
|
|
28
|
+
const check = () =>
|
|
29
|
+
(isTruncated = textEl!.scrollHeight > textEl!.clientHeight + 1);
|
|
30
|
+
// requestAnimationFrame garante que o layout com o clamp já foi aplicado
|
|
31
|
+
// antes de medirmos — medir no mesmo tick pode dar valores desatualizados.
|
|
32
|
+
requestAnimationFrame(check);
|
|
33
|
+
const ro = new ResizeObserver(check);
|
|
34
|
+
ro.observe(textEl);
|
|
35
|
+
return () => ro.disconnect();
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div class="relative">
|
|
40
|
+
<p bind:this={textEl} class="{clampClasses[lines]} {className}">
|
|
41
|
+
{text}
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
{#if isTruncated}
|
|
45
|
+
<Popover.Root>
|
|
46
|
+
<Popover.Trigger
|
|
47
|
+
class="absolute bottom-0 right-0 bg-background pl-1 text-[11px] font-medium text-primary underline underline-offset-2 cursor-pointer"
|
|
48
|
+
>
|
|
49
|
+
{$t("label.view.full")}
|
|
50
|
+
</Popover.Trigger>
|
|
51
|
+
<Popover.Content>
|
|
52
|
+
<p class="max-w-xs text-justify text-[12px]">{text}</p>
|
|
53
|
+
</Popover.Content>
|
|
54
|
+
</Popover.Root>
|
|
55
|
+
{/if}
|
|
56
56
|
</div>
|
|
@@ -1,83 +1,82 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import * as Carousel from "../../../../ui/carousel/index.js";
|
|
3
|
-
import type { CarouselBadgeProps } from "../../types.js";
|
|
4
|
-
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
5
|
-
import CarouselSlot from "../../../panel/CarouselSlot.svelte";
|
|
6
|
-
import Skeleton from "../../../../ui/skeleton/skeleton.svelte";
|
|
7
|
-
import { useDevice } from "../../../../../hooks/responsive.svelte";
|
|
8
|
-
|
|
9
|
-
const {
|
|
10
|
-
items,
|
|
11
|
-
imageClass,
|
|
12
|
-
iconClass,
|
|
13
|
-
labelClass,
|
|
14
|
-
activeClass,
|
|
15
|
-
valueActive,
|
|
16
|
-
isLoading = false,
|
|
17
|
-
itemCarouselClass,
|
|
18
|
-
orientation = "horizontal",
|
|
19
|
-
isBorderItem = false,
|
|
20
|
-
isBorderBottom = false,
|
|
21
|
-
isButtonPreviousAndNext = true,
|
|
22
|
-
buttonPreviousAndNextClass = "",
|
|
23
|
-
}: CarouselBadgeProps = $props();
|
|
24
|
-
|
|
25
|
-
const DEFAULT_IMG_OR_ICON_CLASS = "size-3";
|
|
26
|
-
const responsive = useDevice();
|
|
27
|
-
</script>
|
|
28
|
-
|
|
29
|
-
<CarouselSlot
|
|
30
|
-
{isBorderBottom}
|
|
31
|
-
{isButtonPreviousAndNext}
|
|
32
|
-
{buttonPreviousAndNextClass}
|
|
33
|
-
>
|
|
34
|
-
{#if isLoading}
|
|
35
|
-
<div class="flex items-center justify-between w-full gap-1 md:gap-2">
|
|
36
|
-
{#each Array.from({ length: responsive.isMobile ? 2 : 10 }) as
|
|
37
|
-
<Skeleton class="h-4 w-25 bg-black/20" />
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
${
|
|
48
|
-
${
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
</CarouselSlot>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Carousel from "../../../../ui/carousel/index.js";
|
|
3
|
+
import type { CarouselBadgeProps } from "../../types.js";
|
|
4
|
+
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
5
|
+
import CarouselSlot from "../../../panel/CarouselSlot.svelte";
|
|
6
|
+
import Skeleton from "../../../../ui/skeleton/skeleton.svelte";
|
|
7
|
+
import { useDevice } from "../../../../../hooks/responsive.svelte";
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
items = [],
|
|
11
|
+
imageClass,
|
|
12
|
+
iconClass,
|
|
13
|
+
labelClass,
|
|
14
|
+
activeClass,
|
|
15
|
+
valueActive,
|
|
16
|
+
isLoading = false,
|
|
17
|
+
itemCarouselClass,
|
|
18
|
+
orientation = "horizontal",
|
|
19
|
+
isBorderItem = false,
|
|
20
|
+
isBorderBottom = false,
|
|
21
|
+
isButtonPreviousAndNext = true,
|
|
22
|
+
buttonPreviousAndNextClass = "",
|
|
23
|
+
}: CarouselBadgeProps = $props();
|
|
24
|
+
|
|
25
|
+
const DEFAULT_IMG_OR_ICON_CLASS = "size-3";
|
|
26
|
+
const responsive = useDevice();
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<CarouselSlot
|
|
30
|
+
{isBorderBottom}
|
|
31
|
+
{isButtonPreviousAndNext}
|
|
32
|
+
{buttonPreviousAndNextClass}
|
|
33
|
+
>
|
|
34
|
+
{#if isLoading}
|
|
35
|
+
<div class="flex items-center justify-between w-full gap-1 md:gap-2">
|
|
36
|
+
{#each Array.from({ length: responsive.isMobile ? 2 : 10 }) as _, i (`skeleton-${i}`)}
|
|
37
|
+
<Skeleton class="h-4 w-25 bg-black/20" />
|
|
38
|
+
{/each}
|
|
39
|
+
</div>
|
|
40
|
+
{:else}
|
|
41
|
+
{#each (items ?? []) as item, i (`badge-${i}-${item.value ?? item.label ?? i}`)}
|
|
42
|
+
<Carousel.Item
|
|
43
|
+
onclick={() => item.onClick?.(item.value)}
|
|
44
|
+
class={`
|
|
45
|
+
flex cursor-pointer items-center justify-center gap-1 basis-auto mx-3 pl-0
|
|
46
|
+
${isBorderItem ? "border border-solid border-gray-300 rounded-lg" : ""}
|
|
47
|
+
${item.value === valueActive ? activeClass : ""}
|
|
48
|
+
${itemCarouselClass}`}
|
|
49
|
+
>
|
|
50
|
+
<div
|
|
51
|
+
class={`
|
|
52
|
+
flex gap-1 mx-2 p-0.5 justify-center items-center
|
|
53
|
+
${orientation === "horizontal" ? "flex-row" : "flex-col"}
|
|
54
|
+
`}
|
|
55
|
+
>
|
|
56
|
+
{#if item.image}
|
|
57
|
+
<img
|
|
58
|
+
src={item.image}
|
|
59
|
+
alt={item.label}
|
|
60
|
+
class={imageClass || DEFAULT_IMG_OR_ICON_CLASS}
|
|
61
|
+
/>
|
|
62
|
+
{:else if item.icon}
|
|
63
|
+
{#if typeof item.icon === "string"}
|
|
64
|
+
<div>
|
|
65
|
+
<i
|
|
66
|
+
class={`${item.icon} ${iconClass || DEFAULT_IMG_OR_ICON_CLASS}`}
|
|
67
|
+
>
|
|
68
|
+
</i>
|
|
69
|
+
</div>
|
|
70
|
+
{:else}
|
|
71
|
+
<HugeiconsIcon
|
|
72
|
+
icon={item.icon}
|
|
73
|
+
class={iconClass || DEFAULT_IMG_OR_ICON_CLASS}
|
|
74
|
+
/>
|
|
75
|
+
{/if}
|
|
76
|
+
{/if}
|
|
77
|
+
<div class={`${labelClass}`}>{item.label}</div>
|
|
78
|
+
</div>
|
|
79
|
+
</Carousel.Item>
|
|
80
|
+
{/each}
|
|
81
|
+
{/if}
|
|
82
|
+
</CarouselSlot>
|