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,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>
|
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { CarouselHeroItem, CarouselHeroProps } from "../types";
|
|
3
|
-
import NavMenu from "../../nav/ui/nav-menu.svelte";
|
|
4
|
-
import type { Snippet } from "svelte";
|
|
5
|
-
|
|
6
|
-
let {
|
|
7
|
-
items,
|
|
8
|
-
navMenu,
|
|
9
|
-
children,
|
|
10
|
-
className,
|
|
11
|
-
titleClass,
|
|
12
|
-
descriptionClass,
|
|
13
|
-
intervalMs = 5000,
|
|
14
|
-
transitionMs = 1200,
|
|
15
|
-
}: CarouselHeroProps & {
|
|
16
|
-
children?: Snippet;
|
|
17
|
-
intervalMs?: number;
|
|
18
|
-
transitionMs?: number;
|
|
19
|
-
} = $props();
|
|
20
|
-
|
|
21
|
-
// Buffer duplo: duas camadas fixas, nunca desmontadas — só a opacidade muda.
|
|
22
|
-
let layers = $state<[string, string]>([items?.[0]?.image ?? "", ""]);
|
|
23
|
-
let active = $state<0 | 1>(0);
|
|
24
|
-
let index = $state(0);
|
|
25
|
-
let switching = false; // evita trocas sobrepostas se o intervalo disparar antes do preload acabar
|
|
26
|
-
|
|
27
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
28
|
-
const preloadCache = new Map<string, Promise<void>>();
|
|
29
|
-
|
|
30
|
-
function preload(src: string): Promise<void> {
|
|
31
|
-
if (!src) return Promise.resolve();
|
|
32
|
-
const cached = preloadCache.get(src);
|
|
33
|
-
if (cached) return cached;
|
|
34
|
-
|
|
35
|
-
const promise = new Promise<void>((resolve) => {
|
|
36
|
-
const img = new Image();
|
|
37
|
-
img.onload = () => resolve();
|
|
38
|
-
img.onerror = () => resolve(); // não bloqueia o carousel por uma imagem partida
|
|
39
|
-
img.src = src;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
preloadCache.set(src, promise);
|
|
43
|
-
return promise;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Pré-carrega todas as imagens uma vez, assim que os items ficam disponíveis.
|
|
47
|
-
$effect(() => {
|
|
48
|
-
items?.forEach((item) => void preload(item.image));
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
async function goTo(nextIndex: number) {
|
|
52
|
-
if (!items || nextIndex === index || switching) return;
|
|
53
|
-
switching = true;
|
|
54
|
-
|
|
55
|
-
const nextSrc = items[nextIndex].image;
|
|
56
|
-
await preload(nextSrc); // garante decode/cache antes de trocar a camada -> sem flash
|
|
57
|
-
|
|
58
|
-
const nextLayer = active === 0 ? 1 : 0;
|
|
59
|
-
layers[nextLayer] = nextSrc;
|
|
60
|
-
active = nextLayer;
|
|
61
|
-
index = nextIndex;
|
|
62
|
-
switching = false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
$effect(() => {
|
|
66
|
-
if (!items || items.length <= 1) return;
|
|
67
|
-
|
|
68
|
-
const timer = setInterval(() => {
|
|
69
|
-
goTo((index + 1) % items.length);
|
|
70
|
-
}, intervalMs);
|
|
71
|
-
|
|
72
|
-
return () => clearInterval(timer);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
function fadeIn(node: HTMLElement) {
|
|
76
|
-
requestAnimationFrame(() => {
|
|
77
|
-
node.style.opacity = "1";
|
|
78
|
-
});
|
|
79
|
-
return {};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let selected = $derived<CarouselHeroItem | undefined>(items?.[index]);
|
|
83
|
-
</script>
|
|
84
|
-
|
|
85
|
-
{#if selected}
|
|
86
|
-
<header
|
|
87
|
-
class="w-full relative flex flex-col justify-center items-center h-75 md:h-100 overflow-hidden {className}"
|
|
88
|
-
>
|
|
89
|
-
{#each layers as bg, i (i)}
|
|
90
|
-
<div
|
|
91
|
-
style="
|
|
92
|
-
position: absolute;
|
|
93
|
-
inset: 0;
|
|
94
|
-
will-change: opacity;
|
|
95
|
-
background-image: {bg ? `url(${bg})` : 'none'};
|
|
96
|
-
background-size: cover;
|
|
97
|
-
background-position: center;
|
|
98
|
-
background-repeat: no-repeat;
|
|
99
|
-
opacity: {i === active ? 1 : 0};
|
|
100
|
-
transition: opacity {transitionMs}ms ease-in-out;
|
|
101
|
-
"
|
|
102
|
-
></div>
|
|
103
|
-
{/each}
|
|
104
|
-
|
|
105
|
-
<div
|
|
106
|
-
style="position: absolute; inset: 0; height: 100%; background-color: rgba(0,0,0,0.4);"
|
|
107
|
-
></div>
|
|
108
|
-
|
|
109
|
-
<div class="relative z-10 w-full flex flex-col items-center flex-1">
|
|
110
|
-
<NavMenu {...navMenu} />
|
|
111
|
-
|
|
112
|
-
<section
|
|
113
|
-
class="flex flex-col justify-center items-center flex-1 w-full h-auto text-center px-4"
|
|
114
|
-
>
|
|
115
|
-
{#key selected.title}
|
|
116
|
-
<div
|
|
117
|
-
style="transition: opacity {transitionMs * 0.6}ms ease-in-out;"
|
|
118
|
-
class="opacity-0 flex flex-col justify-center items-center text-center"
|
|
119
|
-
use:fadeIn
|
|
120
|
-
>
|
|
121
|
-
<div>
|
|
122
|
-
<h1
|
|
123
|
-
class="text-white text-2xl md:text-4xl font-bold {titleClass}"
|
|
124
|
-
>
|
|
125
|
-
{selected.title}
|
|
126
|
-
</h1>
|
|
127
|
-
</div>
|
|
128
|
-
<div class="flex flex-col justify-center items-center">
|
|
129
|
-
<p class="text-white/90 mt-2 max-w-2xl {descriptionClass}">
|
|
130
|
-
{selected.description}
|
|
131
|
-
</p>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
{/key}
|
|
135
|
-
|
|
136
|
-
{#if children}
|
|
137
|
-
{@render children()}
|
|
138
|
-
{/if}
|
|
139
|
-
</section>
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
{#if items && items.length > 1}
|
|
143
|
-
<div
|
|
144
|
-
style="position: absolute; bottom: 0.75rem; z-index: 10; display: flex; gap: 0.5rem;"
|
|
145
|
-
>
|
|
146
|
-
{#each items as item, i (i)}
|
|
147
|
-
<button
|
|
148
|
-
aria-label={`Ir para o slide ${i + 1}: ${item.title}`}
|
|
149
|
-
class="w-2 h-2 rounded-full transition-colors {i === index
|
|
150
|
-
? 'bg-white'
|
|
151
|
-
: 'bg-white/40'}"
|
|
152
|
-
onclick={() => goTo(i)}
|
|
153
|
-
></button>
|
|
154
|
-
{/each}
|
|
155
|
-
</div>
|
|
156
|
-
{/if}
|
|
157
|
-
</header>
|
|
158
|
-
{/if}
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { CarouselHeroItem, CarouselHeroProps } from "../types";
|
|
3
|
+
import NavMenu from "../../nav/ui/nav-menu.svelte";
|
|
4
|
+
import type { Snippet } from "svelte";
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
items,
|
|
8
|
+
navMenu,
|
|
9
|
+
children,
|
|
10
|
+
className,
|
|
11
|
+
titleClass,
|
|
12
|
+
descriptionClass,
|
|
13
|
+
intervalMs = 5000,
|
|
14
|
+
transitionMs = 1200,
|
|
15
|
+
}: CarouselHeroProps & {
|
|
16
|
+
children?: Snippet;
|
|
17
|
+
intervalMs?: number;
|
|
18
|
+
transitionMs?: number;
|
|
19
|
+
} = $props();
|
|
20
|
+
|
|
21
|
+
// Buffer duplo: duas camadas fixas, nunca desmontadas — só a opacidade muda.
|
|
22
|
+
let layers = $state<[string, string]>([items?.[0]?.image ?? "", ""]);
|
|
23
|
+
let active = $state<0 | 1>(0);
|
|
24
|
+
let index = $state(0);
|
|
25
|
+
let switching = false; // evita trocas sobrepostas se o intervalo disparar antes do preload acabar
|
|
26
|
+
|
|
27
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
28
|
+
const preloadCache = new Map<string, Promise<void>>();
|
|
29
|
+
|
|
30
|
+
function preload(src: string): Promise<void> {
|
|
31
|
+
if (!src) return Promise.resolve();
|
|
32
|
+
const cached = preloadCache.get(src);
|
|
33
|
+
if (cached) return cached;
|
|
34
|
+
|
|
35
|
+
const promise = new Promise<void>((resolve) => {
|
|
36
|
+
const img = new Image();
|
|
37
|
+
img.onload = () => resolve();
|
|
38
|
+
img.onerror = () => resolve(); // não bloqueia o carousel por uma imagem partida
|
|
39
|
+
img.src = src;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
preloadCache.set(src, promise);
|
|
43
|
+
return promise;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Pré-carrega todas as imagens uma vez, assim que os items ficam disponíveis.
|
|
47
|
+
$effect(() => {
|
|
48
|
+
items?.forEach((item) => void preload(item.image));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
async function goTo(nextIndex: number) {
|
|
52
|
+
if (!items || nextIndex === index || switching) return;
|
|
53
|
+
switching = true;
|
|
54
|
+
|
|
55
|
+
const nextSrc = items[nextIndex].image;
|
|
56
|
+
await preload(nextSrc); // garante decode/cache antes de trocar a camada -> sem flash
|
|
57
|
+
|
|
58
|
+
const nextLayer = active === 0 ? 1 : 0;
|
|
59
|
+
layers[nextLayer] = nextSrc;
|
|
60
|
+
active = nextLayer;
|
|
61
|
+
index = nextIndex;
|
|
62
|
+
switching = false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$effect(() => {
|
|
66
|
+
if (!items || items.length <= 1) return;
|
|
67
|
+
|
|
68
|
+
const timer = setInterval(() => {
|
|
69
|
+
goTo((index + 1) % items.length);
|
|
70
|
+
}, intervalMs);
|
|
71
|
+
|
|
72
|
+
return () => clearInterval(timer);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function fadeIn(node: HTMLElement) {
|
|
76
|
+
requestAnimationFrame(() => {
|
|
77
|
+
node.style.opacity = "1";
|
|
78
|
+
});
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let selected = $derived<CarouselHeroItem | undefined>(items?.[index]);
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
{#if selected}
|
|
86
|
+
<header
|
|
87
|
+
class="w-full relative flex flex-col justify-center items-center h-75 md:h-100 overflow-hidden {className}"
|
|
88
|
+
>
|
|
89
|
+
{#each layers as bg, i (`layer-${i}`)}
|
|
90
|
+
<div
|
|
91
|
+
style="
|
|
92
|
+
position: absolute;
|
|
93
|
+
inset: 0;
|
|
94
|
+
will-change: opacity;
|
|
95
|
+
background-image: {bg ? `url(${bg})` : 'none'};
|
|
96
|
+
background-size: cover;
|
|
97
|
+
background-position: center;
|
|
98
|
+
background-repeat: no-repeat;
|
|
99
|
+
opacity: {i === active ? 1 : 0};
|
|
100
|
+
transition: opacity {transitionMs}ms ease-in-out;
|
|
101
|
+
"
|
|
102
|
+
></div>
|
|
103
|
+
{/each}
|
|
104
|
+
|
|
105
|
+
<div
|
|
106
|
+
style="position: absolute; inset: 0; height: 100%; background-color: rgba(0,0,0,0.4);"
|
|
107
|
+
></div>
|
|
108
|
+
|
|
109
|
+
<div class="relative z-10 w-full flex flex-col items-center flex-1">
|
|
110
|
+
<NavMenu {...navMenu} />
|
|
111
|
+
|
|
112
|
+
<section
|
|
113
|
+
class="flex flex-col justify-center items-center flex-1 w-full h-auto text-center px-4"
|
|
114
|
+
>
|
|
115
|
+
{#key selected.title}
|
|
116
|
+
<div
|
|
117
|
+
style="transition: opacity {transitionMs * 0.6}ms ease-in-out;"
|
|
118
|
+
class="opacity-0 flex flex-col justify-center items-center text-center"
|
|
119
|
+
use:fadeIn
|
|
120
|
+
>
|
|
121
|
+
<div>
|
|
122
|
+
<h1
|
|
123
|
+
class="text-white text-2xl md:text-4xl font-bold {titleClass}"
|
|
124
|
+
>
|
|
125
|
+
{selected.title}
|
|
126
|
+
</h1>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="flex flex-col justify-center items-center">
|
|
129
|
+
<p class="text-white/90 mt-2 max-w-2xl {descriptionClass}">
|
|
130
|
+
{selected.description}
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
{/key}
|
|
135
|
+
|
|
136
|
+
{#if children}
|
|
137
|
+
{@render children()}
|
|
138
|
+
{/if}
|
|
139
|
+
</section>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{#if items && items.length > 1}
|
|
143
|
+
<div
|
|
144
|
+
style="position: absolute; bottom: 0.75rem; z-index: 10; display: flex; gap: 0.5rem;"
|
|
145
|
+
>
|
|
146
|
+
{#each items as item, i (`hero-dot-${i}`)}
|
|
147
|
+
<button
|
|
148
|
+
aria-label={`Ir para o slide ${i + 1}: ${item.title}`}
|
|
149
|
+
class="w-2 h-2 rounded-full transition-colors {i === index
|
|
150
|
+
? 'bg-white'
|
|
151
|
+
: 'bg-white/40'}"
|
|
152
|
+
onclick={() => goTo(i)}
|
|
153
|
+
></button>
|
|
154
|
+
{/each}
|
|
155
|
+
</div>
|
|
156
|
+
{/if}
|
|
157
|
+
</header>
|
|
158
|
+
{/if}
|