negodesign 0.0.13 → 0.0.15

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.
@@ -0,0 +1,140 @@
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
+ navMenu,
8
+ items,
9
+ children,
10
+ intervalMs = 5000,
11
+ transitionMs = 1200,
12
+ }: CarouselHeroProps & {
13
+ children?: Snippet;
14
+ intervalMs?: number;
15
+ transitionMs?: number;
16
+ } = $props();
17
+
18
+ // Buffer duplo: duas camadas fixas, nunca desmontadas — só a opacidade muda.
19
+ let layers = $state<[string, string]>([items?.[0]?.image ?? "", ""]);
20
+ let active = $state<0 | 1>(0);
21
+ let index = $state(0);
22
+ let switching = false; // evita trocas sobrepostas se o intervalo disparar antes do preload acabar
23
+
24
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
25
+ const preloadCache = new Map<string, Promise<void>>();
26
+
27
+ function preload(src: string): Promise<void> {
28
+ if (!src) return Promise.resolve();
29
+ const cached = preloadCache.get(src);
30
+ if (cached) return cached;
31
+
32
+ const promise = new Promise<void>((resolve) => {
33
+ const img = new Image();
34
+ img.onload = () => resolve();
35
+ img.onerror = () => resolve(); // não bloqueia o carousel por uma imagem partida
36
+ img.src = src;
37
+ });
38
+
39
+ preloadCache.set(src, promise);
40
+ return promise;
41
+ }
42
+
43
+ // Pré-carrega todas as imagens uma vez, assim que os items ficam disponíveis.
44
+ $effect(() => {
45
+ items?.forEach((item) => void preload(item.image));
46
+ });
47
+
48
+ async function goTo(nextIndex: number) {
49
+ if (!items || nextIndex === index || switching) return;
50
+ switching = true;
51
+
52
+ const nextSrc = items[nextIndex].image;
53
+ await preload(nextSrc); // garante decode/cache antes de trocar a camada -> sem flash
54
+
55
+ const nextLayer = active === 0 ? 1 : 0;
56
+ layers[nextLayer] = nextSrc;
57
+ active = nextLayer;
58
+ index = nextIndex;
59
+ switching = false;
60
+ }
61
+
62
+ $effect(() => {
63
+ if (!items || items.length <= 1) return;
64
+
65
+ const timer = setInterval(() => {
66
+ goTo((index + 1) % items.length);
67
+ }, intervalMs);
68
+
69
+ return () => clearInterval(timer);
70
+ });
71
+
72
+ function fadeIn(node: HTMLElement) {
73
+ requestAnimationFrame(() => {
74
+ node.style.opacity = "1";
75
+ });
76
+ return {};
77
+ }
78
+
79
+ let selected = $derived<CarouselHeroItem | undefined>(items?.[index]);
80
+ </script>
81
+
82
+ {#if selected}
83
+ <header
84
+ class="w-full relative flex flex-col justify-center items-center h-[300px] md:h-[400px] overflow-hidden"
85
+ >
86
+ {#each layers as bg, i (i)}
87
+ <div
88
+ class="absolute inset-0 bg-cover bg-center will-change-[opacity]"
89
+ style="
90
+ background-image: {bg ? `url(${bg})` : 'none'};
91
+ opacity: {i === active ? 1 : 0};
92
+ transition: opacity {transitionMs}ms ease-in-out;
93
+ "
94
+ ></div>
95
+ {/each}
96
+
97
+ <div class="absolute inset-0 bg-black/40"></div>
98
+
99
+ <div class="relative z-10 w-full flex flex-col items-center flex-1">
100
+ <NavMenu {...navMenu} />
101
+
102
+ <section
103
+ class="flex flex-col justify-center items-center flex-1 w-full h-auto text-center px-4"
104
+ >
105
+ {#key selected.title}
106
+ <div
107
+ style="transition: opacity {transitionMs * 0.6}ms ease-in-out;"
108
+ class="opacity-0"
109
+ use:fadeIn
110
+ >
111
+ <h1 class="text-white text-2xl md:text-4xl font-bold">
112
+ {selected.title}
113
+ </h1>
114
+ <p class="text-white/90 mt-2 max-w-2xl">
115
+ {selected.description}
116
+ </p>
117
+ </div>
118
+ {/key}
119
+
120
+ {#if children}
121
+ {@render children()}
122
+ {/if}
123
+ </section>
124
+ </div>
125
+
126
+ {#if items && items.length > 1}
127
+ <div class="absolute bottom-3 z-10 flex gap-2">
128
+ {#each items as item, i (i)}
129
+ <button
130
+ aria-label={`Ir para o slide ${i + 1}: ${item.title}`}
131
+ class="w-2 h-2 rounded-full transition-colors {i === index
132
+ ? 'bg-white'
133
+ : 'bg-white/40'}"
134
+ onclick={() => goTo(i)}
135
+ ></button>
136
+ {/each}
137
+ </div>
138
+ {/if}
139
+ </header>
140
+ {/if}
@@ -0,0 +1,10 @@
1
+ import type { CarouselHeroProps } from "../types";
2
+ import type { Snippet } from "svelte";
3
+ type $$ComponentProps = CarouselHeroProps & {
4
+ children?: Snippet;
5
+ intervalMs?: number;
6
+ transitionMs?: number;
7
+ };
8
+ declare const CarouselHero: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type CarouselHero = ReturnType<typeof CarouselHero>;
10
+ export default CarouselHero;
@@ -1,5 +1,6 @@
1
1
  import type { IconSvgElement } from "@hugeicons/svelte";
2
2
  import type { CardPromotionProps } from "../card/types";
3
+ import type { NavMenuProps } from "../nav/data/nav-menu";
3
4
  export interface ItemCarousel {
4
5
  value: string;
5
6
  label: string;
@@ -32,3 +33,12 @@ export interface CarouselPromotionProps {
32
33
  containerClass?: string;
33
34
  onButtonViewAll?: () => void;
34
35
  }
36
+ export interface CarouselHeroItem {
37
+ title: string;
38
+ image: string;
39
+ description: string;
40
+ }
41
+ export interface CarouselHeroProps {
42
+ navMenu: NavMenuProps;
43
+ items: CarouselHeroItem[];
44
+ }
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import { Button } from "../../ui/button/index.js";
3
+ import type { HeroSimpleProps } from "./types";
4
+ let {
5
+ title,
6
+ subTitle,
7
+ description,
8
+ image,
9
+ buttons,
10
+ titleClass,
11
+ subTitleClass,
12
+ descriptionClass,
13
+ imageClass,
14
+ }: HeroSimpleProps = $props();
15
+ </script>
16
+
17
+ <section class="relative flex p-2">
18
+ <div class="w-full grid grid-cols-10 md:grid-cols-2 min-h-50 md:min-h-125">
19
+ <div class="flex justify-center items-center">
20
+ <div class="space-y-1">
21
+ {#if title}
22
+ <h1
23
+ class="font-bold text-[24px] md:text-[60px] lg:text-[80px] {titleClass}"
24
+ >
25
+ {title}
26
+ </h1>
27
+ {/if}
28
+ {#if subTitle}
29
+ <p class="font-semibold text-[16px] md:text-[40px] {subTitleClass}">
30
+ {subTitle}
31
+ </p>
32
+ {/if}
33
+ {#if description}
34
+ <p
35
+ class="text-[12px] md:text-[20px] md:w-8/12 mt-5 {descriptionClass}"
36
+ >
37
+ {description}
38
+ </p>
39
+ {/if}
40
+ {#if buttons}
41
+ <div class="flex flex-wrap gap-2 mt-5 md:mt-10">
42
+ {#each buttons as button, i (i)}
43
+ <Button
44
+ href={button.href}
45
+ class={button.className}
46
+ onclick={button.onClick}
47
+ >
48
+ {button.label}
49
+ </Button>
50
+ {/each}
51
+ </div>
52
+ {/if}
53
+ </div>
54
+ </div>
55
+ <div class="flex justify-center items-center">
56
+ {#if image}
57
+ <img
58
+ src={image}
59
+ alt={image}
60
+ class="rounded-lg h-50 md:h-125 w-[60%] object-cover {imageClass}"
61
+ />
62
+ {/if}
63
+ </div>
64
+ </div>
65
+ </section>
@@ -0,0 +1,4 @@
1
+ import type { HeroSimpleProps } from "./types";
2
+ declare const SimpleHero: import("svelte").Component<HeroSimpleProps, {}, "">;
3
+ type SimpleHero = ReturnType<typeof SimpleHero>;
4
+ export default SimpleHero;
@@ -0,0 +1,18 @@
1
+ export interface HeroButtonProps {
2
+ label: string;
3
+ href: string;
4
+ variant?: "primary" | "outline" | "ghost";
5
+ className?: string;
6
+ onClick?: () => void;
7
+ }
8
+ export interface HeroSimpleProps {
9
+ title?: string;
10
+ subTitle?: string;
11
+ description?: string;
12
+ image?: string;
13
+ buttons?: HeroButtonProps[];
14
+ titleClass?: string;
15
+ subTitleClass?: string;
16
+ descriptionClass?: string;
17
+ imageClass?: string;
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,7 @@
1
1
  import type { IconSvgElement } from "@hugeicons/svelte";
2
2
  export interface NavMenuLogoProps {
3
3
  img?: string;
4
+ url?: string;
4
5
  label: string;
5
6
  className?: string;
6
7
  onclick?: () => void;
@@ -20,12 +21,15 @@ export interface NavMenuActionsProps {
20
21
  onclick?: () => void;
21
22
  }
22
23
  export interface NavMenuProps {
24
+ navClass?: string;
23
25
  linkClass?: string;
24
26
  buttonClass?: string;
25
27
  logo?: NavMenuLogoProps;
26
28
  links: NavMenuLinksProps[];
27
- actions: NavMenuActionsProps[];
29
+ actions?: NavMenuActionsProps[];
28
30
  align?: 'LINK_SEPARATED_ACTIONS' | 'LINK_INTO_ACTIONS';
31
+ isLightSwitch?: boolean;
32
+ isLanguageSwitcher?: boolean;
29
33
  onclickButtonLogin?: () => void;
30
34
  onclickButtonRegister?: () => void;
31
35
  }
@@ -1,98 +1,105 @@
1
1
  <script lang="ts">
2
- import { Button } from "../../../ui/button/index.js";
3
- import { HugeiconsIcon } from "@hugeicons/svelte";
4
- import type { NavMenuProps } from "../data/nav-menu";
5
- import LightSwitch from "../../../ui/light-switch/light-switch.svelte";
6
- import LanguageSwitcher from "../../../ui/language-switcher/language-switcher.svelte";
7
- import { t } from "../../../../i18n";
8
- import { Login03Icon, User03Icon } from "@hugeicons/core-free-icons";
9
- import { useIsMobile } from "../../../../hooks/responsive.svelte";
10
- import NavMenuDrawer from "./mobile/nav-menu-drawer.svelte";
11
- import MenuLinks from "./menu-links.svelte";
2
+ import { Button } from "../../../ui/button/index.js";
3
+ import { HugeiconsIcon } from "@hugeicons/svelte";
4
+ import type { NavMenuProps } from "../data/nav-menu";
5
+ import LightSwitch from "../../../ui/light-switch/light-switch.svelte";
6
+ import LanguageSwitcher from "../../../ui/language-switcher/language-switcher.svelte";
7
+ import { t } from "../../../../i18n";
8
+ import { Login03Icon, User03Icon } from "@hugeicons/core-free-icons";
9
+ import { useIsMobile } from "../../../../hooks/responsive.svelte";
10
+ import NavMenuDrawer from "./mobile/nav-menu-drawer.svelte";
11
+ import MenuLinks from "./menu-links.svelte";
12
12
 
13
- let {
14
- logo,
15
- links,
16
- actions,
17
- linkClass = "",
18
- buttonClass = "",
19
- align = "LINK_SEPARATED_ACTIONS",
20
- onclickButtonLogin,
21
- onclickButtonRegister,
22
- }: NavMenuProps = $props();
13
+ let {
14
+ logo,
15
+ links,
16
+ actions,
17
+ navClass = "",
18
+ linkClass = "",
19
+ buttonClass = "",
20
+ align = "LINK_SEPARATED_ACTIONS",
21
+ isLightSwitch = false,
22
+ isLanguageSwitcher = false,
23
+ onclickButtonLogin,
24
+ onclickButtonRegister,
25
+ }: NavMenuProps = $props();
23
26
 
24
- const responsive = useIsMobile();
27
+ const responsive = useIsMobile();
25
28
  </script>
26
29
 
27
30
  {#snippet actionButtons()}
28
- <div class="flex items-center gap-2">
29
- <LightSwitch />
30
- <LanguageSwitcher />
31
- {#each actions as item (item.label)}
32
- <Button
33
- variant={item.type === "LINK" ? "ghost" : "outline"}
34
- class={item.className}
35
- onclick={item.onclick}
36
- >
37
- {#if item.icon}
38
- <HugeiconsIcon icon={item.icon} />
39
- {/if}
40
- {item.label}
41
- </Button>
42
- {/each}
43
- {#if onclickButtonRegister}
44
- <Button
45
- variant="outline"
46
- onclick={onclickButtonRegister}
47
- class={buttonClass}
48
- >
49
- <HugeiconsIcon icon={User03Icon} />
50
- {$t("label.register")}
51
- </Button>
52
- {/if}
53
- {#if onclickButtonLogin}
54
- <Button
55
- variant="outline"
56
- onclick={onclickButtonLogin}
57
- class={buttonClass}
58
- >
59
- <HugeiconsIcon icon={Login03Icon} />
60
- {$t("label.login")}
61
- </Button>
31
+ <div class="flex items-center gap-2">
32
+ {#if isLightSwitch}
33
+ <LightSwitch />
34
+ {/if}
35
+ {#if isLanguageSwitcher}
36
+ <LanguageSwitcher />
37
+ {/if}
38
+ {#each actions as item (item.label)}
39
+ <Button
40
+ variant={item.type === "LINK" ? "ghost" : "outline"}
41
+ class={item.className}
42
+ onclick={item.onclick}
43
+ >
44
+ {#if item.icon}
45
+ <HugeiconsIcon icon={item.icon} />
62
46
  {/if}
63
- </div>
47
+ {item.label}
48
+ </Button>
49
+ {/each}
50
+ {#if onclickButtonRegister}
51
+ <Button
52
+ variant="outline"
53
+ onclick={onclickButtonRegister}
54
+ class={buttonClass}
55
+ >
56
+ <HugeiconsIcon icon={User03Icon} />
57
+ {$t("label.register")}
58
+ </Button>
59
+ {/if}
60
+ {#if onclickButtonLogin}
61
+ <Button
62
+ variant="outline"
63
+ onclick={onclickButtonLogin}
64
+ class={buttonClass}
65
+ >
66
+ <HugeiconsIcon icon={Login03Icon} />
67
+ {$t("label.login")}
68
+ </Button>
69
+ {/if}
70
+ </div>
64
71
  {/snippet}
65
72
 
66
- <nav class="flex items-center justify-between container py-5 md:px-5">
67
- {#if logo}
68
- <div>
69
- {#if logo.img}
70
- <img
71
- src={logo.img}
72
- alt="logo of site"
73
- class={`max-h-50 ${logo.className}`}
74
- />
75
- {:else if logo.label}
76
- <span class={logo.className}>{logo.label}</span>
77
- {/if}
78
- </div>
79
- {:else}
80
- <div>
81
- <span class="hidden">Logo</span>
82
- </div>
83
- {/if}
73
+ <nav
74
+ class={`flex items-center justify-between container py-5 md:px-5 ${navClass}`}
75
+ >
76
+ {#if logo}
77
+ <div>
78
+ {#if logo.img}
79
+ <img
80
+ src={logo.img}
81
+ alt="logo of site"
82
+ class={`max-h-50 ${logo.className}`}
83
+ />
84
+ {:else if logo.label}
85
+ <span class={logo.className}>{logo.label}</span>
86
+ {/if}
87
+ </div>
88
+ {:else}
89
+ <div>
90
+ <span class="hidden">Logo</span>
91
+ </div>
92
+ {/if}
84
93
 
85
- {#if responsive.isMobile}
86
- <NavMenuDrawer {links} {onclickButtonLogin} {onclickButtonRegister} />
87
- {:else}
88
- {#if align === "LINK_SEPARATED_ACTIONS"}
89
- <MenuLinks {links} {linkClass} orientation="horizontal" />
90
- {@render actionButtons()}
91
- {:else if align === "LINK_INTO_ACTIONS"}
92
- <div class="flex items-center gap-2">
93
- <MenuLinks {links} {linkClass} orientation="vertical" />
94
- {@render actionButtons()}
95
- </div>
96
- {/if}
97
- {/if}
94
+ {#if responsive.isMobile}
95
+ <NavMenuDrawer {links} {onclickButtonLogin} {onclickButtonRegister} />
96
+ {:else if align === "LINK_SEPARATED_ACTIONS"}
97
+ <MenuLinks {links} {linkClass} orientation="horizontal" />
98
+ {@render actionButtons()}
99
+ {:else if align === "LINK_INTO_ACTIONS"}
100
+ <div class="flex items-center gap-2">
101
+ <MenuLinks {links} {linkClass} orientation="vertical" />
102
+ {@render actionButtons()}
103
+ </div>
104
+ {/if}
98
105
  </nav>