negodesign 0.0.35 → 0.0.36

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 (73) 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/product/types.d.ts +22 -0
  19. package/dist/components/core/carousel/grid/product/types.js +1 -0
  20. package/dist/components/core/carousel/grid/product/ui/CarouselGridProduct.svelte +61 -0
  21. package/dist/components/core/carousel/grid/product/ui/CarouselGridProduct.svelte.d.ts +4 -0
  22. package/dist/components/core/carousel/grid/promotion/types.d.ts +23 -0
  23. package/dist/components/core/carousel/grid/promotion/types.js +1 -0
  24. package/dist/components/core/carousel/grid/promotion/ui/CarouselGridPromotion.svelte +50 -0
  25. package/dist/components/core/carousel/grid/promotion/ui/CarouselGridPromotion.svelte.d.ts +4 -0
  26. package/dist/components/core/carousel/product/types.d.ts +21 -0
  27. package/dist/components/core/carousel/product/types.js +1 -0
  28. package/dist/components/core/carousel/product/ui/CarouselProduct.svelte +47 -0
  29. package/dist/components/core/carousel/product/ui/CarouselProduct.svelte.d.ts +4 -0
  30. package/dist/components/pages/company-profile/CompanyProfile.svelte +140 -0
  31. package/dist/components/pages/company-profile/CompanyProfile.svelte.d.ts +4 -0
  32. package/dist/components/pages/company-profile/data.d.ts +2 -0
  33. package/dist/components/pages/company-profile/data.js +68 -0
  34. package/dist/components/pages/company-profile/shared/ProfileAboutCompany.svelte +79 -0
  35. package/dist/components/pages/company-profile/shared/ProfileAboutCompany.svelte.d.ts +9 -0
  36. package/dist/components/pages/company-profile/shared/ProfileBanner.svelte +194 -0
  37. package/dist/components/pages/company-profile/shared/ProfileBanner.svelte.d.ts +14 -0
  38. package/dist/components/pages/company-profile/shared/ProfileGallery.svelte +198 -0
  39. package/dist/components/pages/company-profile/shared/ProfileGallery.svelte.d.ts +9 -0
  40. package/dist/components/pages/company-profile/shared/ProfileMenuBadge.svelte +56 -0
  41. package/dist/components/pages/company-profile/shared/ProfileMenuBadge.svelte.d.ts +11 -0
  42. package/dist/components/pages/company-profile/shared/ProfileSidebar.svelte +57 -0
  43. package/dist/components/pages/company-profile/shared/ProfileSidebar.svelte.d.ts +11 -0
  44. package/dist/components/pages/company-profile/types.d.ts +117 -0
  45. package/dist/components/pages/company-profile/types.js +1 -0
  46. package/dist/components/pages/privacy-policy-or-terms-of-use/data.d.ts +3 -0
  47. package/dist/components/pages/privacy-policy-or-terms-of-use/data.js +179 -0
  48. package/dist/components/pages/privacy-policy-or-terms-of-use/shared/DocSectionView.svelte +2 -1
  49. package/dist/components/pages/privacy-policy-or-terms-of-use/shared/DocSectionView.svelte.d.ts +1 -0
  50. package/dist/components/pages/product-details/01/ProductDetails01.svelte +19 -7
  51. package/dist/components/pages/product-details/02/ProductDetails02.svelte +42 -37
  52. package/dist/components/pages/product-details/03/ProductDetails03.svelte +33 -29
  53. package/dist/components/pages/product-details/ProductDetails.svelte +11 -7
  54. package/dist/components/pages/product-details/shared/ProductDetailFeatures.svelte +11 -5
  55. package/dist/components/pages/product-details/shared/ProductDetailFeatures.svelte.d.ts +2 -1
  56. package/dist/components/pages/product-details/shared/ProductDetailFilesVertical.svelte +52 -20
  57. package/dist/components/pages/product-details/shared/ProductDetailFilesVertical.svelte.d.ts +1 -1
  58. package/dist/components/pages/product-details/shared/ProductDetailImage.svelte +51 -20
  59. package/dist/components/pages/product-details/shared/ProductDetailImage.svelte.d.ts +1 -1
  60. package/dist/components/pages/product-details/shared/ProductDetailPrice.svelte +1 -1
  61. package/dist/components/pages/product-details/types.d.ts +9 -2
  62. package/dist/components/pages/profile-user/types.d.ts +7 -0
  63. package/dist/globals.css +244 -5
  64. package/dist/i18n/langs/en.d.ts +10 -0
  65. package/dist/i18n/langs/en.js +10 -0
  66. package/dist/i18n/langs/pt.d.ts +10 -0
  67. package/dist/i18n/langs/pt.js +10 -0
  68. package/dist/i18n/translations.d.ts +20 -0
  69. package/package.json +1 -1
  70. package/dist/components/core/card/shared/CardStartPrice.svelte.d.ts +0 -8
  71. package/dist/components/pages/product-details/04/ProductDetails04.svelte +0 -65
  72. package/dist/components/pages/product-details/04/ProductDetails04.svelte.d.ts +0 -11
  73. /package/dist/components/core/card/{shared/CardStartPrice.svelte → promotion/CardPromotionPrice.svelte} +0 -0
