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.
Files changed (115) hide show
  1. package/dist/assets/placeholder-company.png +0 -0
  2. package/dist/assets/placeholder-image.png +0 -0
  3. package/dist/components/core/breadcrumb/breadcrumb-list.svelte +31 -31
  4. package/dist/components/core/button/ButtonBack.svelte +18 -18
  5. package/dist/components/core/button/ButtonLogin.svelte +15 -15
  6. package/dist/components/core/button/ButtonLoginWith.svelte +57 -57
  7. package/dist/components/core/button/ButtonSend.svelte +15 -15
  8. package/dist/components/core/card/highlight/01/CardHighlight01.svelte +110 -91
  9. package/dist/components/core/card/highlight/02/CardHighlight02.svelte +108 -93
  10. package/dist/components/core/card/highlight/CardHighlight.svelte +22 -22
  11. package/dist/components/core/card/highlight/data.js +4 -1
  12. package/dist/components/core/card/media/01/CardMedia01.svelte +111 -111
  13. package/dist/components/core/card/media/02/CardMedia02.svelte +110 -110
  14. package/dist/components/core/card/media/CardMedia.svelte +17 -17
  15. package/dist/components/core/card/profile/01/CardProfile01.svelte +98 -98
  16. package/dist/components/core/card/profile/02/CardProfile02.svelte +98 -98
  17. package/dist/components/core/card/profile/CardProfile.svelte +17 -17
  18. package/dist/components/core/card/promotion/CardPromotion.svelte +133 -112
  19. package/dist/components/core/card/shared/CardButton.svelte +30 -30
  20. package/dist/components/core/card/shared/CardCart.svelte +36 -36
  21. package/dist/components/core/card/shared/CardDescription.svelte +38 -38
  22. package/dist/components/core/card/shared/CardFavorite.svelte +33 -33
  23. package/dist/components/core/card/shared/CardStarRating.svelte +20 -20
  24. package/dist/components/core/card/shared/CardStartPrice.svelte +32 -34
  25. package/dist/components/core/card/shared/CardTags.svelte +70 -73
  26. package/dist/components/core/card/shared/CardThumbnailVideo.svelte +169 -169
  27. package/dist/components/core/card/shared/TruncatableText.svelte +55 -55
  28. package/dist/components/core/carousel/badge/ui/CarouselBadge.svelte +82 -83
  29. package/dist/components/core/carousel/hero/CarouselHero.svelte +158 -158
  30. package/dist/components/core/carousel/highlights/ui/CarouselHighlights.svelte +65 -63
  31. package/dist/components/core/carousel/promotion/ui/CarouselPromotion.svelte +64 -64
  32. package/dist/components/core/carousel/shared/ui/CarouselHeader.svelte +55 -55
  33. package/dist/components/core/datatable/ui/DataTableActions.svelte +28 -28
  34. package/dist/components/core/datatable/ui/DataTableCheckbox.svelte +22 -22
  35. package/dist/components/core/datatable/ui/DataTableContent.svelte +59 -59
  36. package/dist/components/core/datatable/ui/DataTableCore.svelte +151 -151
  37. package/dist/components/core/datatable/ui/DataTableDragHandle.svelte +8 -8
  38. package/dist/components/core/datatable/ui/DataTableListFilter.svelte +59 -59
  39. package/dist/components/core/datatable/ui/DataTableListPagination.svelte +35 -35
  40. package/dist/components/core/form/ui/input-base.svelte +81 -81
  41. package/dist/components/core/form/ui/input-email-or-phone.svelte +23 -23
  42. package/dist/components/core/form/ui/input-email.svelte +23 -23
  43. package/dist/components/core/form/ui/input-password-confirm.svelte +23 -23
  44. package/dist/components/core/form/ui/input-password.svelte +23 -23
  45. package/dist/components/core/form/ui/input-phone.svelte +33 -33
  46. package/dist/components/core/form/ui/input-username.svelte +22 -22
  47. package/dist/components/core/hero/SimpleHero.svelte +65 -65
  48. package/dist/components/core/label/LabelOr.svelte +25 -25
  49. package/dist/components/core/link/link-forget-password.svelte +12 -12
  50. package/dist/components/core/link/link-privacy-polity.svelte +12 -12
  51. package/dist/components/core/link/link-terms-of-service.svelte +12 -12
  52. package/dist/components/core/nav/ui/menu-links.svelte +27 -27
  53. package/dist/components/core/nav/ui/mobile/nav-menu-drawer.svelte +85 -85
  54. package/dist/components/core/nav/ui/nav-menu-bottom-sidebar.svelte +33 -33
  55. package/dist/components/core/nav/ui/nav-menu-sidebar.svelte +97 -97
  56. package/dist/components/core/nav/ui/nav-menu-top-bar-sidebar.svelte +103 -103
  57. package/dist/components/core/nav/ui/nav-menu.svelte +106 -106
  58. package/dist/components/core/panel/CarouselSlot.svelte +63 -63
  59. package/dist/components/core/panel/LeftHero.svelte +116 -116
  60. package/dist/components/core/search/SearchInput.svelte +32 -32
  61. package/dist/components/core/search/shared/SearchInputCore.svelte +66 -66
  62. package/dist/components/core/sidebar/MenuBarSidebar.svelte +70 -70
  63. package/dist/components/core/tabs/ui/tab-underline.svelte +43 -43
  64. package/dist/components/pages/admin/01/ui/AdminContent.svelte +14 -14
  65. package/dist/components/pages/admin/01/ui/AdminPanel.svelte +46 -46
  66. package/dist/components/pages/security/login/01/ui/FormRegister.svelte +28 -28
  67. package/dist/components/pages/security/login/01/ui/LoginCard.svelte +107 -107
  68. package/dist/components/pages/security/login/PageLogin.svelte +22 -22
  69. package/dist/components/ui/badge/badge.svelte +49 -49
  70. package/dist/components/ui/button/button.svelte +82 -82
  71. package/dist/components/ui/chart/chart-container.svelte +80 -80
  72. package/dist/components/ui/chart/chart-style.svelte +37 -37
  73. package/dist/components/ui/chart/chart-tooltip.svelte +184 -184
  74. package/dist/components/ui/checkbox/checkbox.svelte +40 -40
  75. package/dist/components/ui/data-table/flex-render.svelte +40 -40
  76. package/dist/components/ui/drawer/drawer-close.svelte +7 -7
  77. package/dist/components/ui/drawer/drawer-content.svelte +33 -33
  78. package/dist/components/ui/drawer/drawer-description.svelte +17 -17
  79. package/dist/components/ui/drawer/drawer-footer.svelte +20 -20
  80. package/dist/components/ui/drawer/drawer-header.svelte +20 -20
  81. package/dist/components/ui/drawer/drawer-nested.svelte +12 -12
  82. package/dist/components/ui/drawer/drawer-overlay.svelte +17 -17
  83. package/dist/components/ui/drawer/drawer-portal.svelte +7 -7
  84. package/dist/components/ui/drawer/drawer-title.svelte +17 -17
  85. package/dist/components/ui/drawer/drawer-trigger.svelte +7 -7
  86. package/dist/components/ui/drawer/drawer.svelte +12 -12
  87. package/dist/components/ui/select/select-content.svelte +45 -45
  88. package/dist/components/ui/select/select-group-heading.svelte +21 -21
  89. package/dist/components/ui/select/select-group.svelte +17 -17
  90. package/dist/components/ui/select/select-item.svelte +41 -41
  91. package/dist/components/ui/select/select-label.svelte +20 -20
  92. package/dist/components/ui/select/select-portal.svelte +7 -7
  93. package/dist/components/ui/select/select-scroll-down-button.svelte +21 -21
  94. package/dist/components/ui/select/select-scroll-up-button.svelte +21 -21
  95. package/dist/components/ui/select/select-separator.svelte +18 -18
  96. package/dist/components/ui/select/select-trigger.svelte +30 -30
  97. package/dist/components/ui/select/select.svelte +11 -11
  98. package/dist/components/ui/sidebar/sidebar-menu-sub-button.svelte +39 -39
  99. package/dist/components/ui/star-rating/star-rating-star.svelte +41 -41
  100. package/dist/components/ui/star-rating/star-rating.svelte +20 -20
  101. package/dist/components/ui/table/table-body.svelte +15 -15
  102. package/dist/components/ui/table/table-caption.svelte +20 -20
  103. package/dist/components/ui/table/table-cell.svelte +15 -15
  104. package/dist/components/ui/table/table-footer.svelte +20 -20
  105. package/dist/components/ui/table/table-head.svelte +15 -15
  106. package/dist/components/ui/table/table-header.svelte +20 -20
  107. package/dist/components/ui/table/table-row.svelte +15 -15
  108. package/dist/components/ui/table/table.svelte +17 -17
  109. package/dist/components/ui/tabs/tabs-content.svelte +17 -17
  110. package/dist/components/ui/tabs/tabs-list.svelte +40 -40
  111. package/dist/components/ui/tabs/tabs-trigger.svelte +23 -23
  112. package/dist/components/ui/tabs/tabs.svelte +19 -19
  113. package/dist/globals.css +8 -38
  114. package/dist/styles/globals.css +149 -149
  115. package/package.json +113 -113
