negodesign 0.0.35 → 0.0.37

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 (95) hide show
  1. package/dist/components/core/card/product/01/CardProduct01.svelte +130 -0
  2. package/dist/components/core/card/product/01/CardProduct01.svelte.d.ts +4 -0
  3. package/dist/components/core/card/product/02/CardProduct02.svelte +118 -0
  4. package/dist/components/core/card/product/02/CardProduct02.svelte.d.ts +4 -0
  5. package/dist/components/core/card/product/CardProduct.svelte +22 -0
  6. package/dist/components/core/card/product/CardProduct.svelte.d.ts +11 -0
  7. package/dist/components/core/card/product/data.d.ts +3 -0
  8. package/dist/components/core/card/product/data.js +89 -0
  9. package/dist/components/core/card/profile/01/CardProfile01.svelte +0 -1
  10. package/dist/components/core/card/promotion/CardPromotion.svelte +1 -1
  11. package/dist/components/core/card/promotion/CardPromotionPrice.svelte.d.ts +8 -0
  12. package/dist/components/core/card/shared/CardPhoneOrWhatsapp.svelte +12 -5
  13. package/dist/components/core/card/shared/CardTags.svelte +49 -24
  14. package/dist/components/core/card/shared/CardTitlePrice.svelte +24 -0
  15. package/dist/components/core/card/shared/CardTitlePrice.svelte.d.ts +15 -0
  16. package/dist/components/core/card/shared/TruncatableText.svelte +1 -1
  17. package/dist/components/core/card/types.d.ts +48 -0
  18. package/dist/components/core/carousel/grid/media/ui/CarouselGridMedia.svelte +38 -28
  19. package/dist/components/core/carousel/grid/product/types.d.ts +22 -0
  20. package/dist/components/core/carousel/grid/product/types.js +1 -0
  21. package/dist/components/core/carousel/grid/product/ui/CarouselGridProduct.svelte +71 -0
  22. package/dist/components/core/carousel/grid/product/ui/CarouselGridProduct.svelte.d.ts +4 -0
  23. package/dist/components/core/carousel/grid/profile/ui/CarouselGridProfile.svelte +56 -46
  24. package/dist/components/core/carousel/grid/promotion/types.d.ts +23 -0
  25. package/dist/components/core/carousel/grid/promotion/types.js +1 -0
  26. package/dist/components/core/carousel/grid/promotion/ui/CarouselGridPromotion.svelte +68 -0
  27. package/dist/components/core/carousel/grid/promotion/ui/CarouselGridPromotion.svelte.d.ts +4 -0
  28. package/dist/components/core/carousel/highlights/ui/CarouselHighlights.svelte +57 -47
  29. package/dist/components/core/carousel/product/types.d.ts +21 -0
  30. package/dist/components/core/carousel/product/types.js +1 -0
  31. package/dist/components/core/carousel/product/ui/CarouselProduct.svelte +57 -0
  32. package/dist/components/core/carousel/product/ui/CarouselProduct.svelte.d.ts +4 -0
  33. package/dist/components/core/carousel/promotion/ui/CarouselPromotion.svelte +57 -47
  34. package/dist/components/core/panel/NotFoundEmpty.svelte +76 -0
  35. package/dist/components/core/panel/NotFoundEmpty.svelte.d.ts +16 -0
  36. package/dist/components/pages/company-profile/CompanyProfile.svelte +144 -0
  37. package/dist/components/pages/company-profile/CompanyProfile.svelte.d.ts +4 -0
  38. package/dist/components/pages/company-profile/data.d.ts +2 -0
  39. package/dist/components/pages/company-profile/data.js +115 -0
  40. package/dist/components/pages/company-profile/shared/ProfileAboutCompany.svelte +79 -0
  41. package/dist/components/pages/company-profile/shared/ProfileAboutCompany.svelte.d.ts +9 -0
  42. package/dist/components/pages/company-profile/shared/ProfileBanner.svelte +270 -0
  43. package/dist/components/pages/company-profile/shared/ProfileBanner.svelte.d.ts +14 -0
  44. package/dist/components/pages/company-profile/shared/ProfileGallery.svelte +198 -0
  45. package/dist/components/pages/company-profile/shared/ProfileGallery.svelte.d.ts +9 -0
  46. package/dist/components/pages/company-profile/shared/ProfileMenuBadge.svelte +56 -0
  47. package/dist/components/pages/company-profile/shared/ProfileMenuBadge.svelte.d.ts +11 -0
  48. package/dist/components/pages/company-profile/shared/ProfileProducts.svelte +62 -0
  49. package/dist/components/pages/company-profile/shared/ProfileProducts.svelte.d.ts +9 -0
  50. package/dist/components/pages/company-profile/shared/ProfileSidebar.svelte +57 -0
  51. package/dist/components/pages/company-profile/shared/ProfileSidebar.svelte.d.ts +11 -0
  52. package/dist/components/pages/company-profile/types.d.ts +137 -0
  53. package/dist/components/pages/company-profile/types.js +1 -0
  54. package/dist/components/pages/privacy-policy-or-terms-of-use/data.d.ts +3 -0
  55. package/dist/components/pages/privacy-policy-or-terms-of-use/data.js +179 -0
  56. package/dist/components/pages/privacy-policy-or-terms-of-use/shared/DocSectionView.svelte +2 -1
  57. package/dist/components/pages/privacy-policy-or-terms-of-use/shared/DocSectionView.svelte.d.ts +1 -0
  58. package/dist/components/pages/product-details/01/ProductDetails01.svelte +19 -7
  59. package/dist/components/pages/product-details/02/ProductDetails02.svelte +42 -37
  60. package/dist/components/pages/product-details/03/ProductDetails03.svelte +33 -29
  61. package/dist/components/pages/product-details/ProductDetails.svelte +11 -7
  62. package/dist/components/pages/product-details/shared/ProductDetailFeatures.svelte +11 -5
  63. package/dist/components/pages/product-details/shared/ProductDetailFeatures.svelte.d.ts +2 -1
  64. package/dist/components/pages/product-details/shared/ProductDetailFilesVertical.svelte +52 -20
  65. package/dist/components/pages/product-details/shared/ProductDetailFilesVertical.svelte.d.ts +1 -1
  66. package/dist/components/pages/product-details/shared/ProductDetailImage.svelte +51 -20
  67. package/dist/components/pages/product-details/shared/ProductDetailImage.svelte.d.ts +1 -1
  68. package/dist/components/pages/product-details/shared/ProductDetailPrice.svelte +1 -1
  69. package/dist/components/pages/product-details/types.d.ts +9 -2
  70. package/dist/components/pages/profile-user/types.d.ts +7 -0
  71. package/dist/components/ui/empty/empty-content.svelte +23 -0
  72. package/dist/components/ui/empty/empty-content.svelte.d.ts +5 -0
  73. package/dist/components/ui/empty/empty-description.svelte +23 -0
  74. package/dist/components/ui/empty/empty-description.svelte.d.ts +5 -0
  75. package/dist/components/ui/empty/empty-header.svelte +20 -0
  76. package/dist/components/ui/empty/empty-header.svelte.d.ts +5 -0
  77. package/dist/components/ui/empty/empty-media.svelte +41 -0
  78. package/dist/components/ui/empty/empty-media.svelte.d.ts +26 -0
  79. package/dist/components/ui/empty/empty-title.svelte +20 -0
  80. package/dist/components/ui/empty/empty-title.svelte.d.ts +5 -0
  81. package/dist/components/ui/empty/empty.svelte +23 -0
  82. package/dist/components/ui/empty/empty.svelte.d.ts +5 -0
  83. package/dist/components/ui/empty/index.d.ts +7 -0
  84. package/dist/components/ui/empty/index.js +9 -0
  85. package/dist/globals.css +301 -5
  86. package/dist/i18n/langs/en.d.ts +25 -0
  87. package/dist/i18n/langs/en.js +25 -0
  88. package/dist/i18n/langs/pt.d.ts +25 -0
  89. package/dist/i18n/langs/pt.js +25 -0
  90. package/dist/i18n/translations.d.ts +50 -0
  91. package/package.json +1 -1
  92. package/dist/components/core/card/shared/CardStartPrice.svelte.d.ts +0 -8
  93. package/dist/components/pages/product-details/04/ProductDetails04.svelte +0 -65
  94. package/dist/components/pages/product-details/04/ProductDetails04.svelte.d.ts +0 -11
  95. /package/dist/components/core/card/{shared/CardStartPrice.svelte → promotion/CardPromotionPrice.svelte} +0 -0