@@ -0,0 +1,179 @@
1
+ export const privacyPolicyData = {
2
+ title: "Política de Privacidade e Termos de Uso",
3
+ lastUpdated: "15 de Agosto de 2026",
4
+ breadcrumb: [
5
+ { label: "Início", href: "/" },
6
+ { label: "Institucional", href: "#" },
7
+ { label: "Termos & Privacidade" },
8
+ ],
9
+ sections: [
10
+ {
11
+ id: "introducao",
12
+ number: "1",
13
+ title: "Introdução",
14
+ level: 1,
15
+ blocks: [
16
+ {
17
+ type: "paragraph",
18
+ text: "Bem-vindo à plataforma Negoturismo. A sua privacidade e a transparência nas relações com os nossos utilizadores, parceiros e clientes são valores fundamentais para a nossa organização.",
19
+ },
20
+ {
21
+ type: "paragraph",
22
+ text: "Este documento estabelece as diretrizes de utilização da plataforma digital Negoturismo, bem como as políticas de recolha, tratamento e proteção dos dados pessoais fornecidos durante o acesso aos nossos serviços turísticos, reservas de hotelaria, voos e contratação de guias e tradutores.",
23
+ },
24
+ ],
25
+ subsections: [
26
+ {
27
+ id: "aceitacao-termos",
28
+ number: "1.1",
29
+ title: "Aceitação dos Termos",
30
+ level: 2,
31
+ blocks: [
32
+ {
33
+ type: "paragraph",
34
+ text: "Ao aceder, navegar ou utilizar qualquer funcionalidade do portal Negoturismo, o utilizador declara expressamente que leu, compreendeu e concorda com todas as disposições aqui estipuladas.",
35
+ },
36
+ {
37
+ type: "list",
38
+ items: [
39
+ "Utilizadores registados têm acesso a funcionalidades exclusivas de gestão de reservas.",
40
+ "Parceiros comerciais e fornecedores de serviços sujeitam-se ainda aos contratos específicos de afiliação.",
41
+ "O uso continuado da plataforma após eventuais alterações implica aceitação tácita das novas condições.",
42
+ ],
43
+ },
44
+ ],
45
+ },
46
+ ],
47
+ },
48
+ {
49
+ id: "recolha-dados",
50
+ number: "2",
51
+ title: "Recolha e Tratamento de Dados Pessoais",
52
+ level: 1,
53
+ blocks: [
54
+ {
55
+ type: "paragraph",
56
+ text: "Para garantir a prestação eficaz dos nossos serviços e proporcionar uma experiência personalizada, recolhemos dados necessários para processamento de pagamentos, validação de identidade e emissão de vouchers de viagem.",
57
+ },
58
+ ],
59
+ subsections: [
60
+ {
61
+ id: "dados-fornecidos",
62
+ number: "2.1",
63
+ title: "Dados Fornecidos Diretamente",
64
+ level: 2,
65
+ blocks: [
66
+ {
67
+ type: "list",
68
+ items: [
69
+ "Nome completo, número de identificação civil ou passaporte.",
70
+ "Endereço de correio eletrónico, número de telefone e morada de faturação.",
71
+ "Informações de pagamento (processadas de forma encriptada através de gateways certificados).",
72
+ "Preferências de viagem, itinerários e histórico de reservas efetuadas.",
73
+ ],
74
+ },
75
+ ],
76
+ },
77
+ {
78
+ id: "finalidade-tratamento",
79
+ number: "2.2",
80
+ title: "Finalidades do Tratamento",
81
+ level: 2,
82
+ blocks: [
83
+ {
84
+ type: "paragraph",
85
+ text: "Os dados recolhidos destinam-se exclusivamente às seguintes finalidades legítimas:",
86
+ },
87
+ {
88
+ type: "list",
89
+ items: [
90
+ "Processamento e confirmação de reservas junto a companhias aéreas, hotéis e parceiros locais.",
91
+ "Comunicação com o cliente sobre alterações de voos, confirmações e avisos de suporte ao cliente.",
92
+ "Prevenção contra fraudes e garantia da segurança operacional da plataforma.",
93
+ "Envio de ofertas e recomendações personalizadas quando previamente autorizado pelo utilizador.",
94
+ ],
95
+ },
96
+ ],
97
+ },
98
+ ],
99
+ },
100
+ {
101
+ id: "reservas-pagamentos",
102
+ number: "3",
103
+ title: "Reservas, Preços e Pagamentos",
104
+ level: 1,
105
+ blocks: [
106
+ {
107
+ type: "paragraph",
108
+ text: "Todas as transações financeiras efetuadas na plataforma são expressas na moeda indicada no momento da compra (Kwanza - AOA ou moeda estrangeira correspondente) e incluem todas as taxas aplicáveis por lei.",
109
+ },
110
+ {
111
+ type: "list",
112
+ items: [
113
+ "A confirmação da reserva está sujeita à validação do pagamento pela instituição financeira emissora.",
114
+ "Preços e disponibilidades de quartos e voos são dinâmicos e sujeitos a alteração até à finalização da compra.",
115
+ "O cliente é responsável pela verificação da exatidão dos dados antes da confirmação do pagamento.",
116
+ ],
117
+ },
118
+ ],
119
+ },
120
+ {
121
+ id: "cancelamentos-reembolsos",
122
+ number: "4",
123
+ title: "Cancelamentos e Reembolsos",
124
+ level: 1,
125
+ blocks: [
126
+ {
127
+ type: "paragraph",
128
+ text: "As condições de cancelamento, alteração de datas e direito a reembolso variam de acordo com as políticas individuais de cada fornecedor de serviços (hotel, companhia aérea, operadora de turismo).",
129
+ },
130
+ {
131
+ type: "list",
132
+ items: [
133
+ "Tarifas não-reembolsáveis não conferem direito à devolução dos valores pagos em caso de desistência.",
134
+ "Pedidos de cancelamento dentro do prazo legal são processados num prazo estimado de 5 a 15 dias úteis.",
135
+ "Eventuais taxas administrativas de intermediação poderão ser deduzidas do montante a reembolsar conforme o regulamento da tarifa.",
136
+ ],
137
+ },
138
+ ],
139
+ },
140
+ {
141
+ id: "seguranca-cookies",
142
+ number: "5",
143
+ title: "Segurança da Informação e Cookies",
144
+ level: 1,
145
+ blocks: [
146
+ {
147
+ type: "paragraph",
148
+ text: "Implementamos protocolos de segurança modernos, incluindo encriptação TLS/SSL e padrões de controlo de acesso rigorosos para proteger os seus dados contra acessos não autorizados, perda ou destruição.",
149
+ },
150
+ {
151
+ type: "paragraph",
152
+ text: "Utilizamos cookies de sessão e analíticos para melhorar a navegação, memorizar preferências e compreender como os nossos serviços são utilizados pelos visitantes.",
153
+ },
154
+ ],
155
+ },
156
+ {
157
+ id: "contacto-suporte",
158
+ number: "6",
159
+ title: "Contactos e Apoio ao Cliente",
160
+ level: 1,
161
+ blocks: [
162
+ {
163
+ type: "paragraph",
164
+ text: "Caso tenha qualquer dúvida sobre esta Política de Privacidade ou pretenda exercer os seus direitos de acesso, retificação ou eliminação de dados, contacte a nossa equipa de encarregados de proteção de dados:",
165
+ },
166
+ {
167
+ type: "list",
168
+ items: [
169
+ "E-mail: privacidade@negoturismo.ao",
170
+ "Suporte Geral: suporte@negoturismo.ao",
171
+ "Telefone: +244 923 000 000 / +244 945 000 000",
172
+ "Endereço: Luanda, Angola",
173
+ ],
174
+ },
175
+ ],
176
+ },
177
+ ],
178
+ };
179
+ export default privacyPolicyData;
@@ -12,6 +12,7 @@
12
12
  <script lang="ts">
