negodesign 0.0.26 → 0.0.28
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/assets/placeholder-company.png +0 -0
- package/dist/assets/placeholder-image.png +0 -0
- 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 +110 -91
- package/dist/components/core/card/highlight/02/CardHighlight02.svelte +108 -93
- package/dist/components/core/card/highlight/CardHighlight.svelte +22 -22
- package/dist/components/core/card/highlight/data.js +4 -1
- 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 +133 -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 +32 -34
- package/dist/components/core/card/shared/CardTags.svelte +70 -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 +65 -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 +8 -38
- package/dist/styles/globals.css +149 -149
- package/package.json +113 -113
|
@@ -1,34 +1,32 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
</script>
|
|
12
|
-
|
|
13
|
-
<div class="relative min-w-25 md:min-w-32">
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{/if}
|
|
34
|
-
</div>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import StartBg from "../../../../assets/start.png";
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
newPrice?: string | number;
|
|
6
|
+
oldPrice?: string | number;
|
|
7
|
+
type?: "start" | "normal";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
let { newPrice, oldPrice, type = "normal" }: Props = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div class="relative min-w-25 md:min-w-32">
|
|
14
|
+
{#if type == "start"}
|
|
15
|
+
<img
|
|
16
|
+
src={StartBg}
|
|
17
|
+
alt=""
|
|
18
|
+
class="shadow rounded-tl-lg rounded-bl-lg md:w-20 md:h-20 lg:w-32 lg:h-24 absolute"
|
|
19
|
+
/>
|
|
20
|
+
{/if}
|
|
21
|
+
{#if newPrice && oldPrice}
|
|
22
|
+
<div class="z-20 flex justify-center items-center">
|
|
23
|
+
<div class="font-extrabold text-lg">{newPrice}</div>
|
|
24
|
+
<div class="mx-2">/</div>
|
|
25
|
+
<div class="font-semibold line-through text-red-600 text-[10px]">
|
|
26
|
+
{oldPrice}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
{:else}
|
|
30
|
+
<div class="z-20 font-bold">{newPrice}</div>
|
|
31
|
+
{/if}
|
|
32
|
+
</div>
|
|
@@ -1,73 +1,70 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</script>
|
|
24
|
-
|
|
25
|
-
{#snippet createBadge(tag: CardTagsProps)}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
{/if}
|
|
72
|
-
</div>
|
|
73
|
-
{/if}
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
3
|
+
import type { CardTagsProps } from "../types";
|
|
4
|
+
import Skeleton from "../../../ui/skeleton/skeleton.svelte";
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
className?: string;
|
|
8
|
+
tags?: CardTagsProps[];
|
|
9
|
+
isTagBorderBottom?: boolean;
|
|
10
|
+
isWrap?: boolean;
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
tags,
|
|
16
|
+
className,
|
|
17
|
+
isTagBorderBottom,
|
|
18
|
+
isWrap = false,
|
|
19
|
+
isLoading = false,
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
let isJustify = $derived(tags?.length == 2 || tags?.length == 3);
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
{#snippet createBadge(tag: CardTagsProps)}
|
|
26
|
+
<div
|
|
27
|
+
class="flex items-center h-5 shrink-0 {isTagBorderBottom
|
|
28
|
+
? 'border border-gray-200 rounded-full px-2 py-1'
|
|
29
|
+
: ''}"
|
|
30
|
+
>
|
|
31
|
+
{#if tag.icon}
|
|
32
|
+
<HugeiconsIcon icon={tag.icon} class="mr-0.5 h-3.75 w-3.75 shrink-0" />
|
|
33
|
+
{/if}
|
|
34
|
+
<div class="whitespace-nowrap">{tag.text}</div>
|
|
35
|
+
</div>
|
|
36
|
+
{/snippet}
|
|
37
|
+
|
|
38
|
+
{#snippet skeletonTag()}
|
|
39
|
+
<div class="my-2 flex gap-5">
|
|
40
|
+
{#each Array.from({ length: 3 }) as it, i (i)}
|
|
41
|
+
<Skeleton class="h-5 w-25 bg-black/50" />
|
|
42
|
+
{/each}
|
|
43
|
+
</div>
|
|
44
|
+
{/snippet}
|
|
45
|
+
|
|
46
|
+
{#if isWrap}
|
|
47
|
+
<div class="flex flex-wrap gap-2 md:gap-5 {className}">
|
|
48
|
+
{#if isLoading}
|
|
49
|
+
{@render skeletonTag()}
|
|
50
|
+
{:else if tags && tags.length > 0}
|
|
51
|
+
{#each tags as tag, i (i)}
|
|
52
|
+
{@render createBadge(tag)}
|
|
53
|
+
{/each}
|
|
54
|
+
{/if}
|
|
55
|
+
</div>
|
|
56
|
+
{:else}
|
|
57
|
+
<div
|
|
58
|
+
class="flex overflow-x-auto no-scrollbar w-full
|
|
59
|
+
{isJustify ? 'justify-between gap-3' : 'gap-4 md:gap-5 lg:gap-10'}
|
|
60
|
+
{className}"
|
|
61
|
+
>
|
|
62
|
+
{#if isLoading}
|
|
63
|
+
{@render skeletonTag()}
|
|
64
|
+
{:else if tags && tags.length > 0}
|
|
65
|
+
{#each tags as tag, i (i)}
|
|
66
|
+
{@render createBadge(tag)}
|
|
67
|
+
{/each}
|
|
68
|
+
{/if}
|
|
69
|
+
</div>
|
|
70
|
+
{/if}
|
|
@@ -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}
|