@@ -1,34 +1,32 @@
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 md:text-3xl lg:text-4xl">{newPrice}</div>
24
- <div class="mx-2">/</div>
25
- <div
26
- class="font-semibold line-through text-red-600 text-[10px] md:text-xl"
27
- >
28
- {oldPrice}
29
- </div>
30
- </div>
31
- {:else}
32
- <div class="z-20 font-bold">{newPrice}</div>
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
- 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
33
- icon={tag.icon}
34
- class="mr-0.5 h-3.75 w-3.75 shrink-0"
35
- />
36
- {/if}
37
- <div class="whitespace-nowrap">{tag.text}</div>
38
- </div>
39
- {/snippet}
40
-
41
- {#snippet skeletonTag()}
42
- <div class="my-2 flex gap-5">
43
- {#each Array.from({ length: 3 }) as it, i (i)}
44
- <Skeleton class="h-5 w-25 bg-black/50" />
45
- {/each}
46
- </div>
47
- {/snippet}
48
-
49
- {#if isWrap}
50
- <div class="flex flex-wrap gap-2 md:gap-5 {className}">
51
- {#if isLoading}
52
- {@render skeletonTag()}
53
- {:else if tags && tags.length > 0}
54
- {#each tags as tag, i (i)}
55
- {@render createBadge(tag)}
56
- {/each}
57
- {/if}
58
- </div>
59
- {:else}
60
- <div
61
- class="flex overflow-x-auto no-scrollbar w-full
62
- {isJustify ? 'justify-between' : 'gap-4 md:gap-5 lg:gap-10'}
63
- {className}"
64
- >
65
- {#if isLoading}
66
- {@render skeletonTag()}
67
- {:else if tags && tags.length > 0}
68
- {#each tags as tag, i (i)}
69
- {@render createBadge(tag)}
70
- {/each}
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}