cinqcinqdev-seo 0.1.13 → 0.1.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.
- package/dist/module.json +1 -1
- package/dist/runtime/assets/admin-tw.css +1 -1
- package/dist/runtime/components/admin/Sidebar.vue +41 -13
- package/dist/runtime/components/sections/ContactForm.d.vue.ts +10 -0
- package/dist/runtime/components/sections/ContactForm.vue +134 -0
- package/dist/runtime/components/sections/ContactForm.vue.d.ts +10 -0
- package/dist/runtime/components/sections/FAQ.d.vue.ts +3 -0
- package/dist/runtime/components/sections/FAQ.vue +42 -0
- package/dist/runtime/components/sections/FAQ.vue.d.ts +3 -0
- package/dist/runtime/components/sections/Features.d.vue.ts +3 -0
- package/dist/runtime/components/sections/Features.vue +36 -0
- package/dist/runtime/components/sections/Features.vue.d.ts +3 -0
- package/dist/runtime/components/sections/HeroSection.d.vue.ts +3 -0
- package/dist/runtime/components/sections/HeroSection.vue +129 -0
- package/dist/runtime/components/sections/HeroSection.vue.d.ts +3 -0
- package/dist/runtime/components/sections/Pricing.d.vue.ts +3 -0
- package/dist/runtime/components/sections/Pricing.vue +105 -0
- package/dist/runtime/components/sections/Pricing.vue.d.ts +3 -0
- package/dist/runtime/components/sections/Process.d.vue.ts +3 -0
- package/dist/runtime/components/sections/Process.vue +90 -0
- package/dist/runtime/components/sections/Process.vue.d.ts +3 -0
- package/dist/runtime/components/sections/ServiceGrid.d.vue.ts +3 -0
- package/dist/runtime/components/sections/ServiceGrid.vue +92 -0
- package/dist/runtime/components/sections/ServiceGrid.vue.d.ts +3 -0
- package/dist/runtime/components/sections/Testimonials.d.vue.ts +3 -0
- package/dist/runtime/components/sections/Testimonials.vue +90 -0
- package/dist/runtime/components/sections/Testimonials.vue.d.ts +3 -0
- package/dist/runtime/components/sections/TextVisual.d.vue.ts +3 -0
- package/dist/runtime/components/sections/TextVisual.vue +89 -0
- package/dist/runtime/components/sections/TextVisual.vue.d.ts +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="relative overflow-hidden" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
+
|
|
4
|
+
<!-- CENTERED -->
|
|
5
|
+
<div v-if="layout === 'centered'" class="px-6 md:px-12 pt-20 pb-16 text-center">
|
|
6
|
+
<div class="max-w-4xl mx-auto">
|
|
7
|
+
<span v-if="badge" class="inline-flex items-center gap-1.5 text-[10px] font-bold tracking-[0.4em] uppercase px-4 py-1.5 rounded-full border mb-8" :style="{ borderColor: textColor + '30', color: textColor + '80' }">
|
|
8
|
+
<span class="w-1.5 h-1.5 rounded-full inline-block" :style="{ backgroundColor: textColor + '60' }"></span>{{ badge }}
|
|
9
|
+
</span>
|
|
10
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.88] mb-6" :style="{ fontSize: titleSize === 'sm' ? 'clamp(2.5rem,5vw,4rem)' : titleSize === 'lg' ? 'clamp(3.5rem,9vw,8rem)' : 'clamp(3rem,7vw,6rem)' }">{{ title }}</component>
|
|
11
|
+
<p class="text-sm leading-relaxed max-w-lg mx-auto mb-10" :style="{ opacity: 0.5 }">{{ subtitle }}</p>
|
|
12
|
+
<NuxtLink :to="ctaLink || '#'" class="inline-flex items-center gap-2.5 px-8 py-3.5 rounded-full text-[11px] font-black uppercase tracking-widest transition-all hover:gap-5 active:scale-95" :style="{ backgroundColor: textColor, color: bgColor }">{{ ctaText }}<span>→</span></NuxtLink>
|
|
13
|
+
<div v-if="image" class="mt-14 mx-auto aspect-video rounded-3xl overflow-hidden max-w-3xl">
|
|
14
|
+
<img :src="image" class="w-full h-full object-cover" />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<!-- SPLIT -->
|
|
20
|
+
<div v-else-if="layout === 'split'" class="px-6 md:px-12 py-16">
|
|
21
|
+
<div class="max-w-6xl mx-auto grid md:grid-cols-2 gap-12 md:gap-20 items-center">
|
|
22
|
+
<div>
|
|
23
|
+
<span v-if="badge" class="inline-block text-[9px] font-black tracking-[0.5em] uppercase mb-6 opacity-35">{{ badge }}</span>
|
|
24
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.88] mb-6" :style="{ fontSize: 'clamp(2.2rem,5vw,4.5rem)' }">{{ title }}</component>
|
|
25
|
+
<p class="text-sm leading-relaxed mb-10 max-w-sm" :style="{ opacity: 0.5 }">{{ subtitle }}</p>
|
|
26
|
+
<NuxtLink :to="ctaLink || '#'" class="inline-flex items-center gap-2.5 px-7 py-3 rounded-full text-[11px] font-black uppercase tracking-widest transition-all hover:gap-5 active:scale-95" :style="{ backgroundColor: textColor, color: bgColor }">{{ ctaText }}<span>→</span></NuxtLink>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="aspect-[4/3] rounded-3xl overflow-hidden">
|
|
29
|
+
<img v-if="image" :src="image" class="w-full h-full object-cover" />
|
|
30
|
+
<div v-else class="w-full h-full" :style="{ backgroundColor: textColor + '06' }"></div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- BENTO -->
|
|
36
|
+
<div v-else-if="layout === 'bento'" class="p-3 md:p-5">
|
|
37
|
+
<div class="max-w-6xl mx-auto grid grid-cols-12 gap-3">
|
|
38
|
+
<div class="col-span-12 md:col-span-7 rounded-[28px] p-8 md:p-10 border flex flex-col justify-between min-h-[320px]" :style="{ borderColor: textColor + '10', backgroundColor: textColor + '04' }">
|
|
39
|
+
<span v-if="badge" class="text-[9px] font-black uppercase tracking-[0.4em] opacity-30">{{ badge }}</span>
|
|
40
|
+
<div>
|
|
41
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.88] mb-4" :style="{ fontSize: 'clamp(2rem,4.5vw,3.8rem)' }">{{ title }}</component>
|
|
42
|
+
<p class="text-sm leading-relaxed mb-8 max-w-xs" :style="{ opacity: 0.45 }">{{ subtitle }}</p>
|
|
43
|
+
<NuxtLink :to="ctaLink || '#'" class="inline-flex items-center gap-2.5 px-6 py-3 rounded-full text-[10px] font-black uppercase tracking-widest transition-all hover:gap-5 active:scale-95" :style="{ backgroundColor: textColor, color: bgColor }">{{ ctaText }}<span>→</span></NuxtLink>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="col-span-12 md:col-span-5 rounded-[28px] overflow-hidden min-h-[260px] md:min-h-[320px]">
|
|
47
|
+
<img v-if="image" :src="image" class="w-full h-full object-cover" />
|
|
48
|
+
<div v-else class="w-full h-full" :style="{ backgroundColor: textColor + '08' }"></div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- FULLSCREEN -->
|
|
54
|
+
<div v-else-if="layout === 'fullscreen'" class="relative min-h-[88vh] flex flex-col justify-end">
|
|
55
|
+
<div class="absolute inset-0">
|
|
56
|
+
<img v-if="image" :src="image" class="w-full h-full object-cover" />
|
|
57
|
+
<div v-else class="w-full h-full" :style="{ backgroundColor: textColor + '08' }"></div>
|
|
58
|
+
<div class="absolute inset-0" :style="{ background: `linear-gradient(to top, ${bgColor} 20%, ${bgColor}cc 40%, ${bgColor}44 65%, transparent 100%)` }"></div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="relative z-10 max-w-6xl mx-auto w-full px-6 md:px-12 pb-14">
|
|
61
|
+
<span v-if="badge" class="inline-block text-[9px] font-black uppercase tracking-[0.5em] opacity-40 mb-4">{{ badge }}</span>
|
|
62
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.88] mb-6" :style="{ fontSize: 'clamp(3rem,8vw,7rem)' }">{{ title }}</component>
|
|
63
|
+
<div class="flex flex-col sm:flex-row sm:items-center gap-6 sm:gap-10">
|
|
64
|
+
<p class="text-sm leading-relaxed max-w-xs" :style="{ opacity: 0.6 }">{{ subtitle }}</p>
|
|
65
|
+
<NuxtLink :to="ctaLink || '#'" class="inline-flex items-center gap-2.5 px-7 py-3 rounded-full text-[11px] font-black uppercase tracking-widest border transition-all hover:gap-5 shrink-0" :style="{ borderColor: textColor + '50', color: textColor }">{{ ctaText }}<span>→</span></NuxtLink>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- MINIMAL -->
|
|
71
|
+
<div v-else-if="layout === 'minimal'" class="px-6 md:px-12 py-14">
|
|
72
|
+
<div class="max-w-6xl mx-auto">
|
|
73
|
+
<div class="flex items-center gap-4 mb-10 border-b pb-5" :style="{ borderColor: textColor + '10' }">
|
|
74
|
+
<span v-if="badge" class="text-[9px] font-black uppercase tracking-[0.5em] opacity-30">{{ badge }}</span>
|
|
75
|
+
<div class="flex-1 h-px" :style="{ backgroundColor: textColor + '08' }"></div>
|
|
76
|
+
<span class="text-[9px] font-black uppercase tracking-[0.5em] opacity-20">{{ ctaText }}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.85] mb-10" :style="{ fontSize: 'clamp(3.5rem,10vw,9rem)' }">{{ title }}</component>
|
|
79
|
+
<div v-if="image" class="mb-10 overflow-hidden rounded-2xl aspect-video">
|
|
80
|
+
<img :src="image" class="w-full h-full object-cover" />
|
|
81
|
+
</div>
|
|
82
|
+
<div class="flex flex-col md:flex-row items-start md:items-end justify-between gap-6 border-t pt-8" :style="{ borderColor: textColor + '10' }">
|
|
83
|
+
<p class="text-sm leading-relaxed max-w-xs" :style="{ opacity: 0.5 }">{{ subtitle }}</p>
|
|
84
|
+
<NuxtLink :to="ctaLink || '#'" class="inline-flex items-center gap-2.5 text-[11px] font-black uppercase tracking-widest transition-all hover:gap-5 shrink-0">{{ ctaText }}<span>→</span></NuxtLink>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- EDITORIAL -->
|
|
90
|
+
<div v-else class="px-6 md:px-12 py-14">
|
|
91
|
+
<div class="max-w-6xl mx-auto">
|
|
92
|
+
<div class="flex items-center gap-4 mb-8">
|
|
93
|
+
<div class="w-8 h-px" :style="{ backgroundColor: textColor + '30' }"></div>
|
|
94
|
+
<span class="text-[9px] font-black uppercase tracking-[0.5em] opacity-30">{{ badge || '—' }}</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="flex flex-col md:flex-row md:items-end gap-8 mb-8">
|
|
97
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.85] flex-1" :style="{ fontSize: 'clamp(3rem,9vw,9rem)' }">{{ title }}</component>
|
|
98
|
+
<div v-if="image" class="md:w-56 md:shrink-0 aspect-[3/4] rounded-3xl overflow-hidden">
|
|
99
|
+
<img :src="image" class="w-full h-full object-cover" />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-6 border-t pt-8" :style="{ borderColor: textColor + '10' }">
|
|
103
|
+
<p class="text-sm leading-relaxed max-w-sm" :style="{ opacity: 0.5 }">{{ subtitle }}</p>
|
|
104
|
+
<NuxtLink :to="ctaLink || '#'" class="inline-flex items-center gap-2.5 px-7 py-3 rounded-full text-[11px] font-black uppercase tracking-widest transition-all hover:gap-5 shrink-0 active:scale-95" :style="{ backgroundColor: textColor, color: bgColor }">{{ ctaText }}<span>→</span></NuxtLink>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
</section>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<script setup>
|
|
113
|
+
defineProps({
|
|
114
|
+
layout: { type: String, default: "centered" },
|
|
115
|
+
animation: { type: String, default: "fadeUp" },
|
|
116
|
+
titleSize: { type: String, default: "md" },
|
|
117
|
+
title: { type: String, default: "Impactful Title" },
|
|
118
|
+
titleTag: { type: String, default: "h1" },
|
|
119
|
+
subtitle: { type: String, default: "" },
|
|
120
|
+
badge: { type: String, default: "" },
|
|
121
|
+
ctaText: { type: String, default: "Get Started" },
|
|
122
|
+
ctaLink: { type: String, default: "#" },
|
|
123
|
+
image: { type: String, default: "" },
|
|
124
|
+
imageStyle: { type: String, default: "cover" },
|
|
125
|
+
imagePosition: { type: String, default: "center" },
|
|
126
|
+
bgColor: { type: String, default: "#3d35ff" },
|
|
127
|
+
textColor: { type: String, default: "#ffffff" }
|
|
128
|
+
});
|
|
129
|
+
</script>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="py-14 px-6 md:px-12" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
+
<div class="max-w-6xl mx-auto">
|
|
4
|
+
|
|
5
|
+
<div v-if="title || description" class="mb-12 text-center max-w-2xl mx-auto">
|
|
6
|
+
<span v-if="badge" class="inline-block text-[10px] font-bold tracking-[0.5em] uppercase px-3 py-1 rounded-full border mb-4" :style="{ borderColor: textColor + '25', color: textColor + 'aa' }">{{ badge }}</span>
|
|
7
|
+
<component :is="titleTag" class="text-3xl md:text-5xl font-black uppercase tracking-tighter leading-[0.9] mb-4">{{ title }}</component>
|
|
8
|
+
<p v-if="description" class="text-sm leading-relaxed" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- CARDS -->
|
|
12
|
+
<div v-if="layout === 'cards'" class="grid grid-cols-1 md:grid-cols-3 gap-6 items-end">
|
|
13
|
+
<div v-for="(plan, i) in items" :key="i"
|
|
14
|
+
class="rounded-3xl border p-8 flex flex-col transition-all duration-300 relative"
|
|
15
|
+
:class="plan.highlighted ? 'md:-translate-y-4 shadow-2xl' : ''"
|
|
16
|
+
:style="plan.highlighted ? { backgroundColor: textColor, color: bgColor, borderColor: textColor } : { backgroundColor: textColor + '04', borderColor: textColor + '12' }">
|
|
17
|
+
<div v-if="plan.highlighted" class="absolute -top-3 left-1/2 -translate-x-1/2">
|
|
18
|
+
<span class="px-4 py-1 rounded-full text-[9px] font-black uppercase tracking-widest" :style="{ backgroundColor: bgColor, color: textColor }">Populaire</span>
|
|
19
|
+
</div>
|
|
20
|
+
<p class="text-[11px] font-black uppercase tracking-widest mb-4" :style="{ opacity: 0.6 }">{{ plan.name }}</p>
|
|
21
|
+
<div class="mb-2">
|
|
22
|
+
<span class="text-4xl font-black tracking-tighter">{{ plan.price }}</span>
|
|
23
|
+
<span v-if="plan.period" class="text-sm ml-1" :style="{ opacity: 0.5 }">{{ plan.period }}</span>
|
|
24
|
+
</div>
|
|
25
|
+
<p v-if="plan.description" class="text-xs leading-relaxed mb-6" :style="{ opacity: 0.55 }">{{ plan.description }}</p>
|
|
26
|
+
<ul v-if="plan.features && plan.features.length" class="space-y-3 mb-8 flex-1">
|
|
27
|
+
<li v-for="(feat, fi) in plan.features" :key="fi" class="flex items-start gap-2.5 text-sm">
|
|
28
|
+
<span class="text-[10px] mt-0.5 shrink-0">✓</span>
|
|
29
|
+
<span :style="{ opacity: 0.75 }">{{ typeof feat === 'object' ? (feat.fr || feat) : feat }}</span>
|
|
30
|
+
</li>
|
|
31
|
+
</ul>
|
|
32
|
+
<div class="flex-1"></div>
|
|
33
|
+
<NuxtLink v-if="plan.ctaText || plan.ctaLink" :to="plan.ctaLink || '#'"
|
|
34
|
+
class="block text-center py-3 px-6 rounded-full text-[11px] font-black uppercase tracking-widest transition-all hover:opacity-80 active:scale-95 mt-6"
|
|
35
|
+
:style="plan.highlighted ? { backgroundColor: bgColor, color: textColor } : { backgroundColor: textColor, color: bgColor }">
|
|
36
|
+
{{ plan.ctaText || 'Get started' }}
|
|
37
|
+
</NuxtLink>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- MINIMAL -->
|
|
42
|
+
<div v-else-if="layout === 'minimal'" class="divide-y max-w-2xl mx-auto" :style="{ borderColor: textColor + '12' }">
|
|
43
|
+
<div v-for="(plan, i) in items" :key="i" class="flex items-center gap-6 py-7" :style="{ borderColor: textColor + '12' }">
|
|
44
|
+
<div class="flex-1">
|
|
45
|
+
<p class="font-black uppercase tracking-tight text-lg leading-none mb-1">{{ plan.name }}</p>
|
|
46
|
+
<p v-if="plan.description" class="text-xs" :style="{ opacity: 0.5 }">{{ plan.description }}</p>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="text-right shrink-0">
|
|
49
|
+
<p class="text-2xl font-black tracking-tighter">{{ plan.price }}</p>
|
|
50
|
+
<p v-if="plan.period" class="text-[10px]" :style="{ opacity: 0.4 }">{{ plan.period }}</p>
|
|
51
|
+
</div>
|
|
52
|
+
<NuxtLink v-if="plan.ctaText || plan.ctaLink" :to="plan.ctaLink || '#'"
|
|
53
|
+
class="px-5 py-2 rounded-full text-[10px] font-black uppercase tracking-widest shrink-0"
|
|
54
|
+
:style="{ backgroundColor: textColor, color: bgColor }">
|
|
55
|
+
{{ plan.ctaText || '→' }}
|
|
56
|
+
</NuxtLink>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- TABLE -->
|
|
61
|
+
<div v-else class="overflow-x-auto">
|
|
62
|
+
<table class="w-full text-sm border-collapse">
|
|
63
|
+
<thead>
|
|
64
|
+
<tr class="border-b" :style="{ borderColor: textColor + '12' }">
|
|
65
|
+
<th class="text-left py-4 pr-6 font-black uppercase tracking-widest text-[10px]" :style="{ opacity: 0.4 }">Plan</th>
|
|
66
|
+
<th v-for="(plan, i) in items" :key="i" class="text-center py-4 px-4 font-black uppercase tracking-tight text-sm" :class="plan.highlighted ? '' : ''" :style="plan.highlighted ? { color: textColor } : { opacity: 0.7 }">
|
|
67
|
+
{{ plan.name }}<br/><span class="text-2xl font-black tracking-tighter">{{ plan.price }}</span>
|
|
68
|
+
</th>
|
|
69
|
+
</tr>
|
|
70
|
+
</thead>
|
|
71
|
+
<tbody>
|
|
72
|
+
<tr v-for="(feat, fi) in allFeatures" :key="fi" class="border-b" :style="{ borderColor: textColor + '08' }">
|
|
73
|
+
<td class="py-4 pr-6 text-xs" :style="{ opacity: 0.6 }">{{ typeof feat === 'object' ? (feat.fr || feat) : feat }}</td>
|
|
74
|
+
<td v-for="(plan, pi) in items" :key="pi" class="py-4 px-4 text-center text-sm">
|
|
75
|
+
<span v-if="planHasFeature(plan, feat)">✓</span>
|
|
76
|
+
<span v-else :style="{ opacity: 0.2 }">—</span>
|
|
77
|
+
</td>
|
|
78
|
+
</tr>
|
|
79
|
+
</tbody>
|
|
80
|
+
</table>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<script setup>
|
|
88
|
+
import { computed } from "vue";
|
|
89
|
+
const props = defineProps({
|
|
90
|
+
layout: { type: String, default: "cards" },
|
|
91
|
+
title: { type: String, default: "Our Pricing" },
|
|
92
|
+
titleTag: { type: String, default: "h2" },
|
|
93
|
+
badge: { type: String, default: "" },
|
|
94
|
+
description: { type: String, default: "" },
|
|
95
|
+
items: { type: Array, default: () => [] },
|
|
96
|
+
bgColor: { type: String, default: "#ffffff" },
|
|
97
|
+
textColor: { type: String, default: "#000000" }
|
|
98
|
+
});
|
|
99
|
+
const allFeatures = computed(() => {
|
|
100
|
+
const set = /* @__PURE__ */ new Set();
|
|
101
|
+
props.items.forEach((p) => (p.features || []).forEach((f) => set.add(typeof f === "object" ? f.fr || JSON.stringify(f) : f)));
|
|
102
|
+
return [...set];
|
|
103
|
+
});
|
|
104
|
+
const planHasFeature = (plan, feat) => (plan.features || []).some((f) => (typeof f === "object" ? f.fr || JSON.stringify(f) : f) === (typeof feat === "object" ? feat.fr || JSON.stringify(feat) : feat));
|
|
105
|
+
</script>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="py-14 px-6 md:px-12" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
+
<div class="max-w-6xl mx-auto">
|
|
4
|
+
|
|
5
|
+
<div v-if="title || description" class="mb-12" :class="['horizontal','cards'].includes(layout) ? 'text-center max-w-2xl mx-auto' : 'max-w-xl'">
|
|
6
|
+
<span v-if="badge" class="inline-block text-[10px] font-bold tracking-[0.5em] uppercase px-3 py-1 rounded-full border mb-4" :style="{ borderColor: textColor + '25', color: textColor + 'aa' }">{{ badge }}</span>
|
|
7
|
+
<component :is="titleTag" class="text-3xl md:text-5xl font-black uppercase tracking-tighter leading-[0.9] mb-4">{{ title }}</component>
|
|
8
|
+
<p v-if="description" class="text-sm leading-relaxed" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- STEPS (vertical) -->
|
|
12
|
+
<div v-if="layout === 'steps'" class="relative max-w-2xl">
|
|
13
|
+
<div class="absolute left-5 top-0 bottom-0 w-px" :style="{ backgroundColor: textColor + '12' }"></div>
|
|
14
|
+
<div v-for="(step, i) in items" :key="i" class="relative flex gap-8 pb-12 last:pb-0">
|
|
15
|
+
<div class="relative z-10 w-10 h-10 rounded-full flex items-center justify-center text-[11px] font-black shrink-0" :style="{ backgroundColor: textColor, color: bgColor }">
|
|
16
|
+
{{ step.icon || String(i+1).padStart(2,'0') }}
|
|
17
|
+
</div>
|
|
18
|
+
<div class="pt-1.5 flex-1">
|
|
19
|
+
<component :is="itemTag" class="text-lg font-black uppercase tracking-tight mb-2 leading-tight">{{ step.title }}</component>
|
|
20
|
+
<p class="text-sm leading-relaxed" :style="{ opacity: 0.55 }">{{ step.description }}</p>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<!-- HORIZONTAL -->
|
|
26
|
+
<div v-else-if="layout === 'horizontal'" class="flex flex-col md:flex-row gap-8">
|
|
27
|
+
<div v-for="(step, i) in items" :key="i" class="flex-1 flex flex-col items-center text-center relative">
|
|
28
|
+
<div v-if="i < items.length - 1" class="hidden md:block absolute top-5 left-1/2 w-full h-px" :style="{ backgroundColor: textColor + '15' }"></div>
|
|
29
|
+
<div class="relative z-10 w-10 h-10 rounded-full flex items-center justify-center text-[11px] font-black mb-5 shrink-0" :style="{ backgroundColor: textColor, color: bgColor }">
|
|
30
|
+
{{ step.icon || String(i+1).padStart(2,'0') }}
|
|
31
|
+
</div>
|
|
32
|
+
<component :is="itemTag" class="text-sm font-black uppercase tracking-tight mb-2 leading-tight">{{ step.title }}</component>
|
|
33
|
+
<p class="text-xs leading-relaxed px-4" :style="{ opacity: 0.5 }">{{ step.description }}</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- CARDS -->
|
|
38
|
+
<div v-else-if="layout === 'cards'" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
|
39
|
+
<div v-for="(step, i) in items" :key="i" class="p-7 rounded-3xl border" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '10' }">
|
|
40
|
+
<div class="w-10 h-10 rounded-full flex items-center justify-center text-[11px] font-black mb-5" :style="{ backgroundColor: textColor, color: bgColor }">
|
|
41
|
+
{{ step.icon || String(i+1).padStart(2,'0') }}
|
|
42
|
+
</div>
|
|
43
|
+
<component :is="itemTag" class="font-black uppercase tracking-tight mb-3 leading-tight">{{ step.title }}</component>
|
|
44
|
+
<p class="text-sm leading-relaxed" :style="{ opacity: 0.55 }">{{ step.description }}</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- TIMELINE -->
|
|
49
|
+
<div v-else-if="layout === 'timeline'" class="relative max-w-3xl mx-auto">
|
|
50
|
+
<div class="absolute left-1/2 -translate-x-1/2 top-0 bottom-0 w-px" :style="{ backgroundColor: textColor + '12' }"></div>
|
|
51
|
+
<div v-for="(step, i) in items" :key="i" :class="['relative flex items-start gap-8 pb-12 last:pb-0', i % 2 === 0 ? 'flex-row' : 'flex-row-reverse']">
|
|
52
|
+
<div class="flex-1 text-right" :class="i % 2 === 0 ? '' : 'text-left'">
|
|
53
|
+
<component :is="itemTag" class="font-black uppercase tracking-tight mb-2 leading-tight">{{ step.title }}</component>
|
|
54
|
+
<p class="text-sm leading-relaxed" :style="{ opacity: 0.55 }">{{ step.description }}</p>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="relative z-10 w-10 h-10 rounded-full flex items-center justify-center text-[11px] font-black shrink-0" :style="{ backgroundColor: textColor, color: bgColor }">
|
|
57
|
+
{{ step.icon || String(i+1).padStart(2,'0') }}
|
|
58
|
+
</div>
|
|
59
|
+
<div class="flex-1"></div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- MINIMAL -->
|
|
64
|
+
<div v-else class="divide-y max-w-2xl" :style="{ borderColor: textColor + '10' }">
|
|
65
|
+
<div v-for="(step, i) in items" :key="i" class="flex items-start gap-6 py-6" :style="{ borderColor: textColor + '10' }">
|
|
66
|
+
<span class="text-[11px] font-black opacity-20 shrink-0 w-5">{{ String(i+1).padStart(2,'0') }}</span>
|
|
67
|
+
<div>
|
|
68
|
+
<component :is="itemTag" class="font-black uppercase tracking-tight mb-1 leading-tight">{{ step.title }}</component>
|
|
69
|
+
<p class="text-sm" :style="{ opacity: 0.5 }">{{ step.description }}</p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup>
|
|
79
|
+
defineProps({
|
|
80
|
+
layout: { type: String, default: "steps" },
|
|
81
|
+
title: { type: String, default: "How it works" },
|
|
82
|
+
titleTag: { type: String, default: "h2" },
|
|
83
|
+
badge: { type: String, default: "" },
|
|
84
|
+
description: { type: String, default: "" },
|
|
85
|
+
itemTag: { type: String, default: "h3" },
|
|
86
|
+
items: { type: Array, default: () => [] },
|
|
87
|
+
bgColor: { type: String, default: "#ffffff" },
|
|
88
|
+
textColor: { type: String, default: "#000000" }
|
|
89
|
+
});
|
|
90
|
+
</script>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="py-14 px-6 md:px-12" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
+
<div class="max-w-6xl mx-auto">
|
|
4
|
+
|
|
5
|
+
<div v-if="title || description" class="mb-12" :class="headerAlign === 'center' ? 'text-center max-w-2xl mx-auto' : 'max-w-xl'">
|
|
6
|
+
<span v-if="badge" class="inline-block text-[10px] font-bold tracking-[0.5em] uppercase px-3 py-1 rounded-full border mb-4" :style="{ borderColor: textColor + '25', color: textColor + 'aa' }">{{ badge }}</span>
|
|
7
|
+
<component :is="titleTag" class="text-3xl md:text-5xl font-black uppercase tracking-tighter leading-[0.9] mb-4">{{ title }}</component>
|
|
8
|
+
<p v-if="description" class="text-sm leading-relaxed mb-6" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
9
|
+
<div v-if="ctas && ctas.length" class="flex flex-wrap gap-3">
|
|
10
|
+
<NuxtLink v-for="(cta, i) in ctas" :key="i" :to="cta.link || '#'"
|
|
11
|
+
class="inline-flex items-center gap-2.5 px-6 py-3 rounded-full text-[11px] font-black uppercase tracking-widest transition-all hover:gap-5"
|
|
12
|
+
:style="i === 0 ? { backgroundColor: textColor, color: bgColor } : { border: `1px solid ${textColor}40`, color: textColor }">
|
|
13
|
+
{{ cta.text }}<span>→</span>
|
|
14
|
+
</NuxtLink>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- GRID -->
|
|
19
|
+
<div v-if="layout === 'grid'" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
20
|
+
<div v-for="(service, idx) in items" :key="idx"
|
|
21
|
+
class="group relative p-8 rounded-3xl border transition-all duration-300 overflow-hidden hover:-translate-y-1 hover:shadow-lg"
|
|
22
|
+
:style="{ backgroundColor: textColor + '04', borderColor: textColor + '12' }">
|
|
23
|
+
<span class="absolute -top-6 -right-2 text-[100px] font-black italic opacity-[0.04] select-none">{{ String(idx+1).padStart(2,'0') }}</span>
|
|
24
|
+
<img v-if="service.image" :src="service.image" class="w-10 h-10 object-contain mb-5 rounded-xl" />
|
|
25
|
+
<component :is="itemTag" class="text-lg font-black uppercase tracking-tight mb-3 leading-tight">{{ service.title }}</component>
|
|
26
|
+
<p class="text-sm leading-relaxed mb-4" :style="{ opacity: 0.5 }">{{ service.description }}</p>
|
|
27
|
+
<p v-if="service.price" class="text-2xl font-black tracking-tight mb-4">{{ service.price }}</p>
|
|
28
|
+
<NuxtLink v-if="service.ctaText || service.link" :to="service.link || '#'" class="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-widest transition-all hover:gap-4" :style="{ color: textColor }">{{ service.ctaText || 'En savoir +' }}<span>→</span></NuxtLink>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- LIST -->
|
|
33
|
+
<div v-else-if="layout === 'list'" :style="{ borderColor: textColor + '12' }">
|
|
34
|
+
<div v-for="(service, idx) in items" :key="idx" class="group flex items-start gap-8 py-8 border-b last:border-b-0" :style="{ borderColor: textColor + '10' }">
|
|
35
|
+
<span class="text-[11px] font-black opacity-20 pt-1 w-6 shrink-0">{{ String(idx+1).padStart(2,'0') }}</span>
|
|
36
|
+
<img v-if="service.image" :src="service.image" class="w-12 h-12 object-contain rounded-xl shrink-0" />
|
|
37
|
+
<div class="flex-1 min-w-0">
|
|
38
|
+
<component :is="itemTag" class="text-xl font-black uppercase tracking-tight mb-2">{{ service.title }}</component>
|
|
39
|
+
<p class="text-sm leading-relaxed" :style="{ opacity: 0.5 }">{{ service.description }}</p>
|
|
40
|
+
<p v-if="service.price" class="text-xl font-black tracking-tight mt-3">{{ service.price }}</p>
|
|
41
|
+
</div>
|
|
42
|
+
<NuxtLink v-if="service.ctaText || service.link" :to="service.link || '#'" class="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-widest shrink-0 mt-1 transition-all hover:gap-4" :style="{ color: textColor }">{{ service.ctaText || '→' }}</NuxtLink>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- CARDS -->
|
|
47
|
+
<div v-else-if="layout === 'cards'" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
48
|
+
<div v-for="(service, idx) in items" :key="idx"
|
|
49
|
+
class="flex gap-6 p-7 rounded-3xl border transition-all hover:shadow-md"
|
|
50
|
+
:style="{ backgroundColor: textColor + '03', borderColor: textColor + '10' }">
|
|
51
|
+
<div class="w-12 h-12 rounded-2xl flex items-center justify-center shrink-0" :style="{ backgroundColor: textColor + '10' }">
|
|
52
|
+
<img v-if="service.image" :src="service.image" class="w-7 h-7 object-contain" />
|
|
53
|
+
<span v-else class="text-lg font-black opacity-30">{{ String(idx+1).padStart(2,'0') }}</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<component :is="itemTag" class="text-base font-black uppercase tracking-tight mb-2 leading-tight">{{ service.title }}</component>
|
|
57
|
+
<p class="text-sm leading-relaxed" :style="{ opacity: 0.5 }">{{ service.description }}</p>
|
|
58
|
+
<p v-if="service.price" class="text-xl font-black mt-3">{{ service.price }}</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- MINIMAL -->
|
|
64
|
+
<div v-else class="max-w-2xl">
|
|
65
|
+
<div v-for="(service, idx) in items" :key="idx" class="flex items-start justify-between gap-6 py-6 border-b last:border-b-0" :style="{ borderColor: textColor + '10' }">
|
|
66
|
+
<div>
|
|
67
|
+
<component :is="itemTag" class="font-black uppercase tracking-tight mb-1">{{ service.title }}</component>
|
|
68
|
+
<p v-if="service.description" class="text-sm" :style="{ opacity: 0.5 }">{{ service.description }}</p>
|
|
69
|
+
</div>
|
|
70
|
+
<p v-if="service.price" class="text-xl font-black tracking-tighter shrink-0">{{ service.price }}</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup>
|
|
79
|
+
defineProps({
|
|
80
|
+
layout: { type: String, default: "grid" },
|
|
81
|
+
title: { type: String, default: "Our Services" },
|
|
82
|
+
titleTag: { type: String, default: "h2" },
|
|
83
|
+
badge: { type: String, default: "" },
|
|
84
|
+
description: { type: String, default: "" },
|
|
85
|
+
headerAlign: { type: String, default: "left" },
|
|
86
|
+
itemTag: { type: String, default: "h3" },
|
|
87
|
+
items: { type: Array, default: () => [] },
|
|
88
|
+
ctas: { type: Array, default: () => [] },
|
|
89
|
+
bgColor: { type: String, default: "#ffffff" },
|
|
90
|
+
textColor: { type: String, default: "#000000" }
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="py-14 px-6 md:px-12" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
+
<div class="max-w-6xl mx-auto">
|
|
4
|
+
|
|
5
|
+
<div v-if="title || description" class="mb-12 max-w-xl">
|
|
6
|
+
<span v-if="badge" class="inline-block text-[10px] font-bold tracking-[0.5em] uppercase px-3 py-1 rounded-full border mb-4" :style="{ borderColor: textColor + '25', color: textColor + 'aa' }">{{ badge }}</span>
|
|
7
|
+
<component :is="titleTag" class="text-3xl md:text-5xl font-black uppercase tracking-tighter leading-[0.9] mb-3">{{ title }}</component>
|
|
8
|
+
<p v-if="description" class="text-sm leading-relaxed" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- GRID -->
|
|
12
|
+
<div v-if="layout === 'grid'" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
13
|
+
<div v-for="(item, i) in items" :key="i" class="flex flex-col p-7 rounded-3xl border transition-all hover:-translate-y-1" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '10' }">
|
|
14
|
+
<div v-if="item.rating" class="flex gap-0.5 mb-4">
|
|
15
|
+
<span v-for="s in 5" :key="s" class="text-xs" :style="{ opacity: s <= item.rating ? 1 : 0.15 }">★</span>
|
|
16
|
+
</div>
|
|
17
|
+
<p class="text-sm leading-relaxed flex-1 italic mb-6" :style="{ opacity: 0.7 }">"{{ item.quote }}"</p>
|
|
18
|
+
<div class="flex items-center gap-3 pt-4 border-t" :style="{ borderColor: textColor + '10' }">
|
|
19
|
+
<img v-if="item.avatar" :src="item.avatar" class="w-9 h-9 rounded-full object-cover shrink-0" />
|
|
20
|
+
<div v-else class="w-9 h-9 rounded-full flex items-center justify-center text-[11px] font-black shrink-0" :style="{ backgroundColor: textColor + '15' }">{{ (item.name || '?')[0] }}</div>
|
|
21
|
+
<div>
|
|
22
|
+
<p class="text-[11px] font-black uppercase tracking-wide leading-none">{{ item.name }}</p>
|
|
23
|
+
<p v-if="item.role || item.company" class="text-[9px] mt-0.5" :style="{ opacity: 0.4 }">{{ [item.role, item.company].filter(Boolean).join(' · ') }}</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- FEATURED -->
|
|
30
|
+
<div v-else-if="layout === 'featured'">
|
|
31
|
+
<div v-if="items[0]" class="p-10 rounded-[32px] border mb-6" :style="{ backgroundColor: textColor + '05', borderColor: textColor + '10' }">
|
|
32
|
+
<div v-if="items[0].rating" class="flex gap-0.5 mb-6">
|
|
33
|
+
<span v-for="s in 5" :key="s" class="text-base" :style="{ opacity: s <= items[0].rating ? 1 : 0.15 }">★</span>
|
|
34
|
+
</div>
|
|
35
|
+
<p class="text-xl md:text-3xl font-bold italic leading-snug mb-8 max-w-3xl" :style="{ opacity: 0.8 }">"{{ items[0].quote }}"</p>
|
|
36
|
+
<div class="flex items-center gap-4">
|
|
37
|
+
<img v-if="items[0].avatar" :src="items[0].avatar" class="w-12 h-12 rounded-full object-cover" />
|
|
38
|
+
<div v-else class="w-12 h-12 rounded-full flex items-center justify-center text-sm font-black" :style="{ backgroundColor: textColor + '15' }">{{ (items[0].name || '?')[0] }}</div>
|
|
39
|
+
<div>
|
|
40
|
+
<p class="text-sm font-black uppercase tracking-wide">{{ items[0].name }}</p>
|
|
41
|
+
<p v-if="items[0].role || items[0].company" class="text-[10px]" :style="{ opacity: 0.4 }">{{ [items[0].role, items[0].company].filter(Boolean).join(' · ') }}</p>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
46
|
+
<div v-for="(item, i) in items.slice(1)" :key="i" class="p-7 rounded-3xl border" :style="{ backgroundColor: textColor + '03', borderColor: textColor + '08' }">
|
|
47
|
+
<p class="text-sm italic leading-relaxed mb-5" :style="{ opacity: 0.7 }">"{{ item.quote }}"</p>
|
|
48
|
+
<div class="flex items-center gap-3">
|
|
49
|
+
<img v-if="item.avatar" :src="item.avatar" class="w-8 h-8 rounded-full object-cover" />
|
|
50
|
+
<div v-else class="w-8 h-8 rounded-full flex items-center justify-center text-[10px] font-black" :style="{ backgroundColor: textColor + '12' }">{{ (item.name || '?')[0] }}</div>
|
|
51
|
+
<div>
|
|
52
|
+
<p class="text-[10px] font-black uppercase">{{ item.name }}</p>
|
|
53
|
+
<p v-if="item.role" class="text-[9px]" :style="{ opacity: 0.4 }">{{ item.role }}</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- WALL / MINIMAL -->
|
|
61
|
+
<div v-else class="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6">
|
|
62
|
+
<div v-for="(item, i) in items" :key="i" class="break-inside-avoid p-6 rounded-2xl border mb-6" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '08' }">
|
|
63
|
+
<div v-if="item.rating" class="flex gap-0.5 mb-3">
|
|
64
|
+
<span v-for="s in 5" :key="s" class="text-[10px]" :style="{ opacity: s <= item.rating ? 1 : 0.15 }">★</span>
|
|
65
|
+
</div>
|
|
66
|
+
<p class="text-sm italic leading-relaxed mb-4" :style="{ opacity: 0.7 }">"{{ item.quote }}"</p>
|
|
67
|
+
<div class="flex items-center gap-2.5">
|
|
68
|
+
<img v-if="item.avatar" :src="item.avatar" class="w-7 h-7 rounded-full object-cover" />
|
|
69
|
+
<div v-else class="w-7 h-7 rounded-full flex items-center justify-center text-[9px] font-black" :style="{ backgroundColor: textColor + '15' }">{{ (item.name || '?')[0] }}</div>
|
|
70
|
+
<p class="text-[10px] font-black uppercase">{{ item.name }}</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script setup>
|
|
80
|
+
defineProps({
|
|
81
|
+
layout: { type: String, default: "grid" },
|
|
82
|
+
title: { type: String, default: "What our clients say" },
|
|
83
|
+
titleTag: { type: String, default: "h2" },
|
|
84
|
+
badge: { type: String, default: "" },
|
|
85
|
+
description: { type: String, default: "" },
|
|
86
|
+
items: { type: Array, default: () => [] },
|
|
87
|
+
bgColor: { type: String, default: "#ffffff" },
|
|
88
|
+
textColor: { type: String, default: "#000000" }
|
|
89
|
+
});
|
|
90
|
+
</script>
|