negodesign 0.0.36 → 0.0.38

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 (39) hide show
  1. package/dist/components/core/card/shared/CardDescription.svelte +1 -1
  2. package/dist/components/core/carousel/grid/media/ui/CarouselGridMedia.svelte +38 -28
  3. package/dist/components/core/carousel/grid/product/ui/CarouselGridProduct.svelte +48 -38
  4. package/dist/components/core/carousel/grid/profile/ui/CarouselGridProfile.svelte +56 -46
  5. package/dist/components/core/carousel/grid/promotion/ui/CarouselGridPromotion.svelte +46 -28
  6. package/dist/components/core/carousel/highlights/ui/CarouselHighlights.svelte +57 -47
  7. package/dist/components/core/carousel/product/ui/CarouselProduct.svelte +57 -47
  8. package/dist/components/core/carousel/promotion/types.d.ts +5 -0
  9. package/dist/components/core/carousel/promotion/ui/CarouselPromotion.svelte +67 -47
  10. package/dist/components/core/panel/NotFoundEmpty.svelte +76 -0
  11. package/dist/components/core/panel/NotFoundEmpty.svelte.d.ts +16 -0
  12. package/dist/components/pages/company-profile/CompanyProfile.svelte +13 -9
  13. package/dist/components/pages/company-profile/data.js +47 -0
  14. package/dist/components/pages/company-profile/shared/ProfileBanner.svelte +136 -60
  15. package/dist/components/pages/company-profile/shared/ProfileProducts.svelte +62 -0
  16. package/dist/components/pages/company-profile/shared/ProfileProducts.svelte.d.ts +9 -0
  17. package/dist/components/pages/company-profile/shared/ProfileSidebar.svelte +1 -1
  18. package/dist/components/pages/company-profile/types.d.ts +25 -5
  19. package/dist/components/ui/empty/empty-content.svelte +23 -0
  20. package/dist/components/ui/empty/empty-content.svelte.d.ts +5 -0
  21. package/dist/components/ui/empty/empty-description.svelte +23 -0
  22. package/dist/components/ui/empty/empty-description.svelte.d.ts +5 -0
  23. package/dist/components/ui/empty/empty-header.svelte +20 -0
  24. package/dist/components/ui/empty/empty-header.svelte.d.ts +5 -0
  25. package/dist/components/ui/empty/empty-media.svelte +41 -0
  26. package/dist/components/ui/empty/empty-media.svelte.d.ts +26 -0
  27. package/dist/components/ui/empty/empty-title.svelte +20 -0
  28. package/dist/components/ui/empty/empty-title.svelte.d.ts +5 -0
  29. package/dist/components/ui/empty/empty.svelte +23 -0
  30. package/dist/components/ui/empty/empty.svelte.d.ts +5 -0
  31. package/dist/components/ui/empty/index.d.ts +7 -0
  32. package/dist/components/ui/empty/index.js +9 -0
  33. package/dist/globals.css +60 -0
  34. package/dist/i18n/langs/en.d.ts +15 -0
  35. package/dist/i18n/langs/en.js +15 -0
  36. package/dist/i18n/langs/pt.d.ts +15 -0
  37. package/dist/i18n/langs/pt.js +15 -0
  38. package/dist/i18n/translations.d.ts +30 -0
  39. package/package.json +1 -1