@@ -0,0 +1,270 @@
1
+ <script lang="ts" module>
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ /**
4
+ * Banner de apresentação da empresa/fábrica: imagem de capa, logótipo,
5
+ * nome, país, botão de contacto e faixa de estatísticas rápidas.
6
+ * Quando `isLoading` é true, exibe Skeletons.
7
+ * @component
8
+ * @example
9
+ * ```svelte
10
+ * <ProfileBanner data={data.banner} id={data.id} {onContact} {onCopyLink} {onShare} />
11
+ * ```
12
+ */
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ import Skeleton from "../../../ui/skeleton/skeleton.svelte";
17
+ import { Button } from "../../../ui/button/index.js";
18
+ import ImgPlaceholder from "../../../../assets/placeholder-image.png";
19
+ import type { ProfileBannerData, ProfileBannerTag } from "../types";
20
+ import { HugeiconsIcon } from "@hugeicons/svelte";
21
+ import { onDestroy } from "svelte";
22
+ import {
23
+ Calendar01FreeIcons,
24
+ Copy,
25
+ DashboardBrowsingIcon,
26
+ Email,
27
+ Link01FreeIcons,
28
+ MapPin,
29
+ Share,
30
+ WhatsappIcon,
31
+ } from "@hugeicons/core-free-icons";
32
+
33
+ const AUTOPLAY_MS = 8000;
34
+
35
+ let {
36
+ id,
37
+ data,
38
+ isLoading = false,
39
+ onWhatsapp,
40
+ onEmail,
41
+ onCopyLink,
42
+ onShare,
43
+ tags,
44
+ }: {
45
+ id: string | number;
46
+ data: ProfileBannerData;
47
+ isLoading?: boolean;
48
+ onWhatsapp?: (id: string | number) => void;
49
+ onEmail?: (id: string | number) => void;
50
+ onCopyLink?: (id: string | number) => void;
51
+ onShare?: (id: string | number) => void;
52
+ tags?: ProfileBannerTag[];
53
+ } = $props();
54
+
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
+ ]);
126
+ </script>
127
+
128
+ {#if isLoading}
129
+ <div class="flex flex-col gap-4">
130
+ <Skeleton class="aspect-21/9 w-full rounded-xl" />
131
+ <div class="flex flex-col items-center gap-3">
132
+ <Skeleton class="size-24 rounded-2xl -mt-16 border-4 border-background" />
133
+ <Skeleton class="h-7 w-48 rounded-md" />
134
+ <Skeleton class="h-10 w-44 rounded-md" />
135
+ </div>
136
+ <div class="flex justify-center gap-6">
137
+ {#each Array.from({ length: 4 }) as _, i (i)}
138
+ <Skeleton class="h-12 w-24 rounded-md" />
139
+ {/each}
140
+ </div>
141
+ </div>
142
+ {:else}
143
+ <div
144
+ class="flex flex-col items-center justify-center p-2 relative rounded-lg overflow-hidden
145
+ {isCover ? '' : 'bg-gradient'}"
146
+ >
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
+
180
+ <div class="overflow-hidden rounded-xl absolute top-0 right-0">
181
+ <div class="flex gap-2 md:gap-5 p-4">
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}
193
+ </div>
194
+ </div>
195
+
196
+ <div
197
+ class="flex flex-col items-center gap-3 px-4 text-center justify-center md:-mb-10"
198
+ >
199
+ <div class="flex shrink-0 items-center justify-center">
200
+ {#if data.logo}
201
+ <img
202
+ src={data.logo}
203
+ alt={data.name}
204
+ class="rounded-full w-15 md:w-25 h-15 md:h-25 lg:w-40 lg:h-40 p-2"
205
+ onerror={(e) =>
206
+ ((e.target as HTMLImageElement).src = ImgPlaceholder)}
207
+ />
208
+ {:else}
209
+ <span class="text-2xl font-bold text-muted-foreground"
210
+ >{data.name?.[0] ?? "?"}</span
211
+ >
212
+ {/if}
213
+ </div>
214
+ <h1 class="text-lg font-bold tracking-tight md:text-xl mb-5 text-white">
215
+ {data.name}
216
+ </h1>
217
+ </div>
218
+
219
+ <div class="flex justify-between w-full p-5">
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}
231
+ </div>
232
+ </div>
233
+
234
+ <div
235
+ class="bg-gray-50 dark:bg-background p-2 md:p-10 flex flex-col items-center justify-center"
236
+ >
237
+ <div
238
+ class="grid grid-cols-2 items-center w-full
239
+ [&>*:nth-child(odd)]:border-r
240
+ [&>*:nth-child(-n+2)]:pb-1 [&>*:nth-child(-n+2)]:md:pb-2.5 [&>*:nth-child(-n+2)]:lg:pb-5
241
+ [&>*:nth-child(-n+2)]:border-b
242
+ [&>*:nth-child(n+3)]:pt-1 [&>*:nth-child(n+3)]:md:pt-2.5 [&>*:nth-child(n+3)]:lg:pt-5
243
+ *:border-border
244
+ *:w-full
245
+ *:flex *:flex-col *:items-center"
246
+ >
247
+ {#each infoItems as item (item.title)}
248
+ {@render companyInfo(item)}
249
+ {/each}
250
+ </div>
251
+ </div>
252
+ {/if}
253
+
254
+ {#snippet companyInfo({
255
+ title,
256
+ description,
257
+ icon,
258
+ }: {
259
+ title: string;
260
+ description?: string;
261
+ icon: any;
262
+ })}
263
+ <div class="flex flex-col gap-2 items-center">
264
+ <h3 class="text-md font-semibold flex gap-2 items-center">
265
+ <HugeiconsIcon {icon} size={15} />
266
+ {title}
267
+ </h3>
268
+ <p class="text-md text-gray-400">{description ?? "---"}</p>
269
+ </div>
270
+ {/snippet}
@@ -0,0 +1,14 @@
1
+ import type { ProfileBannerData, ProfileBannerTag } from "../types";
2
+ type $$ComponentProps = {
3
+ id: string | number;
4
+ data: ProfileBannerData;
5
+ isLoading?: boolean;
6
+ onWhatsapp?: (id: string | number) => void;
7
+ onEmail?: (id: string | number) => void;
8
+ onCopyLink?: (id: string | number) => void;
9
+ onShare?: (id: string | number) => void;
10
+ tags?: ProfileBannerTag[];
11
+ };
12
+ declare const ProfileBanner: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type ProfileBanner = ReturnType<typeof ProfileBanner>;
14
+ export default ProfileBanner;
@@ -0,0 +1,198 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * Secção "Galeria": grelha de imagens/vídeos da empresa com lightbox simples.
4
+ * O vídeo (se existir) ocupa sempre o card em destaque.
5
+ * Quando `isLoading` é true, exibe Skeletons.
6
+ * @component
7
+ * @example
8
+ * ```svelte
9
+ * <ProfileGallery data={data.gallery} />
10
+ * ```
11
+ */
12
+ </script>
13
+
14
+ <script lang="ts">
15
+ import Skeleton from "../../../ui/skeleton/skeleton.svelte";
16
+ import type { ProfileGalleryData, ProfileImage } from "../types";
17
+ import ImgPlaceholder from "../../../../assets/placeholder-image.png";
18
+ import { HugeiconsIcon } from "@hugeicons/svelte";
19
+ import { GalleryThumbnailsFreeIcons } from "@hugeicons/core-free-icons";
20
+
21
+ let {
22
+ data,
23
+ isLoading = false,
24
+ className,
25
+ }: {
26
+ data: ProfileGalleryData;
27
+ isLoading?: boolean;
28
+ className?: string;
29
+ } = $props();
30
+
31
+ // Vídeo primeiro (ocupa o card em destaque), resto mantém a ordem original.
32
+ const images = $derived(
33
+ [...(data?.images ?? [])].sort((a, b) =>
34
+ a.type === "video" ? -1 : b.type === "video" ? 1 : 0,
35
+ ),
36
+ );
37
+ const visible = $derived(images.slice(0, 5));
38
+
39
+ let index = $state<number | null>(null);
40
+ const active = $derived(index !== null ? images[index] : undefined);
41
+
42
+ const open = (i: number) => (index = i);
43
+ const close = () => (index = null);
44
+ const step = (dir: 1 | -1) => {
45
+ if (index === null || !images.length) return;
46
+ index = (index + dir + images.length) % images.length;
47
+ };
48
+ const onKeydown = (e: KeyboardEvent) => {
49
+ if (index === null) return;
50
+ if (e.key === "Escape") close();
51
+ else if (e.key === "ArrowRight") step(1);
52
+ else if (e.key === "ArrowLeft") step(-1);
53
+ };
54
+ </script>
55
+
56
+ <svelte:window onkeydown={onKeydown} />
57
+
58
+ {#snippet media(item: ProfileImage, cls: string)}
59
+ {#if item.type === "video"}
60
+ <video src={item.src} class={cls} muted playsinline preload="metadata"
61
+ ></video>
62
+ <span
63
+ class="absolute inset-0 flex items-center justify-center bg-black/20 transition-colors group-hover:bg-black/30"
64
+ >
65
+ <span
66
+ class="flex size-11 items-center justify-center rounded-full bg-black/50 text-white"
67
+ >
68
+ <svg viewBox="0 0 24 24" class="size-5 fill-white" aria-hidden="true">
69
+ <path d="M8 5v14l11-7z" />
70
+ </svg>
71
+ </span>
72
+ </span>
73
+ {:else}
74
+ <img
75
+ src={item.src}
76
+ alt={item.alt ?? ""}
77
+ class={cls}
78
+ loading="lazy"
79
+ decoding="async"
80
+ onerror={(e) => ((e.target as HTMLImageElement).src = ImgPlaceholder)}
81
+ />
82
+ {/if}
83
+ {/snippet}
84
+
85
+ {#snippet arrow(dir: 1 | -1, side: string)}
86
+ <button
87
+ type="button"
88
+ aria-label={dir === -1 ? "Imagem anterior" : "Próxima imagem"}
89
+ onclick={() => step(dir)}
90
+ class="absolute {side} flex size-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20"
91
+ >
92
+ <svg
93
+ viewBox="0 0 24 24"
94
+ class="size-5"
95
+ fill="none"
96
+ stroke="currentColor"
97
+ stroke-width="2"
98
+ stroke-linecap="round"
99
+ stroke-linejoin="round"
100
+ aria-hidden="true"
101
+ >
102
+ <path d={dir === -1 ? "m15 18-6-6 6-6" : "m9 18 6-6-6-6"} />
103
+ </svg>
104
+ </button>
105
+ {/snippet}
106
+
107
+ <div class="bg-gray-50 dark:bg-background p-4 md:p-6 {className}">
108
+ <h2 class="mb-4 flex items-center gap-2 text-lg font-bold">
109
+ <HugeiconsIcon icon={GalleryThumbnailsFreeIcons} />
110
+ {data.title ?? "Galeria"}
111
+ </h2>
112
+
113
+ {#if isLoading}
114
+ <div class="grid grid-cols-2 gap-3 md:grid-cols-3">
115
+ <Skeleton
116
+ class="col-span-2 row-span-2 aspect-square rounded-lg md:aspect-auto"
117
+ />
118
+ {#each Array.from({ length: 4 }) as _, i (i)}
119
+ <Skeleton class="aspect-square rounded-lg" />
120
+ {/each}
121
+ </div>
122
+ {:else if visible.length}
123
+ <div class="grid grid-cols-2 gap-3 md:grid-cols-3 md:auto-rows-36">
124
+ {#each visible as item, i (item.src)}
125
+ <button
126
+ type="button"
127
+ onclick={() => open(i)}
128
+ class="group relative overflow-hidden rounded-lg border {i === 0
129
+ ? 'col-span-2 row-span-2'
130
+ : ''}"
131
+ aria-label={item.alt ??
132
+ (item.type === "video"
133
+ ? "Reproduzir vídeo"
134
+ : `Ver imagem ${i + 1}`)}
135
+ >
136
+ {@render media(
137
+ item,
138
+ "size-full object-cover transition-transform duration-300 group-hover:scale-105",
139
+ )}
140
+ </button>
141
+ {/each}
142
+ </div>
143
+ {:else}
144
+ <p class="text-sm text-muted-foreground">
145
+ Galeria ainda não foi submetida.
146
+ </p>
147
+ {/if}
148
+ </div>
149
+
150
+ {#if active}
151
+ <div
152
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
153
+ role="dialog"
154
+ aria-modal="true"
155
+ >
156
+ <button
157
+ type="button"
158
+ aria-label="Fechar"
159
+ onclick={close}
160
+ class="absolute right-4 top-4 flex size-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20"
161
+ >
162
+ <svg
163
+ viewBox="0 0 24 24"
164
+ class="size-5"
165
+ fill="none"
166
+ stroke="currentColor"
167
+ stroke-width="2"
168
+ stroke-linecap="round"
169
+ aria-hidden="true"
170
+ >
171
+ <path d="M18 6 6 18M6 6l12 12" />
172
+ </svg>
173
+ </button>
174
+
175
+ {#if images.length > 1}
176
+ {@render arrow(-1, "left-2 md:left-6")}
177
+ {@render arrow(1, "right-2 md:right-6")}
178
+ {/if}
179
+
180
+ {#if active.type === "video"}
181
+ <!-- svelte-ignore a11y_media_has_caption -->
182
+ <video
183
+ src={active.src}
184
+ class="max-h-[85vh] max-w-full rounded-lg object-contain"
185
+ controls
186
+ autoplay
187
+ playsinline
188
+ ></video>
189
+ {:else}
190
+ <img
191
+ src={active.src}
192
+ alt={active.alt ?? ""}
193
+ class="max-h-[85vh] max-w-full rounded-lg object-contain"
194
+ onerror={(e) => ((e.target as HTMLImageElement).src = ImgPlaceholder)}
195
+ />
196
+ {/if}
197
+ </div>
198
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { ProfileGalleryData } from "../types";
2
+ type $$ComponentProps = {
3
+ data: ProfileGalleryData;
4
+ isLoading?: boolean;
5
+ className?: string;
6
+ };
7
+ declare const ProfileGallery: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type ProfileGallery = ReturnType<typeof ProfileGallery>;
9
+ export default ProfileGallery;
@@ -0,0 +1,56 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * Menu de navegação entre secções do perfil em formato de badges,
4
+ * usado em mobile no lugar da `ProfileSidebar`. Clicar num badge
5
+ * desloca a página até à secção correspondente.
6
+ * Quando `isLoading` é true, exibe Skeletons.
7
+ * @component
8
+ * @example
9
+ * ```svelte
10
+ * <ProfileMenuBadge items={sections} active={activeSection} onSelect={scrollToSection} />
11
+ * ```
12
+ */
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ import Skeleton from "../../../ui/skeleton/skeleton.svelte";
17
+ import type { ProfileNavItem } from "../types";
18
+
19
+ let {
20
+ items,
21
+ active,
22
+ isLoading = false,
23
+ onSelect,
24
+ className,
25
+ }: {
26
+ items: ProfileNavItem[];
27
+ active?: string;
28
+ isLoading?: boolean;
29
+ onSelect?: (id: string) => void;
30
+ className?: string;
31
+ } = $props();
32
+ </script>
33
+
34
+ <div
35
+ class="sticky top-0 z-10 -mx-4 flex gap-2 overflow-x-auto border-b bg-background/95 px-4 py-3 no-scrollbar backdrop-blur {className ?? ''}"
36
+ >
37
+ {#if isLoading}
38
+ {#each Array.from({ length: 3 }) as _, i (i)}
39
+ <Skeleton class="h-8 w-28 shrink-0 rounded-full" />
40
+ {/each}
41
+ {:else}
42
+ {#each items as item (item.id)}
43
+ <button
44
+ type="button"
45
+ onclick={() => onSelect?.(item.id)}
46
+ aria-current={active === item.id ? "true" : undefined}
47
+ class="shrink-0 whitespace-nowrap rounded-full border px-4 py-1.5 text-sm font-medium transition-colors {active ===
48
+ item.id
49
+ ? 'border-transparent bg-gradient text-white'
50
+ : 'border-border text-muted-foreground hover:text-foreground'}"
51
+ >
52
+ {item.label}
53
+ </button>
54
+ {/each}
55
+ {/if}
56
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { ProfileNavItem } from "../types";
2
+ type $$ComponentProps = {
3
+ items: ProfileNavItem[];
4
+ active?: string;
5
+ isLoading?: boolean;
6
+ onSelect?: (id: string) => void;
7
+ className?: string;
8
+ };
9
+ declare const ProfileMenuBadge: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type ProfileMenuBadge = ReturnType<typeof ProfileMenuBadge>;
11
+ export default ProfileMenuBadge;
@@ -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;
@@ -0,0 +1,57 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * Barra lateral de navegação entre secções do perfil da empresa.
4
+ * Visível apenas em ecrãs médios/grandes — em mobile usar `ProfileMenuBadge`.
5
+ * Quando `isLoading` é true, exibe Skeletons.
6
+ * @component
7
+ * @example
8
+ * ```svelte
9
+ * <ProfileSidebar items={sections} active={activeSection} onSelect={scrollToSection} />
10
+ * ```
11
+ */
12
+ </script>
13
+
14
+ <script lang="ts">
15
+ import Skeleton from "../../../ui/skeleton/skeleton.svelte";
16
+ import type { ProfileNavItem } from "../types";
17
+
18
+ let {
19
+ items,
20
+ active,
21
+ isLoading = false,
22
+ onSelect,
23
+ className,
24
+ }: {
25
+ items: ProfileNavItem[];
26
+ active?: string;
27
+ isLoading?: boolean;
28
+ onSelect?: (id: string) => void;
29
+ className?: string;
30
+ } = $props();
31
+ </script>
32
+
33
+ <nav aria-label="Navegação do perfil" class="w-40 shrink-0 {className ?? ''}">
34
+ <div
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
+ >
37
+ {#if isLoading}
38
+ {#each Array.from({ length: 4 }) as _, i (i)}
39
+ <Skeleton class="h-9 w-full rounded-md" />
40
+ {/each}
41
+ {:else}
42
+ {#each items as item (item.id)}
43
+ <button
44
+ type="button"
45
+ onclick={() => onSelect?.(item.id)}
46
+ aria-current={active === item.id ? "true" : undefined}
47
+ class="flex items-center gap-2 rounded-sm px-3 py-2 text-left text-sm font-medium transition-colors {active ===
48
+ item.id
49
+ ? 'bg-blue-100'
50
+ : 'text-muted-foreground hover:bg-muted hover:text-foreground'}"
51
+ >
52
+ {item.label}
53
+ </button>
54
+ {/each}
55
+ {/if}
56
+ </div>
57
+ </nav>