13
13
  import type { DocSection } from "../types";
14
14
  import ContentBlockView from "./ContentBlockView.svelte";
15
+ import DocSectionView from "./DocSectionView.svelte";
15
16
 
16
17
  let { section }: { section: DocSection } = $props();
17
18
 
@@ -37,7 +38,7 @@
37
38
  {#if section.subsections}
38
39
  <div class="flex flex-col gap-6 mt-2">
39
40
  {#each section.subsections as subsection (subsection.id)}
40
- <svelte:self section={subsection} />
41
+ <DocSectionView section={subsection} />
41
42
  {/each}
42
43
  </div>
43
44
  {/if}
@@ -1,4 +1,5 @@
1
1
  import type { DocSection } from "../types";
2
+ import DocSectionView from "./DocSectionView.svelte";
2
3
  type $$ComponentProps = {
3
4
  section: DocSection;
4
5
  };
@@ -17,6 +17,7 @@
17
17
  import ProductDetailHeader from "../shared/ProductDetailHeader.svelte";
18
18
  import ProductDetailImage from "../shared/ProductDetailImage.svelte";
19
19
  import ProductDetailMap from "../shared/ProductDetailMap.svelte";
20
+ import ProductDetailReviews from "../shared/ProductDetailReviews.svelte";
20
21
  import ProductDetailTags from "../shared/ProductDetailTags.svelte";
21
22
  import ProductPurchasePanel from "../shared/ProductPurchasePanel.svelte";
22
23
  import type { ProductDetailsData } from "../types";
@@ -42,10 +43,14 @@
42
43
  </div>
43
44
 
44
45
  <div class="grid gap-10 lg:grid-cols-2">
45
- <ProductDetailImage images={data.gallery} {isLoading} />
46
+ <ProductDetailImage files={data.gallery} {isLoading} />
46
47
 
47
48
  <div class="flex flex-col gap-5">
48
- <ProductDetailHeader title={data.title} subtitle={data.subtitle} {isLoading} />
49
+ <ProductDetailHeader
50
+ title={data.title}
51
+ subtitle={data.subtitle}
52
+ {isLoading}
53
+ />
49
54
 
50
55
  <ProductDetailTags tags={data.tags} {isLoading} />
51
56
 
@@ -63,9 +68,16 @@
63
68
  </div>
64
69
  </div>
65
70
 
66
- {#if data.location}
67
- <div class="mt-12">
68
- <ProductDetailMap location={data.location} {isLoading} />
69
- </div>
70
- {/if}
71
+ <div class="grid gap-10 grid-cols-1 md:grid-cols-2 mt-5 md:mt-10">
72
+ {#if data.reviews}
73
+ <div>
74
+ <ProductDetailReviews reviews={data.reviews} {isLoading} />
75
+ </div>
76
+ {/if}
77
+ {#if data.location}
78
+ <div>
79
+ <ProductDetailMap location={data.location} {isLoading} />
80
+ </div>
81
+ {/if}
82
+ </div>
71
83
  </div>
@@ -1,11 +1,11 @@
1
1
  <script lang="ts" module>
2
2
  /**
3
- * Variante 02 da página de detalhes do produto.
4
- * Layout com galeria em destaque e conteúdo em coluna centralizada.
3
+ * Variante 03 da página de detalhes do produto.
4
+ * Layout com galeria, conteúdo expandido e painel de compra sticky.
5
5
  * @component
6
6
  * @example
7
7
  * ```svelte
8
- * <ProductDetails02 {data} {onBuy} {onFavorite} {onCart} />
8
+ * <ProductDetails03 {data} {onBuy} {onFavorite} {onCart} />
9
9
  * ```
10
10
  */
11
11
  </script>
@@ -17,6 +17,7 @@
17
17
  import ProductDetailFilesVertical from "../shared/ProductDetailFilesVertical.svelte";
18
18
  import ProductDetailHeader from "../shared/ProductDetailHeader.svelte";
19
19
  import ProductDetailMap from "../shared/ProductDetailMap.svelte";
20
+ import ProductDetailReviews from "../shared/ProductDetailReviews.svelte";
20
21
  import ProductDetailTags from "../shared/ProductDetailTags.svelte";
21
22
  import ProductPurchasePanel from "../shared/ProductPurchasePanel.svelte";
22
23
  import type { ProductDetailsData } from "../types";
@@ -36,52 +37,56 @@
36
37
  } = $props();
37
38
  </script>
38
39
 
39
- <div class="bg-background">
40
- <ProductDetailFilesVertical
41
- images={data.gallery}
42
- className="mx-auto max-w-5xl px-6 pt-10"
43
- {isLoading}
44
- />
40
+ <div class="mx-auto max-w-7xl px-6 py-10">
41
+ <div class="mb-8">
42
+ <ProductDetailBreadcrumb items={data.breadcrumb} {isLoading} />
43
+ </div>
45
44
 
46
- <div class="mx-auto max-w-3xl px-6 py-12">
47
- <div class="flex flex-col gap-6">
48
- <div class="flex justify-center">
49
- <ProductDetailBreadcrumb items={data.breadcrumb} {isLoading} />
50
- </div>
45
+ <div class="grid gap-10 lg:grid-cols-[1fr_26rem]">
46
+ <div class="flex flex-col gap-8">
47
+ <ProductDetailFilesVertical files={data.gallery} {isLoading} />
51
48
 
52
- <ProductDetailHeader
53
- title={data.title}
54
- subtitle={data.subtitle}
55
- className="items-center text-center"
56
- {isLoading}
57
- />
49
+ <div class="flex flex-col gap-5">
50
+ <ProductDetailHeader
51
+ title={data.title}
52
+ subtitle={data.subtitle}
53
+ {isLoading}
54
+ />
58
55
 
59
- <div class="flex justify-center">
60
56
  <ProductDetailTags tags={data.tags} {isLoading} />
61
- </div>
62
57
 
63
- <ProductDetailDescription
64
- description={data.description}
65
- longDescription={data.longDescription}
66
- {isLoading}
67
- />
58
+ <ProductDetailDescription
59
+ description={data.description}
60
+ longDescription={data.longDescription}
61
+ {isLoading}
62
+ />
68
63
 
69
- {#if data.features}
70
- <div class="mx-auto w-full max-w-md">
64
+ {#if data.features}
71
65
  <ProductDetailFeatures features={data.features} {isLoading} />
72
- </div>
73
- {/if}
74
-
75
- <div class="flex justify-center">
76
- <div class="w-full max-w-sm">
77
- <ProductPurchasePanel {data} {isLoading} {onBuy} {onFavorite} {onCart} />
78
- </div>
66
+ {/if}
79
67
  </div>
80
68
  </div>
69
+
70
+ <aside class="lg:sticky lg:top-8 lg:self-start">
71
+ <div class="rounded-lg border p-6">
72
+ <ProductPurchasePanel
73
+ {data}
74
+ {isLoading}
75
+ {onBuy}
76
+ {onFavorite}
77
+ {onCart}
78
+ />
79
+ </div>
80
+ {#if data.reviews}
81
+ <div class="mt-5 md:mt-10">
82
+ <ProductDetailReviews reviews={data.reviews} {isLoading} />
83
+ </div>
84
+ {/if}
85
+ </aside>
81
86
  </div>
82
87
 
83
88
  {#if data.location}
84
- <div class="mx-auto max-w-3xl px-6 pb-12">
89
+ <div class="mt-12">
85
90
  <ProductDetailMap location={data.location} {isLoading} />
86
91
  </div>
87
92
  {/if}
@@ -1,25 +1,26 @@
1
1
  <script lang="ts" module>
2
2
  /**
3
- * Variante 03 da página de detalhes do produto.
4
- * Layout com galeria, conteúdo expandido e painel de compra sticky.
3
+ * Variante 04 da página de detalhes do produto.
4
+ * Layout com galeria, informações e abas de conteúdo (descrição,
5
+ * avaliações) abaixo dos dados principais.
5
6
  * @component
6
7
  * @example
7
8
  * ```svelte
8
- * <ProductDetails03 {data} {onBuy} {onFavorite} {onCart} />
9
+ * <ProductDetails04 {data} {onBuy} {onFavorite} {onCart} />
9
10
  * ```
10
11
  */
11
12
  </script>
12
13
 
13
14
  <script lang="ts">
14
15
  import ProductDetailBreadcrumb from "../shared/ProductDetailBreadcrumb.svelte";
15
- import ProductDetailDescription from "../shared/ProductDetailDescription.svelte";
16
- import ProductDetailFeatures from "../shared/ProductDetailFeatures.svelte";
17
- import ProductDetailFilesVertical from "../shared/ProductDetailFilesVertical.svelte";
18
16
  import ProductDetailHeader from "../shared/ProductDetailHeader.svelte";
17
+ import ProductDetailImage from "../shared/ProductDetailImage.svelte";
19
18
  import ProductDetailMap from "../shared/ProductDetailMap.svelte";
20
19
  import ProductDetailTags from "../shared/ProductDetailTags.svelte";
20
+ import ProductDetailTabs from "../shared/ProductDetailTabs.svelte";
21
21
  import ProductPurchasePanel from "../shared/ProductPurchasePanel.svelte";
22
22
  import type { ProductDetailsData } from "../types";
23
+ import { useDevice } from "../../../../hooks/responsive.svelte";
23
24
 
24
25
  let {
25
26
  data,
@@ -34,6 +35,7 @@
34
35
  onFavorite?: (id: string | number) => void;
35
36
  onCart?: (id: string | number) => void;
36
37
  } = $props();
38
+ const responsive = useDevice();
37
39
  </script>
38
40
 
39
41
  <div class="mx-auto max-w-7xl px-6 py-10">
@@ -41,37 +43,39 @@
41
43
  <ProductDetailBreadcrumb items={data.breadcrumb} {isLoading} />
42
44
  </div>
43
45
 
44
- <div class="grid gap-10 lg:grid-cols-[1fr_22rem]">
45
- <div class="flex flex-col gap-8">
46
- <ProductDetailFilesVertical images={data.gallery} {isLoading} />
46
+ <div class="grid gap-10 lg:grid-cols-2">
47
+ <ProductDetailImage files={data.gallery} {isLoading} />
47
48
 
48
- <div class="flex flex-col gap-5">
49
- <ProductDetailHeader title={data.title} subtitle={data.subtitle} {isLoading} />
49
+ <div class="flex flex-col gap-5">
50
+ <ProductDetailHeader
51
+ title={data.title}
52
+ subtitle={data.subtitle}
53
+ {isLoading}
54
+ />
50
55
 
51
- <ProductDetailTags tags={data.tags} {isLoading} />
56
+ <ProductDetailTags tags={data.tags} {isLoading} />
52
57
 
53
- <ProductDetailDescription
54
- description={data.description}
55
- longDescription={data.longDescription}
56
- {isLoading}
57
- />
58
+ <ProductPurchasePanel {data} {isLoading} {onBuy} {onFavorite} {onCart} />
58
59
 
59
- {#if data.features}
60
- <ProductDetailFeatures features={data.features} {isLoading} />
60
+ {#if !responsive.isMobile}
61
+ {#if data.location}
62
+ <div class="mt-5">
63
+ <ProductDetailMap location={data.location} {isLoading} />
64
+ </div>
61
65
  {/if}
62
- </div>
66
+ {/if}
63
67
  </div>
68
+ </div>
64
69
 
65
- <aside class="lg:sticky lg:top-8 lg:self-start">
66
- <div class="rounded-lg border p-6">
67
- <ProductPurchasePanel {data} {isLoading} {onBuy} {onFavorite} {onCart} />
68
- </div>
69
- </aside>
70
+ <div class="mt-12 border-t pt-8">
71
+ <ProductDetailTabs tabs={data.tabs} {data} {isLoading} />
70
72
  </div>
71
73
 
72
- {#if data.location}
73
- <div class="mt-12">
74
- <ProductDetailMap location={data.location} {isLoading} />
75
- </div>
74
+ {#if responsive.isMobile}
75
+ {#if data.location}
76
+ <div class="mt-12">
77
+ <ProductDetailMap location={data.location} {isLoading} />
78
+ </div>
79
+ {/if}
76
80
  {/if}
77
81
  </div>
@@ -20,18 +20,22 @@
20
20
  <script lang="ts">
21
21
  import type { ProductDetailsProps } from "./types";
22
22
  import ProductDetails01 from "./01/ProductDetails01.svelte";
23
- import ProductDetails02 from "./02/ProductDetails02.svelte";
24
- import ProductDetails03 from "./03/ProductDetails03.svelte";
25
- import ProductDetails04 from "./04/ProductDetails04.svelte";
23
+ import ProductDetails03 from "./02/ProductDetails02.svelte";
24
+ import ProductDetails04 from "./03/ProductDetails03.svelte";
26
25
 
27
- let { varient = 1, data, isLoading = false, onBuy, onFavorite, onCart }: ProductDetailsProps = $props();
26
+ let {
27
+ varient = 1,
28
+ data,
29
+ isLoading = false,
30
+ onBuy,
31
+ onFavorite,
32
+ onCart,
33
+ }: ProductDetailsProps = $props();
28
34
  </script>
29
35
 
30
36
  {#if varient === 2}
31
- <ProductDetails02 {data} {isLoading} {onBuy} {onFavorite} {onCart} />
32
- {:else if varient === 3}
33
37
  <ProductDetails03 {data} {isLoading} {onBuy} {onFavorite} {onCart} />
34
- {:else if varient === 4}
38
+ {:else if varient === 3}
35
39
  <ProductDetails04 {data} {isLoading} {onBuy} {onFavorite} {onCart} />
36
40
  {:else}
37
41
  <ProductDetails01 {data} {isLoading} {onBuy} {onFavorite} {onCart} />
@@ -12,12 +12,13 @@
12
12
 
13
13
  <script lang="ts">
14
14
  import Skeleton from "../../../ui/skeleton/skeleton.svelte";
15
+ import type { ProductDetailsFeatureProps } from "../types";
15
16
 
16
17
  let {
17
18
  features,
18
19
  isLoading = false,
19
20
  }: {
20
- features: string[];
21
+ features: string[] | ProductDetailsFeatureProps[];
21
22
  isLoading?: boolean;
22
23
  } = $props();
23
24
  </script>
@@ -29,10 +30,15 @@
29
30
  {/each}
30
31
  </div>
31
32
  {:else}
32
- <ul class="flex flex-col gap-2">
33
- {#each features as feature, i (feature)}
34
- <li class="flex items-start gap-2 text-sm text-foreground/90">
35
- <span aria-hidden="true" class="mt-1.5 size-1.5 shrink-0 rounded-full bg-primary"></span>
33
+ <ul class="flex flex-wrap gap-2 md:gap-5">
34
+ {#each features as feature, i (i)}
35
+ <li
36
+ class="flex items-start gap-2 text-sm text-foreground/90 border rounded-lg p-2"
37
+ >
38
+ <span
39
+ aria-hidden="true"
40
+ class="mt-1.5 size-1.5 shrink-0 rounded-full bg-gradient"
41
+ ></span>
36
42
  <span>{feature}</span>
37
43
  </li>
38
44
  {/each}
@@ -1,5 +1,6 @@
1
+ import type { ProductDetailsFeatureProps } from "../types";
1
2
  type $$ComponentProps = {
2
- features: string[];
3
+ features: string[] | ProductDetailsFeatureProps[];
3
4
  isLoading?: boolean;
4
5
  };
5
6
  declare const ProductDetailFeatures: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1,11 +1,12 @@
1
1
  <script lang="ts" module>
2
2
  /**
3
- * Lista vertical de ficheiros/thumbnails do produto com seleção de imagem.
3
+ * Lista vertical de ficheiros/thumbnails do produto com seleção de imagem/vídeo.
4
4
  * Quando `isLoading` é true, exibe Skeletons.
5
+ * Se houver um item do tipo "video", ele é priorizado (aparece primeiro).
5
6
  * @component
6
7
  * @example
7
8
  * ```svelte
8
- * <ProductDetailFilesVertical images={data.gallery} />
9
+ * <ProductDetailFilesVertical files={data.gallery} />
9
10
  * ```
10
11
  */
11
12
  </script>
@@ -15,57 +16,88 @@
15
16
  import type { ProductDetailsImage } from "../types";
16
17
 
17
18
  let {
18
- images,
19
+ files,
19
20
  className,
20
21
  isLoading = false,
21
22
  }: {
22
- images: ProductDetailsImage[];
23
+ files: ProductDetailsImage[];
23
24
  className?: string;
24
25
  isLoading?: boolean;
25
26
  } = $props();
26
27
 
28
+ // Vídeo primeiro, mantendo a ordem relativa do resto
29
+ const sortedFiles = $derived(
30
+ [...(files ?? [])].sort(
31
+ (a, b) => (a.type === "video" ? 0 : 1) - (b.type === "video" ? 0 : 1),
32
+ ),
33
+ );
34
+
27
35
  let activeIndex = $state(0);
28
- const active = $derived(images[activeIndex]);
36
+ const active = $derived(sortedFiles[activeIndex]);
29
37
  </script>
30
38
 
39
+ {#snippet media(file: ProductDetailsImage, cls: string)}
40
+ {#if file.type === "video"}
41
+ <!-- svelte-ignore a11y_media_has_caption -->
42
+ <video src={file.src} class={cls} muted playsinline></video>
43
+ {:else}
44
+ <img src={file.src} alt={file.alt ?? ""} class={cls} />
45
+ {/if}
46
+ {/snippet}
47
+
31
48
  <div class="flex flex-col gap-3 {className}">
32
49
  {#if isLoading}
33
50
  <div class="flex gap-3">
34
51
  <div class="flex shrink-0 flex-col gap-2">
35
- {#each Array.from({ length: Math.min(images.length || 3, 4) }) as _, i (i)}
52
+ {#each Array.from( { length: Math.min(files.length || 3, 4) }, ) as _, i (i)}
36
53
  <Skeleton class="h-16 w-16 rounded-md" />
37
54
  {/each}
38
55
  </div>
39
56
  <Skeleton class="aspect-4/3 min-w-0 flex-1 rounded-lg" />
40
57
  </div>
41
- {:else if images.length > 0}
58
+ {:else if sortedFiles.length > 0}
42
59
  <div class="flex gap-3">
43
60
  <div class="flex shrink-0 flex-col gap-2">
44
- {#each images as image, i (image.src)}
61
+ {#each sortedFiles as file, i (file.src)}
45
62
  <button
46
63
  type="button"
47
- aria-label={image.alt ?? `Imagem ${i + 1}`}
64
+ aria-label={file.alt ?? `Item ${i + 1}`}
48
65
  onclick={() => (activeIndex = i)}
49
- class="overflow-hidden rounded-md border transition-opacity {i ===
66
+ class="relative overflow-hidden rounded-md border transition-opacity {i ===
50
67
  activeIndex
51
68
  ? 'border-foreground opacity-100'
52
69
  : 'border-muted opacity-60 hover:opacity-100'}"
53
70
  >
54
- <img
55
- src={image.src}
56
- alt={image.alt ?? ""}
57
- class="h-16 w-16 object-cover"
58
- />
71
+ {@render media(file, "h-16 w-16 object-cover")}
72
+ {#if file.type === "video"}
73
+ <span
74
+ class="absolute inset-0 flex items-center justify-center bg-black/30"
75
+ >
76
+ <svg
77
+ viewBox="0 0 24 24"
78
+ class="h-4 w-4 fill-white"
79
+ aria-hidden="true"
80
+ >
81
+ <path d="M8 5v14l11-7z" />
82
+ </svg>
83
+ </span>
84
+ {/if}
59
85
  </button>
60
86
  {/each}
61
87
  </div>
62
88
 
63
89
  <div class="min-w-0 flex-1 overflow-hidden rounded-lg border">
64
- <img
65
- src={active.src}
66
- alt={active.alt ?? ""}
67
- class="aspect-4/3 w-full object-cover"
68
- />
90
+ {#if active.type === "video"}
91
+ <!-- svelte-ignore a11y_media_has_caption -->
92
+ <video
93
+ src={active.src}
94
+ class="aspect-4/3 w-full object-cover"
95
+ controls
96
+ playsinline
97
+ ></video>
98
+ {:else}
99
+ {@render media(active, "aspect-4/3 w-full object-cover")}
100
+ {/if}
69
101
  </div>
70
102
  </div>
71
103
  {/if}