@@ -1,47 +1,67 @@
1
- <script lang="ts">
2
- import CardPromotion from "../../../card/promotion/CardPromotion.svelte";
3
- import CarouselSlot from "../../../panel/CarouselSlot.svelte";
4
- import * as Carousel from "../../../../ui/carousel/index.js";
5
- import type { CarouselPromotionProps } from "../types";
6
- import CarouselHeader from "../../shared/ui/CarouselHeader.svelte";
7
- import { useDevice } from "../../../../../hooks/responsive.svelte";
8
- import Autoplay from "embla-carousel-autoplay";
9
-
10
- const {
11
- headerProps,
12
- slotProps,
13
- items,
14
- isLoading = false,
15
- isDescriptionIcon,
16
- isDescriptionLabel,
17
- }: CarouselPromotionProps = $props();
18
-
19
- const responsive = useDevice();
20
- </script>
21
-
22
- <CarouselHeader {...headerProps}>
23
- <CarouselSlot
24
- {...slotProps}
25
- containerClass="w-full"
26
- plugins={[Autoplay({ delay: 4000, stopOnInteraction: true })]}
27
- >
28
- {#if isLoading}
29
- {#each Array.from( { length: responsive.isMobile ? 5 : 10 }, ) as _, i (`loading-${i}`)}
30
- <Carousel.Item class="pl-5 w-75 basis-auto">
31
- <CardPromotion
32
- id={i}
33
- {isLoading}
34
- {isDescriptionIcon}
35
- {isDescriptionLabel}
36
- />
37
- </Carousel.Item>
38
- {/each}
39
- {:else}
40
- {#each items as item, i (`promotion-${item.id ?? i}`)}
41
- <Carousel.Item class="pl-2 w-75 basis-auto">
42
- <CardPromotion {...item} {isDescriptionIcon} {isDescriptionLabel} />
43
- </Carousel.Item>
44
- {/each}
45
- {/if}
46
- </CarouselSlot>
47
- </CarouselHeader>
1
+ <script lang="ts">
2
+ import CardPromotion from "../../../card/promotion/CardPromotion.svelte";
3
+ import CarouselSlot from "../../../panel/CarouselSlot.svelte";
4
+ import NotFoundEmpty from "../../../panel/NotFoundEmpty.svelte";
5
+ import * as Carousel from "../../../../ui/carousel/index.js";
6
+ import type { CarouselPromotionProps } from "../types";
7
+ import CarouselHeader from "../../shared/ui/CarouselHeader.svelte";
8
+ import { useDevice } from "../../../../../hooks/responsive.svelte";
9
+ import { t } from "../../../../../i18n";
10
+ import Autoplay from "embla-carousel-autoplay";
11
+
12
+ const {
13
+ headerProps,
14
+ slotProps,
15
+ items,
16
+ isLoading = false,
17
+ isDescriptionIcon,
18
+ isDescriptionLabel,
19
+ onClickBuy,
20
+ onClickShop,
21
+ onClickFavorite,
22
+ }: CarouselPromotionProps = $props();
23
+
24
+ const responsive = useDevice();
25
+ const isEmpty = $derived(!isLoading && (!items || items.length === 0));
26
+ </script>
27
+
28
+ <CarouselHeader {...headerProps}>
29
+ {#if isEmpty}
30
+ <NotFoundEmpty
31
+ title={$t("empty.promotions.title")}
32
+ description={$t("empty.promotions.description")}
33
+ />
34
+ {:else}
35
+ <CarouselSlot
36
+ {...slotProps}
37
+ containerClass="w-full"
38
+ plugins={[Autoplay({ delay: 4000, stopOnInteraction: true })]}
39
+ >
40
+ {#if isLoading}
41
+ {#each Array.from( { length: responsive.isMobile ? 5 : 10 }, ) as _, i (`loading-${i}`)}
42
+ <Carousel.Item class="pl-5 w-75 basis-auto">
43
+ <CardPromotion
44
+ id={i}
45
+ {isLoading}
46
+ {isDescriptionIcon}
47
+ {isDescriptionLabel}
48
+ />
49
+ </Carousel.Item>
50
+ {/each}
51
+ {:else}
52
+ {#each items as item, i (`promotion-${item.id ?? i}`)}
53
+ <Carousel.Item class="pl-2 w-75 basis-auto">
54
+ <CardPromotion
55
+ {...item}
56
+ {isDescriptionIcon}
57
+ {isDescriptionLabel}
58
+ {onClickBuy}
59
+ {onClickShop}
60
+ {onClickFavorite}
61
+ />
62
+ </Carousel.Item>
63
+ {/each}
64
+ {/if}
65
+ </CarouselSlot>
66
+ {/if}
67
+ </CarouselHeader>
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ import * as Empty from "../../ui/empty/index.js";
3
+ import { t } from "../../../i18n";
4
+ import { HugeiconsIcon } from "@hugeicons/svelte";
5
+ import { Search01Icon } from "@hugeicons/core-free-icons";
6
+ import type { Snippet } from "svelte";
7
+ import type { IconSvgElement } from "@hugeicons/svelte";
8
+
9
+ type Props = {
10
+ title?: string;
11
+ description?: string;
12
+ icon?: IconSvgElement;
13
+ className?: string;
14
+ actionHref?: string;
15
+ actionLabel?: string;
16
+ onAction?: () => void;
17
+ showAction?: boolean;
18
+ children?: Snippet;
19
+ };
20
+
21
+ let {
22
+ title,
23
+ description,
24
+ icon = Search01Icon,
25
+ className = "",
26
+ actionHref = "#/",
27
+ actionLabel,
28
+ onAction,
29
+ showAction = false,
30
+ children,
31
+ }: Props = $props();
32
+
33
+ const resolvedTitle = $derived(title ?? $t("empty.title"));
34
+ const resolvedDescription = $derived(description ?? $t("empty.description"));
35
+ </script>
36
+
37
+ <Empty.Root class="w-full py-8 {className}">
38
+ <Empty.Header>
39
+ {#if icon}
40
+ <Empty.Media variant="icon">
41
+ <HugeiconsIcon {icon} class="size-6 text-muted-foreground" />
42
+ </Empty.Media>
43
+ {/if}
44
+ <Empty.Title class="text-base font-semibold">
45
+ {resolvedTitle}
46
+ </Empty.Title>
47
+ <Empty.Description class="text-sm text-muted-foreground max-w-sm">
48
+ {resolvedDescription}
49
+ </Empty.Description>
50
+ </Empty.Header>
51
+
52
+ {#if children}
53
+ <Empty.Content>
54
+ {@render children()}
55
+ </Empty.Content>
56
+ {:else if showAction || onAction}
57
+ <Empty.Content>
58
+ {#if onAction}
59
+ <button
60
+ type="button"
61
+ onclick={onAction}
62
+ class="text-xs font-medium text-primary hover:underline"
63
+ >
64
+ {actionLabel ?? $t("empty.contact_support")}
65
+ </button>
66
+ {:else}
67
+ <Empty.Description class="text-xs">
68
+ {$t("empty.support")}
69
+ <a href={actionHref} class="text-primary hover:underline font-medium ml-1">
70
+ {actionLabel ?? $t("empty.contact_support")}
71
+ </a>
72
+ </Empty.Description>
73
+ {/if}
74
+ </Empty.Content>
75
+ {/if}
76
+ </Empty.Root>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { IconSvgElement } from "@hugeicons/svelte";
3
+ type Props = {
4
+ title?: string;
5
+ description?: string;
6
+ icon?: IconSvgElement;
7
+ className?: string;
8
+ actionHref?: string;
9
+ actionLabel?: string;
10
+ onAction?: () => void;
11
+ showAction?: boolean;
12
+ children?: Snippet;
13
+ };
14
+ declare const NotFoundEmpty: import("svelte").Component<Props, {}, "">;
15
+ type NotFoundEmpty = ReturnType<typeof NotFoundEmpty>;
16
+ export default NotFoundEmpty;
@@ -27,16 +27,13 @@
27
27
  import ProfileMenuBadge from "./shared/ProfileMenuBadge.svelte";
28
28
  import ProfileAboutCompany from "./shared/ProfileAboutCompany.svelte";
29
29
  import ProfileGallery from "./shared/ProfileGallery.svelte";
30
+ import ProfileProducts from "./shared/ProfileProducts.svelte";
30
31
  import { defaultCompanyProfileData } from "./data";
31
32
  import type { CompanyProfileProps, ProfileNavItem } from "./types";
32
33
 
33
34
  let {
34
35
  data = defaultCompanyProfileData,
35
36
  isLoading = false,
36
- yearFounded,
37
- category,
38
- address,
39
- website,
40
37
  onEmail,
41
38
  onWhatsapp,
42
39
  onCopyLink,
@@ -47,8 +44,9 @@
47
44
 
48
45
  const sections: ProfileNavItem[] = [
49
46
  { id: "banner", label: "Apresentação" },
50
- { id: "sobre-empresa", label: "Sobre a empresa" },
51
- { id: "galeria-empresa", label: "Galeria da empresa" },
47
+ { id: "sobre-empresa", label: "Sobre" },
48
+ { id: "galeria-empresa", label: "Galeria" },
49
+ { id: "products-empresa", label: "Produtos" },
52
50
  ];
53
51
 
54
52
  let activeSection = $state(sections[0]?.id);
@@ -100,7 +98,7 @@
100
98
  />
101
99
  {/if}
102
100
 
103
- <div class="flex min-w-0 flex-1 flex-col gap-6 md:px-5 md:pl-10 lg:pl-12">
101
+ <div class="flex min-w-0 flex-1 flex-col gap-6 md:px-5 md:pl-10 lg:pl-12 pb-5 md:pb-10">
104
102
  <div
105
103
  id="banner"
106
104
  bind:this={sectionEls["banner"]}
@@ -126,8 +124,6 @@
126
124
  <ProfileAboutCompany data={data.about} {isLoading} />
127
125
  </div>
128
126
 
129
- <hr />
130
-
131
127
  <div
132
128
  id="galeria-empresa"
133
129
  bind:this={sectionEls["galeria-empresa"]}
@@ -135,6 +131,14 @@
135
131
  >
136
132
  <ProfileGallery data={data.gallery} {isLoading} />
137
133
  </div>
134
+
135
+ <div
136
+ id="products-empresa"
137
+ bind:this={sectionEls["products-empresa"]}
138
+ data-section-id="products-empresa"
139
+ >
140
+ <ProfileProducts data={data.products} {isLoading} />
141
+ </div>
138
142
  </div>
139
143
  </div>
140
144
  </div>
@@ -65,4 +65,51 @@ export const defaultCompanyProfileData = {
65
65
  },
66
66
  ],
67
67
  },
68
+ products: {
69
+ title: "Produtos e Soluções",
70
+ description: "Conheça as principais soluções de embalagens e produtos industriais fabricados sob medida.",
71
+ variant: 1,
72
+ items: [
73
+ {
74
+ id: "prod-1",
75
+ title: "Caixas de Papelão Canelado",
76
+ content: "Embalagens resistentes de alta durabilidade para transporte industrial e exportação.",
77
+ price: "12.500 Kz",
78
+ imageUrl: "https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?q=80&w=600&auto=format&fit=crop",
79
+ buttonBuyText: "Solicitar Cotação",
80
+ onClickBuy: (id) => alert(`Solicitar produto ${id}`),
81
+ tags: [{ text: "Industrial" }, { text: "Canelado" }],
82
+ },
83
+ {
84
+ id: "prod-2",
85
+ title: "Bobinas de Papel Kraft",
86
+ content: "Bobinas industriais de papel kraft para empacotamento e proteção de mercadorias.",
87
+ price: "35.000 Kz",
88
+ imageUrl: "https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?q=80&w=600&auto=format&fit=crop",
89
+ buttonBuyText: "Solicitar Cotação",
90
+ onClickBuy: (id) => alert(`Solicitar produto ${id}`),
91
+ tags: [{ text: "Kraft" }, { text: "Sustentável" }],
92
+ },
93
+ {
94
+ id: "prod-3",
95
+ title: "Caixas para Setor Alimentar",
96
+ content: "Embalagens certificadas para conservação e distribuição de alimentos frescos e congelados.",
97
+ price: "18.000 Kz",
98
+ imageUrl: "https://images.unsplash.com/photo-1504917599217-d4dc5ebe6122?q=80&w=600&auto=format&fit=crop",
99
+ buttonBuyText: "Solicitar Cotação",
100
+ onClickBuy: (id) => alert(`Solicitar produto ${id}`),
101
+ tags: [{ text: "Alimentar" }, { text: "Certificado" }],
102
+ },
103
+ {
104
+ id: "prod-4",
105
+ title: "Embalagens Personalizadas",
106
+ content: "Impressão flexográfica personalizada em caixas de todos os formatos e medidas.",
107
+ price: "22.000 Kz",
108
+ imageUrl: "https://images.unsplash.com/photo-1587293852726-70cdb56c2866?q=80&w=600&auto=format&fit=crop",
109
+ buttonBuyText: "Solicitar Cotação",
110
+ onClickBuy: (id) => alert(`Solicitar produto ${id}`),
111
+ tags: [{ text: "Personalizado" }, { text: "Design" }],
112
+ },
113
+ ],
114
+ },
68
115
  };
@@ -18,6 +18,7 @@
18
18
  import ImgPlaceholder from "../../../../assets/placeholder-image.png";
19
19
  import type { ProfileBannerData, ProfileBannerTag } from "../types";
20
20
  import { HugeiconsIcon } from "@hugeicons/svelte";
21
+ import { onDestroy } from "svelte";
21
22
  import {
22
23
  Calendar01FreeIcons,
23
24
  Copy,
@@ -29,6 +30,8 @@
29
30
  WhatsappIcon,
30
31
  } from "@hugeicons/core-free-icons";
31
32
 
33
+ const AUTOPLAY_MS = 8000;
34
+
32
35
  let {
33
36
  id,
34
37
  data,
@@ -50,6 +53,76 @@
50
53
  } = $props();
51
54
 
52
55
  const cover = $derived(data?.coverImages ?? []);
56
+ const isCover = $derived(cover.length > 0);
57
+ const hasMultipleCovers = $derived(cover.length > 1);
58
+
59
+ // ---- Carrossel de imagens de capa ----
60
+ let activeIndex = $state(0);
61
+ let intervalId: ReturnType<typeof setInterval> | undefined;
62
+
63
+ function stopAutoplay() {
64
+ if (intervalId) clearInterval(intervalId);
65
+ }
66
+
67
+ function startAutoplay() {
68
+ stopAutoplay();
69
+ if (hasMultipleCovers) {
70
+ intervalId = setInterval(() => {
71
+ activeIndex = (activeIndex + 1) % cover.length;
72
+ }, AUTOPLAY_MS);
73
+ }
74
+ }
75
+
76
+ function goTo(index: number) {
77
+ activeIndex = index;
78
+ startAutoplay(); // reinicia o timer ao clicar manualmente
79
+ }
80
+
81
+ // Reinicia apenas quando o conjunto de imagens muda (não a cada render)
82
+ $effect(() => {
83
+ void cover.length; // dependência explícita
84
+ activeIndex = 0;
85
+ startAutoplay();
86
+ return stopAutoplay;
87
+ });
88
+
89
+ onDestroy(stopAutoplay);
90
+
91
+ // Ações do header (topo direito) — evita repetir markup de botão
92
+ const headerActions = $derived([
93
+ { icon: Copy, label: "Copiar link", onclick: () => onCopyLink?.(id) },
94
+ { icon: Share, label: "Partilhar", onclick: () => onShare?.(id) },
95
+ ]);
96
+
97
+ const contactActions = $derived([
98
+ {
99
+ icon: Email,
100
+ label: "Email",
101
+ iconClass: "text-red-500",
102
+ onclick: () => onEmail?.(id),
103
+ },
104
+ {
105
+ icon: WhatsappIcon,
106
+ label: "Whatsapp",
107
+ iconClass: "text-green-500",
108
+ onclick: () => onWhatsapp?.(id),
109
+ },
110
+ ]);
111
+
112
+ const infoItems = $derived([
113
+ {
114
+ title: "Categoria",
115
+ description: data.category,
116
+ icon: DashboardBrowsingIcon,
117
+ },
118
+ { title: "Endereço", description: data.address, icon: MapPin },
119
+ {
120
+ title: "Anos de Atuação",
121
+ description: data.yearFounded,
122
+ icon: Calendar01FreeIcons,
123
+ },
124
+ { title: "Website", description: data.website, icon: Link01FreeIcons },
125
+ ]);
53
126
  </script>
54
127
 
55
128
  {#if isLoading}
@@ -68,33 +141,58 @@
68
141
  </div>
69
142
  {:else}
70
143
  <div
71
- class="flex flex-col items-center justify-center bg-gradient p-2 relative rounded-lg"
144
+ class="flex flex-col items-center justify-center p-2 relative rounded-lg overflow-hidden
145
+ {isCover ? '' : 'bg-gradient'}"
72
146
  >
73
- <!-- Capa -->
147
+ {#if isCover}
148
+ <div class="absolute inset-0 -z-10">
149
+ {#each cover as image, i (image.src)}
150
+ <img
151
+ src={image.src}
152
+ alt={image.alt ?? ""}
153
+ class="absolute inset-0 h-full w-full object-cover transition-opacity duration-700 ease-in-out {i ===
154
+ activeIndex
155
+ ? 'opacity-100'
156
+ : 'opacity-0 pointer-events-none'}"
157
+ />
158
+ {/each}
159
+ <div class="absolute inset-0 bg-black/30"></div>
160
+
161
+ {#if hasMultipleCovers}
162
+ <div
163
+ class="absolute bottom-3 left-1/2 z-10 flex -translate-x-1/2 gap-1.5"
164
+ >
165
+ {#each cover as _, i (i)}
166
+ <button
167
+ type="button"
168
+ aria-label={`Ir para imagem ${i + 1}`}
169
+ onclick={() => goTo(i)}
170
+ class="h-1.5 rounded-full transition-all {i === activeIndex
171
+ ? 'w-4 bg-white'
172
+ : 'w-1.5 bg-white/50'}"
173
+ ></button>
174
+ {/each}
175
+ </div>
176
+ {/if}
177
+ </div>
178
+ {/if}
179
+
74
180
  <div class="overflow-hidden rounded-xl absolute top-0 right-0">
75
181
  <div class="flex gap-2 md:gap-5 p-4">
76
- <Button
77
- type="button"
78
- variant="ghost"
79
- class="rounded-full bg-white dark:bg-background"
80
- aria-label="Copiar link"
81
- onclick={() => onCopyLink?.(id)}
82
- >
83
- <HugeiconsIcon icon={Copy} />
84
- </Button>
85
- <Button
86
- type="button"
87
- variant="ghost"
88
- class="rounded-full bg-white dark:bg-background"
89
- aria-label="Partilhar"
90
- onclick={() => onShare?.(id)}
91
- >
92
- <HugeiconsIcon icon={Share} />
93
- </Button>
182
+ {#each headerActions as action (action.label)}
183
+ <Button
184
+ type="button"
185
+ variant="ghost"
186
+ class="rounded-full bg-white dark:bg-background"
187
+ aria-label={action.label}
188
+ onclick={action.onclick}
189
+ >
190
+ <HugeiconsIcon icon={action.icon} />
191
+ </Button>
192
+ {/each}
94
193
  </div>
95
194
  </div>
96
195
 
97
- <!-- Logótipo + nome + contacto -->
98
196
  <div
99
197
  class="flex flex-col items-center gap-3 px-4 text-center justify-center md:-mb-10"
100
198
  >
@@ -103,7 +201,7 @@
103
201
  <img
104
202
  src={data.logo}
105
203
  alt={data.name}
106
- class="rounded-full w-15 md:w-25 h-15 md:h-25 p-2"
204
+ class="rounded-full w-15 md:w-25 h-15 md:h-25 lg:w-40 lg:h-40 p-2"
107
205
  onerror={(e) =>
108
206
  ((e.target as HTMLImageElement).src = ImgPlaceholder)}
109
207
  />
@@ -117,27 +215,22 @@
117
215
  {data.name}
118
216
  </h1>
119
217
  </div>
218
+
120
219
  <div class="flex justify-between w-full p-5">
121
- <Button
122
- variant="ghost"
123
- size="sm"
124
- onclick={() => onEmail?.(id)}
125
- class="bg-white cursor-pointer"
126
- >
127
- <HugeiconsIcon icon={Email} class="text-red-500" />
128
- Email
129
- </Button>
130
- <Button
131
- variant="ghost"
132
- size="sm"
133
- onclick={() => onWhatsapp?.(id)}
134
- class="bg-white cursor-pointer"
135
- >
136
- <HugeiconsIcon icon={WhatsappIcon} class="text-green-500" />
137
- Whatsapp
138
- </Button>
220
+ {#each contactActions as action (action.label)}
221
+ <Button
222
+ variant="ghost"
223
+ size="sm"
224
+ onclick={action.onclick}
225
+ class="bg-white cursor-pointer"
226
+ >
227
+ <HugeiconsIcon icon={action.icon} class={action.iconClass} />
228
+ {action.label}
229
+ </Button>
230
+ {/each}
139
231
  </div>
140
232
  </div>
233
+
141
234
  <div
142
235
  class="bg-gray-50 dark:bg-background p-2 md:p-10 flex flex-col items-center justify-center"
143
236
  >
@@ -151,26 +244,9 @@
151
244
  *:w-full
152
245
  *:flex *:flex-col *:items-center"
153
246
  >
154
- {@render companyInfo({
155
- title: "Categoria",
156
- description: data.category,
157
- icon: DashboardBrowsingIcon,
158
- })}
159
- {@render companyInfo({
160
- title: "Endereço",
161
- description: data.address,
162
- icon: MapPin,
163
- })}
164
- {@render companyInfo({
165
- title: "Anos de Atuação",
166
- description: data.yearFounded,
167
- icon: Calendar01FreeIcons,
168
- })}
169
- {@render companyInfo({
170
- title: "Website",
171
- description: data.website,
172
- icon: Link01FreeIcons,
173
- })}
247
+ {#each infoItems as item (item.title)}
248
+ {@render companyInfo(item)}
249
+ {/each}
174
250
  </div>
175
251
  </div>
176
252
  {/if}
@@ -0,0 +1,62 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * Secção "Produtos": exibe a lista de produtos/serviços da empresa
4
+ * utilizando o componente CarouselGridProduct.
5
+ * Quando `isLoading` é true, exibe Skeletons.
6
+ * @component
7
+ * @example
8
+ * ```svelte
9
+ * <ProfileProducts data={data.products} />
10
+ * ```
11
+ */
12
+ </script>
13
+
14
+ <script lang="ts">
15
+ import CarouselGridProduct from "../../../core/carousel/grid/product/ui/CarouselGridProduct.svelte";
16
+ import type { ProfileProductsData } from "../types";
17
+ import { HugeiconsIcon } from "@hugeicons/svelte";
18
+ import { PackageIcon } from "@hugeicons/core-free-icons";
19
+ import NotFoundEmpty from "../../../core/panel/NotFoundEmpty.svelte";
20
+
21
+ let {
22
+ data,
23
+ isLoading = false,
24
+ className = "",
25
+ }: {
26
+ data?: ProfileProductsData;
27
+ isLoading?: boolean;
28
+ className?: string;
29
+ } = $props();
30
+
31
+ const items = $derived(data?.items ?? []);
32
+ const loading = $derived(isLoading || Boolean(data?.isLoading));
33
+ const variant = $derived(data?.variant ?? data?.varient ?? 1);
34
+ </script>
35
+
36
+ <div class="bg-gray-50 dark:bg-background p-4 md:p-6 rounded-lg {className}">
37
+ <h2 class="mb-4 flex items-center gap-2 text-lg font-bold">
38
+ <HugeiconsIcon icon={PackageIcon} class="size-5" />
39
+ {data?.title ?? "Produtos"}
40
+ </h2>
41
+
42
+ {#if !loading && items.length === 0}
43
+ <NotFoundEmpty />
44
+ {:else}
45
+ <CarouselGridProduct
46
+ {items}
47
+ {variant}
48
+ isLoading={loading}
49
+ headerProps={{
50
+ title: "",
51
+ description: data?.description,
52
+ containerClass: "p-0 bg-transparent",
53
+ }}
54
+ slotProps={{
55
+ buttonPreviousAndNextClass:
56
+ "bg-white dark:bg-gray-800 shadow-md hover:bg-gray-100 dark:hover:bg-gray-700",
57
+ positionButtonPreviousAndNext: "center",
58
+ }}
59
+ gridClass="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
60
+ />
61
+ {/if}
62
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { ProfileProductsData } from "../types";
2
+ type $$ComponentProps = {
3
+ data?: ProfileProductsData;
4
+ isLoading?: boolean;
5
+ className?: string;
6
+ };
7
+ declare const ProfileProducts: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type ProfileProducts = ReturnType<typeof ProfileProducts>;
9
+ export default ProfileProducts;
@@ -32,7 +32,7 @@
32
32
 
33
33
  <nav aria-label="Navegação do perfil" class="w-40 shrink-0 {className ?? ''}">
34
34
  <div
35
- class="fixed flex flex-col gap-1 md:gap-2 bg-card p-6 md:p-8 h-screen border-r"
35
+ class="fixed flex flex-col gap-1 md:pt-10 lg:pt-20 md:gap-2 bg-card p-6 md:p-8 h-screen border-r"
36
36
  >
37
37
  {#if isLoading}
38
38
  {#each Array.from({ length: 4 }) as _, i (i)}