cinqcinqdev-seo 0.1.44 → 0.1.45
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/sections/BlogHero.vue +16 -10
- package/dist/runtime/components/sections/CallToAction.vue +17 -11
- package/dist/runtime/components/sections/Comparison.d.vue.ts +3 -0
- package/dist/runtime/components/sections/Comparison.vue +264 -0
- package/dist/runtime/components/sections/Comparison.vue.d.ts +3 -0
- package/dist/runtime/components/sections/ContactForm.vue +11 -5
- package/dist/runtime/components/sections/FAQ.vue +11 -5
- package/dist/runtime/components/sections/Features.vue +11 -5
- package/dist/runtime/components/sections/HeroSection.vue +19 -14
- package/dist/runtime/components/sections/Newsletter.vue +17 -11
- package/dist/runtime/components/sections/Pricing.vue +11 -5
- package/dist/runtime/components/sections/Process.vue +11 -5
- package/dist/runtime/components/sections/Quote.vue +13 -8
- package/dist/runtime/components/sections/RichText.vue +11 -4
- package/dist/runtime/components/sections/ServiceGrid.vue +11 -5
- package/dist/runtime/components/sections/Testimonials.vue +11 -5
- package/dist/runtime/components/sections/TextVisual.vue +17 -11
- package/dist/runtime/composables/useAdminSections.js +187 -27
- package/package.json +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section :class="[sectionPad]" :dir="textDirection" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
+
<div :class="[containerClass]">
|
|
4
|
+
|
|
5
|
+
<!-- Header -->
|
|
6
|
+
<div v-if="badge || title || description" :class="['mb-14', headerAlign === 'center' ? 'text-center' : '']">
|
|
7
|
+
<span v-if="badge" class="inline-block text-[10px] font-black uppercase tracking-[0.4em] px-3 py-1 rounded-full border mb-5" :style="{ borderColor: ca(textColor, '25'), color: ca(textColor, 'aa') }">{{ badge }}</span>
|
|
8
|
+
<component :is="titleTag" v-if="title" class="font-black uppercase tracking-tighter leading-[0.9] mb-4" :style="{ fontSize: titleFontSize }">{{ title }}</component>
|
|
9
|
+
<p v-if="description" :class="[bodyClass, 'leading-relaxed', headerAlign === 'center' ? 'max-w-xl mx-auto' : 'max-w-xl']" :style="{ opacity: 0.6 }">{{ description }}</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- ── CARDS layout ──────────────────────────────────────── -->
|
|
13
|
+
<div v-if="layout === 'cards'" :class="['grid gap-5', gridCols]">
|
|
14
|
+
<div
|
|
15
|
+
v-for="(plan, i) in parsedPlans" :key="i"
|
|
16
|
+
:class="['relative flex flex-col p-8 border transition-all duration-300', cardRound, plan.highlighted ? 'shadow-2xl scale-[1.03] z-10' : '']"
|
|
17
|
+
:style="plan.highlighted
|
|
18
|
+
? { backgroundColor: textColor, color: bgColor, borderColor: 'transparent' }
|
|
19
|
+
: { backgroundColor: ca(textColor, '04'), borderColor: ca(textColor, '0d') }"
|
|
20
|
+
>
|
|
21
|
+
<!-- Popular badge -->
|
|
22
|
+
<span v-if="plan.badge" class="absolute -top-3 left-1/2 -translate-x-1/2 text-[9px] font-black uppercase tracking-[0.3em] px-3 py-1 rounded-full whitespace-nowrap"
|
|
23
|
+
:style="plan.highlighted ? { backgroundColor: bgColor, color: textColor } : { backgroundColor: ca(textColor, '12'), color: textColor }">
|
|
24
|
+
{{ plan.badge }}
|
|
25
|
+
</span>
|
|
26
|
+
<!-- Plan name -->
|
|
27
|
+
<div class="text-[10px] font-black uppercase tracking-[0.4em] mb-5" :style="{ opacity: plan.highlighted ? 0.55 : 0.4 }">{{ plan.name }}</div>
|
|
28
|
+
<!-- Price -->
|
|
29
|
+
<div class="flex items-end gap-1 mb-2">
|
|
30
|
+
<span class="text-5xl font-black tracking-tight leading-none">{{ plan.price }}</span>
|
|
31
|
+
<span class="text-sm pb-1" :style="{ opacity: 0.45 }">{{ plan.priceUnit }}</span>
|
|
32
|
+
</div>
|
|
33
|
+
<!-- Description -->
|
|
34
|
+
<p v-if="plan.description" :class="[bodyClass, 'leading-relaxed mb-8 mt-3']" :style="{ opacity: plan.highlighted ? 0.65 : 0.5 }">{{ plan.description }}</p>
|
|
35
|
+
<!-- Divider -->
|
|
36
|
+
<div class="h-px mb-7" :style="{ backgroundColor: plan.highlighted ? ca(bgColor, '20') : ca(textColor, '0c') }"></div>
|
|
37
|
+
<!-- Features -->
|
|
38
|
+
<ul class="space-y-3.5 flex-1 mb-8">
|
|
39
|
+
<li v-for="(feat, j) in plan.features" :key="j" class="flex items-start gap-3">
|
|
40
|
+
<span class="shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-black leading-none mt-0.5"
|
|
41
|
+
:style="feat.type === 'excluded'
|
|
42
|
+
? { backgroundColor: plan.highlighted ? ca(bgColor, '15') : ca(textColor, '0a'), color: plan.highlighted ? ca(bgColor, '40') : ca(textColor, '30') }
|
|
43
|
+
: { backgroundColor: plan.highlighted ? bgColor : textColor, color: plan.highlighted ? textColor : bgColor }">
|
|
44
|
+
{{ feat.type === 'excluded' ? '✗' : '✓' }}
|
|
45
|
+
</span>
|
|
46
|
+
<span :class="[bodyClass, 'leading-snug']" :style="{ opacity: feat.type === 'excluded' ? 0.3 : (plan.highlighted ? 0.85 : 0.7) }">{{ feat.text }}</span>
|
|
47
|
+
</li>
|
|
48
|
+
</ul>
|
|
49
|
+
<!-- CTA -->
|
|
50
|
+
<NuxtLink v-if="plan.ctaText" :to="plan.ctaLink || '#'"
|
|
51
|
+
:class="['flex items-center justify-center gap-2 py-3.5 text-[11px] font-black uppercase tracking-widest transition-all hover:gap-4 active:scale-95', cardRound]"
|
|
52
|
+
:style="plan.highlighted
|
|
53
|
+
? { backgroundColor: bgColor, color: textColor }
|
|
54
|
+
: { border: `1px solid ${ca(textColor, '20')}`, color: textColor }">
|
|
55
|
+
{{ plan.ctaText }}<span>→</span>
|
|
56
|
+
</NuxtLink>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- ── TABLE layout ──────────────────────────────────────── -->
|
|
61
|
+
<div v-else-if="layout === 'table'" class="overflow-x-auto -mx-4 px-4">
|
|
62
|
+
<table class="w-full min-w-[540px]" style="border-collapse: separate; border-spacing: 0;">
|
|
63
|
+
<!-- Plan headers -->
|
|
64
|
+
<thead>
|
|
65
|
+
<tr>
|
|
66
|
+
<th class="text-left pb-8 pr-6 align-bottom" style="width: 30%">
|
|
67
|
+
<span v-if="rowsLabel" :class="[bodyClass, 'font-black uppercase tracking-wider']" :style="{ opacity: 0.35 }">{{ rowsLabel }}</span>
|
|
68
|
+
</th>
|
|
69
|
+
<th v-for="(plan, i) in parsedPlans" :key="i" class="pb-8 px-4 align-bottom" :style="{ width: `${70 / Math.max(parsedPlans.length, 1)}%` }">
|
|
70
|
+
<div :class="['px-5 py-5 transition-all', plan.highlighted ? cardRound : 'rounded-2xl']"
|
|
71
|
+
:style="plan.highlighted
|
|
72
|
+
? { backgroundColor: textColor, color: bgColor }
|
|
73
|
+
: { backgroundColor: ca(textColor, '04') }">
|
|
74
|
+
<span v-if="plan.badge" class="block text-[9px] font-black uppercase tracking-[0.35em] mb-2" :style="{ opacity: plan.highlighted ? 0.6 : 0.4 }">{{ plan.badge }}</span>
|
|
75
|
+
<div class="font-black uppercase tracking-tight text-sm mb-2">{{ plan.name }}</div>
|
|
76
|
+
<div class="flex items-end gap-1">
|
|
77
|
+
<span class="text-2xl font-black leading-none">{{ plan.price }}</span>
|
|
78
|
+
<span class="text-xs pb-0.5" :style="{ opacity: 0.45 }">{{ plan.priceUnit }}</span>
|
|
79
|
+
</div>
|
|
80
|
+
<NuxtLink v-if="plan.ctaText" :to="plan.ctaLink || '#'"
|
|
81
|
+
class="mt-4 flex items-center justify-center gap-1.5 px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all hover:gap-3"
|
|
82
|
+
:style="plan.highlighted
|
|
83
|
+
? { backgroundColor: bgColor, color: textColor }
|
|
84
|
+
: { border: `1px solid ${ca(textColor, '18')}`, color: textColor }">
|
|
85
|
+
{{ plan.ctaText }}
|
|
86
|
+
</NuxtLink>
|
|
87
|
+
</div>
|
|
88
|
+
</th>
|
|
89
|
+
</tr>
|
|
90
|
+
</thead>
|
|
91
|
+
<!-- Feature rows -->
|
|
92
|
+
<tbody>
|
|
93
|
+
<tr v-for="(row, ri) in parsedRows" :key="ri"
|
|
94
|
+
:class="[ri % 2 === 0 ? '' : '']"
|
|
95
|
+
:style="{ borderTop: `1px solid ${ca(textColor, '07')}` }">
|
|
96
|
+
<td class="py-4 pr-6">
|
|
97
|
+
<span :class="[bodyClass, 'font-medium']" :style="{ opacity: 0.7 }">{{ row }}</span>
|
|
98
|
+
</td>
|
|
99
|
+
<td v-for="(plan, pi) in parsedPlans" :key="pi" class="py-4 px-4 text-center">
|
|
100
|
+
<div class="flex items-center justify-center">
|
|
101
|
+
<template v-if="plan.features[ri]">
|
|
102
|
+
<span v-if="plan.features[ri].type === 'included'"
|
|
103
|
+
class="inline-flex items-center justify-center w-6 h-6 rounded-full text-[10px] font-black"
|
|
104
|
+
:style="{ backgroundColor: textColor, color: bgColor }">✓</span>
|
|
105
|
+
<span v-else-if="plan.features[ri].type === 'excluded'"
|
|
106
|
+
class="inline-flex items-center justify-center w-6 h-6 rounded-full text-[10px]"
|
|
107
|
+
:style="{ backgroundColor: ca(textColor, '08'), color: ca(textColor, '30') }">✗</span>
|
|
108
|
+
<span v-else :class="[bodyClass, 'font-semibold']" :style="{ opacity: 0.8 }">{{ plan.features[ri].text }}</span>
|
|
109
|
+
</template>
|
|
110
|
+
<span v-else class="text-xs" :style="{ opacity: 0.15 }">—</span>
|
|
111
|
+
</div>
|
|
112
|
+
</td>
|
|
113
|
+
</tr>
|
|
114
|
+
</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<!-- ── VERSUS layout ─────────────────────────────────────── -->
|
|
119
|
+
<div v-else class="relative">
|
|
120
|
+
<div :class="['grid grid-cols-1 md:grid-cols-2 overflow-hidden border', cardRound]" :style="{ borderColor: ca(textColor, '10') }">
|
|
121
|
+
<div v-for="(plan, i) in parsedPlans.slice(0, 2)" :key="i"
|
|
122
|
+
class="flex flex-col p-8 md:p-12"
|
|
123
|
+
:style="i === 1 ? { backgroundColor: ca(textColor, '04'), borderLeft: `1px solid ${ca(textColor, '08')}` } : {}">
|
|
124
|
+
<!-- Badge -->
|
|
125
|
+
<span v-if="plan.badge" class="inline-block self-start text-[9px] font-black uppercase tracking-[0.3em] px-2.5 py-1 rounded-full mb-5"
|
|
126
|
+
:style="{ backgroundColor: ca(textColor, '10'), color: textColor }">{{ plan.badge }}</span>
|
|
127
|
+
<!-- Name -->
|
|
128
|
+
<div class="text-[10px] font-black uppercase tracking-[0.4em] mb-3" :style="{ opacity: 0.4 }">{{ plan.name }}</div>
|
|
129
|
+
<!-- Price -->
|
|
130
|
+
<div class="flex items-end gap-1 mb-3">
|
|
131
|
+
<span class="text-4xl font-black tracking-tight leading-none">{{ plan.price }}</span>
|
|
132
|
+
<span class="text-sm pb-0.5" :style="{ opacity: 0.45 }">{{ plan.priceUnit }}</span>
|
|
133
|
+
</div>
|
|
134
|
+
<!-- Description -->
|
|
135
|
+
<p v-if="plan.description" :class="[bodyClass, 'leading-relaxed mb-8 mt-1']" :style="{ opacity: 0.55 }">{{ plan.description }}</p>
|
|
136
|
+
<!-- Divider -->
|
|
137
|
+
<div class="h-px mb-7" :style="{ backgroundColor: ca(textColor, '0a') }"></div>
|
|
138
|
+
<!-- Features -->
|
|
139
|
+
<ul class="space-y-3.5 flex-1 mb-10">
|
|
140
|
+
<li v-for="(feat, j) in plan.features" :key="j" class="flex items-start gap-3">
|
|
141
|
+
<span class="shrink-0 w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-black leading-none mt-0.5"
|
|
142
|
+
:style="feat.type === 'excluded'
|
|
143
|
+
? { backgroundColor: ca(textColor, '0a'), color: ca(textColor, '30') }
|
|
144
|
+
: { backgroundColor: textColor, color: bgColor }">
|
|
145
|
+
{{ feat.type === 'excluded' ? '✗' : '✓' }}
|
|
146
|
+
</span>
|
|
147
|
+
<span :class="[bodyClass, 'leading-snug']" :style="{ opacity: feat.type === 'excluded' ? 0.3 : 0.7 }">{{ feat.text }}</span>
|
|
148
|
+
</li>
|
|
149
|
+
</ul>
|
|
150
|
+
<!-- CTA -->
|
|
151
|
+
<NuxtLink v-if="plan.ctaText" :to="plan.ctaLink || '#'"
|
|
152
|
+
:class="['inline-flex items-center gap-2 px-7 py-3.5 text-[11px] font-black uppercase tracking-widest transition-all hover:gap-4 active:scale-95', cardRound]"
|
|
153
|
+
:style="plan.highlighted
|
|
154
|
+
? { backgroundColor: textColor, color: bgColor }
|
|
155
|
+
: { border: `1px solid ${ca(textColor, '20')}`, color: textColor }">
|
|
156
|
+
{{ plan.ctaText }}<span>→</span>
|
|
157
|
+
</NuxtLink>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<!-- VS badge -->
|
|
161
|
+
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 hidden md:flex items-center justify-center w-12 h-12 rounded-full text-[10px] font-black uppercase tracking-widest border-2 z-10"
|
|
162
|
+
:style="{ backgroundColor: bgColor, borderColor: ca(textColor, '15'), color: textColor }">VS</div>
|
|
163
|
+
<!-- VS divider mobile -->
|
|
164
|
+
<div class="md:hidden py-3 text-center text-[10px] font-black tracking-widest border-t border-b"
|
|
165
|
+
:style="{ borderColor: ca(textColor, '08'), backgroundColor: ca(textColor, '04'), color: ca(textColor, '50') }">VS</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<!-- Empty state placeholder -->
|
|
169
|
+
<div v-if="!parsedPlans.length" class="py-16 text-center" :style="{ opacity: 0.2 }">
|
|
170
|
+
<div class="text-4xl font-black mb-2">≡</div>
|
|
171
|
+
<div class="text-xs font-black uppercase tracking-widest">Add plans to compare</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
</div>
|
|
175
|
+
</section>
|
|
176
|
+
</template>
|
|
177
|
+
|
|
178
|
+
<script setup>
|
|
179
|
+
const ca = (c, h) => {
|
|
180
|
+
const m = c.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
|
|
181
|
+
return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : c + h;
|
|
182
|
+
};
|
|
183
|
+
import { computed } from "vue";
|
|
184
|
+
const parseFeatureLine = (line) => {
|
|
185
|
+
const t = line.trim();
|
|
186
|
+
if (!t) return null;
|
|
187
|
+
if (t.startsWith("\u2713") || t.toLowerCase().startsWith("yes") || t.startsWith("+")) return { type: "included", text: t.replace(/^[✓+]|^yes/i, "").trim() || t };
|
|
188
|
+
if (t.startsWith("\u2717") || t.toLowerCase().startsWith("no") || t.startsWith("-")) return { type: "excluded", text: t.replace(/^[✗\-]|^no/i, "").trim() || t };
|
|
189
|
+
return { type: "text", text: t };
|
|
190
|
+
};
|
|
191
|
+
const props = defineProps({
|
|
192
|
+
layout: { type: String, default: "cards" },
|
|
193
|
+
headerAlign: { type: String, default: "center" },
|
|
194
|
+
titleTag: { type: String, default: "h2" },
|
|
195
|
+
badge: { type: String, default: "" },
|
|
196
|
+
title: { type: String, default: "" },
|
|
197
|
+
description: { type: String, default: "" },
|
|
198
|
+
rowsLabel: { type: String, default: "Features" },
|
|
199
|
+
rows: { type: String, default: "Projects\nStorage\nSupport\nAPI Access\nCustom Domain" },
|
|
200
|
+
plans: { type: Array, default: () => [
|
|
201
|
+
{
|
|
202
|
+
name: "Starter",
|
|
203
|
+
badge: "",
|
|
204
|
+
price: "$0",
|
|
205
|
+
priceUnit: "/mo",
|
|
206
|
+
description: "Perfect for individuals and small projects.",
|
|
207
|
+
highlighted: false,
|
|
208
|
+
ctaText: "Get started",
|
|
209
|
+
ctaLink: "#",
|
|
210
|
+
features: "3 projects\n1 GB storage\nCommunity support\n\u2717 API access\n\u2717 Custom domain"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "Pro",
|
|
214
|
+
badge: "Popular",
|
|
215
|
+
price: "$29",
|
|
216
|
+
priceUnit: "/mo",
|
|
217
|
+
description: "Everything you need to scale your business.",
|
|
218
|
+
highlighted: true,
|
|
219
|
+
ctaText: "Start free trial",
|
|
220
|
+
ctaLink: "#",
|
|
221
|
+
features: "Unlimited projects\n50 GB storage\nPriority support\n\u2713 API access\n\u2713 Custom domain"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "Enterprise",
|
|
225
|
+
badge: "",
|
|
226
|
+
price: "$99",
|
|
227
|
+
priceUnit: "/mo",
|
|
228
|
+
description: "Advanced features for large organizations.",
|
|
229
|
+
highlighted: false,
|
|
230
|
+
ctaText: "Contact sales",
|
|
231
|
+
ctaLink: "#",
|
|
232
|
+
features: "Unlimited projects\nUnlimited storage\nDedicated support\n\u2713 API access\n\u2713 Custom domain"
|
|
233
|
+
}
|
|
234
|
+
] },
|
|
235
|
+
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
236
|
+
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
237
|
+
spacing: { type: String, default: "normal" },
|
|
238
|
+
radius: { type: String, default: "rounded" },
|
|
239
|
+
textDirection: { type: String, default: "ltr" },
|
|
240
|
+
contentWidth: { type: String, default: "normal" },
|
|
241
|
+
textSize: { type: String, default: "md" }
|
|
242
|
+
});
|
|
243
|
+
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
244
|
+
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
|
|
245
|
+
const containerClass = computed(() => ({ narrow: "max-w-3xl mx-auto", normal: "max-w-6xl mx-auto", wide: "max-w-7xl mx-auto", full: "max-w-full" })[props.contentWidth] || "max-w-6xl mx-auto");
|
|
246
|
+
const titleFontSize = computed(() => ({ sm: "clamp(1.5rem,3vw,2.5rem)", md: "clamp(2rem,4vw,3.5rem)", lg: "clamp(2.5rem,6vw,5rem)" })[props.textSize] || "clamp(2rem,4vw,3.5rem)");
|
|
247
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
248
|
+
const parsedPlans = computed(
|
|
249
|
+
() => (props.plans || []).map((plan) => ({
|
|
250
|
+
...plan,
|
|
251
|
+
features: (plan.features || "").split("\n").map(parseFeatureLine).filter(Boolean)
|
|
252
|
+
}))
|
|
253
|
+
);
|
|
254
|
+
const parsedRows = computed(
|
|
255
|
+
() => (props.rows || "").split("\n").map((r) => r.trim()).filter(Boolean)
|
|
256
|
+
);
|
|
257
|
+
const gridCols = computed(() => {
|
|
258
|
+
const n = parsedPlans.value.length;
|
|
259
|
+
if (n === 1) return "grid-cols-1 max-w-sm mx-auto";
|
|
260
|
+
if (n === 2) return "grid-cols-1 md:grid-cols-2";
|
|
261
|
+
if (n === 4) return "grid-cols-1 sm:grid-cols-2 xl:grid-cols-4";
|
|
262
|
+
return "grid-cols-1 md:grid-cols-3";
|
|
263
|
+
});
|
|
264
|
+
</script>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
-
<div class="
|
|
2
|
+
<section :class="[sectionPad]" :dir="textDirection" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
+
<div :class="[containerClass]">
|
|
4
4
|
|
|
5
5
|
<!-- SPLIT layout -->
|
|
6
6
|
<div v-if="layout === 'split'" class="grid grid-cols-1 md:grid-cols-2 gap-16 items-start">
|
|
7
7
|
<div>
|
|
8
8
|
<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: ca(textColor, '25'), color: ca(textColor, 'aa') }">{{ badge }}</span>
|
|
9
|
-
<component :is="titleTag" class="
|
|
9
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.9] mb-4" :style="{ fontSize: titleFontSize }">{{ title }}</component>
|
|
10
10
|
<p v-if="description" class="text-sm leading-relaxed mb-8" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
11
11
|
<div v-if="infos && infos.length" class="space-y-4">
|
|
12
12
|
<div v-for="(info, i) in infos" :key="i" class="flex items-start gap-3">
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<!-- CENTERED layout -->
|
|
45
45
|
<div v-else-if="layout === 'centered'" class="max-w-xl mx-auto text-center">
|
|
46
46
|
<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: ca(textColor, '25'), color: ca(textColor, 'aa') }">{{ badge }}</span>
|
|
47
|
-
<component :is="titleTag" class="
|
|
47
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.9] mb-4" :style="{ fontSize: titleFontSize }">{{ title }}</component>
|
|
48
48
|
<p v-if="description" class="text-sm leading-relaxed mb-10" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
49
49
|
<form @submit.prevent="handleSubmit" class="space-y-4 text-left">
|
|
50
50
|
<div v-for="field in fields" :key="field.name" class="flex flex-col gap-1.5">
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
<!-- MINIMAL layout (default) -->
|
|
69
69
|
<div v-else class="max-w-2xl">
|
|
70
70
|
<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: ca(textColor, '25'), color: ca(textColor, 'aa') }">{{ badge }}</span>
|
|
71
|
-
<component :is="titleTag" class="
|
|
71
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.9] mb-4" :style="{ fontSize: titleFontSize }">{{ title }}</component>
|
|
72
72
|
<p v-if="description" class="text-sm leading-relaxed mb-10" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
73
73
|
<form @submit.prevent="handleSubmit" class="space-y-6">
|
|
74
74
|
<div v-for="field in fields" :key="field.name" class="flex flex-col gap-2 border-b pb-6" :style="{ borderColor: ca(textColor, '10') }">
|
|
@@ -109,6 +109,9 @@ const props = defineProps({
|
|
|
109
109
|
description: { type: String, default: "" },
|
|
110
110
|
ctaText: { type: String, default: "Envoyer" },
|
|
111
111
|
successMessage: { type: String, default: "Message envoy\xE9 !" },
|
|
112
|
+
textDirection: { type: String, default: "ltr" },
|
|
113
|
+
contentWidth: { type: String, default: "normal" },
|
|
114
|
+
textSize: { type: String, default: "md" },
|
|
112
115
|
fields: {
|
|
113
116
|
type: Array,
|
|
114
117
|
default: () => [
|
|
@@ -129,6 +132,9 @@ const sent = ref(false);
|
|
|
129
132
|
const formData = reactive({});
|
|
130
133
|
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
131
134
|
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
|
|
135
|
+
const containerClass = computed(() => ({ narrow: "max-w-3xl mx-auto", normal: "max-w-6xl mx-auto", wide: "max-w-7xl mx-auto", full: "max-w-full" })[props.contentWidth] || "max-w-6xl mx-auto");
|
|
136
|
+
const titleFontSize = computed(() => ({ sm: "clamp(1.5rem,3vw,2.5rem)", md: "clamp(2rem,4vw,3.5rem)", lg: "clamp(2.5rem,6vw,5rem)" })[props.textSize] || "clamp(2rem,4vw,3.5rem)");
|
|
137
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
132
138
|
const handleSubmit = async () => {
|
|
133
139
|
submitting.value = true;
|
|
134
140
|
emit("submit", { ...formData });
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
-
<div class="
|
|
2
|
+
<section :class="[sectionPad]" :dir="textDirection" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
+
<div :class="[containerClass]">
|
|
4
4
|
<div class="text-center mb-16">
|
|
5
|
-
<component :is="titleTag" class="
|
|
5
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter italic" :style="{ fontSize: titleFontSize }">{{ title }}</component>
|
|
6
6
|
</div>
|
|
7
7
|
<div class="space-y-3">
|
|
8
8
|
<div v-for="(item, index) in items" :key="index"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<span class="text-2xl font-light shrink-0 transition-transform duration-300" :style="{ transform: openIndex === index ? 'rotate(45deg)' : 'none' }">+</span>
|
|
14
14
|
</button>
|
|
15
15
|
<div v-if="openIndex === index" class="px-7 pb-7">
|
|
16
|
-
<p class="
|
|
16
|
+
<p :class="[bodyClass, 'leading-relaxed']" :style="{ opacity: 0.65 }">{{ item.answer }}</p>
|
|
17
17
|
</div>
|
|
18
18
|
</div>
|
|
19
19
|
<div v-if="!items.length" v-for="n in 3" :key="n"
|
|
@@ -39,10 +39,16 @@ const props = defineProps({
|
|
|
39
39
|
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
40
40
|
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
41
41
|
spacing: { type: String, default: "normal" },
|
|
42
|
-
radius: { type: String, default: "rounded" }
|
|
42
|
+
radius: { type: String, default: "rounded" },
|
|
43
|
+
textDirection: { type: String, default: "ltr" },
|
|
44
|
+
contentWidth: { type: String, default: "narrow" },
|
|
45
|
+
textSize: { type: String, default: "md" }
|
|
43
46
|
});
|
|
44
47
|
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
45
48
|
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-[24px]", pill: "rounded-[36px]" })[props.radius] || "rounded-[24px]");
|
|
49
|
+
const containerClass = computed(() => ({ narrow: "max-w-3xl mx-auto", normal: "max-w-6xl mx-auto", wide: "max-w-7xl mx-auto", full: "max-w-full" })[props.contentWidth] || "max-w-3xl mx-auto");
|
|
50
|
+
const titleFontSize = computed(() => ({ sm: "clamp(1.5rem,3vw,2.5rem)", md: "clamp(2rem,4vw,3.5rem)", lg: "clamp(2.5rem,6vw,5rem)" })[props.textSize] || "clamp(2rem,4vw,3.5rem)");
|
|
51
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
46
52
|
const openIndex = ref(null);
|
|
47
53
|
const toggle = (index) => {
|
|
48
54
|
openIndex.value = openIndex.value === index ? null : index;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section :class="[sectionPad, 'overflow-hidden']" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
-
<div class="
|
|
2
|
+
<section :class="[sectionPad, 'overflow-hidden']" :dir="textDirection" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
+
<div :class="[containerClass]">
|
|
4
4
|
<div class="mb-16 md:mb-20">
|
|
5
|
-
<component :is="titleTag" class="
|
|
5
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-none italic" :style="{ fontSize: titleFontSize }">{{ title }}</component>
|
|
6
6
|
</div>
|
|
7
7
|
<div v-if="items.length" :class="['grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-px overflow-hidden', cardRound]" :style="{ backgroundColor: ca(textColor, '10'), border: `1px solid ${ca(textColor, '10')}` }">
|
|
8
8
|
<div v-for="(item, index) in items" :key="index" class="p-10 transition-all duration-300 hover:brightness-95" :style="{ backgroundColor: bgColor }">
|
|
9
9
|
<div class="flex flex-col h-full">
|
|
10
10
|
<span class="text-[10px] font-black opacity-20 mb-8 tracking-widest">{{ index < 9 ? '0' : '' }}{{ index + 1 }}</span>
|
|
11
11
|
<component :is="itemTag" class="text-xl font-black uppercase tracking-tight mb-4 leading-tight">{{ item.title }}</component>
|
|
12
|
-
<p class="
|
|
12
|
+
<p :class="[bodyClass, 'leading-relaxed']" :style="{ opacity: 0.6 }">{{ item.description || item.text }}</p>
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
15
|
</div>
|
|
@@ -38,8 +38,14 @@ const props = defineProps({
|
|
|
38
38
|
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
39
39
|
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
40
40
|
spacing: { type: String, default: "normal" },
|
|
41
|
-
radius: { type: String, default: "rounded" }
|
|
41
|
+
radius: { type: String, default: "rounded" },
|
|
42
|
+
textDirection: { type: String, default: "ltr" },
|
|
43
|
+
contentWidth: { type: String, default: "normal" },
|
|
44
|
+
textSize: { type: String, default: "md" }
|
|
42
45
|
});
|
|
43
46
|
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
44
47
|
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-[32px]", pill: "rounded-[48px]" })[props.radius] || "rounded-[32px]");
|
|
48
|
+
const containerClass = computed(() => ({ narrow: "max-w-3xl mx-auto", normal: "max-w-6xl mx-auto", wide: "max-w-7xl mx-auto", full: "max-w-full" })[props.contentWidth] || "max-w-6xl mx-auto");
|
|
49
|
+
const titleFontSize = computed(() => ({ sm: "clamp(1.5rem,3vw,2.5rem)", md: "clamp(2rem,5vw,4rem)", lg: "clamp(2.5rem,7vw,6rem)" })[props.textSize] || "clamp(2rem,5vw,4rem)");
|
|
50
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
45
51
|
</script>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="relative overflow-hidden" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
2
|
+
<section class="relative overflow-hidden" :dir="textDirection" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
3
|
|
|
4
4
|
<!-- CENTERED -->
|
|
5
5
|
<div v-if="layout === 'centered'" class="px-6 md:px-12 pt-20 pb-16 text-center">
|
|
6
|
-
<div class="
|
|
6
|
+
<div :class="[containerClass]">
|
|
7
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: ca(textColor, '30'), color: ca(textColor, '80') }">
|
|
8
8
|
<span class="w-1.5 h-1.5 rounded-full inline-block" :style="{ backgroundColor: ca(textColor, '60') }"></span>{{ badge }}
|
|
9
9
|
</span>
|
|
10
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="
|
|
11
|
+
<p :class="[bodyClass, 'leading-relaxed max-w-lg mx-auto mb-10']" :style="{ opacity: 0.5 }">{{ subtitle }}</p>
|
|
12
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
13
|
<div v-if="image" :class="['mt-14 mx-auto aspect-video overflow-hidden max-w-3xl', cardRound]">
|
|
14
14
|
<img :src="image" class="w-full h-full object-cover" />
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
<!-- SPLIT -->
|
|
20
20
|
<div v-else-if="layout === 'split'" class="px-6 md:px-12 py-16">
|
|
21
|
-
<div class="
|
|
21
|
+
<div :class="[containerClass, 'grid md:grid-cols-2 gap-12 md:gap-20 items-center']">
|
|
22
22
|
<div>
|
|
23
23
|
<span v-if="badge" class="inline-block text-[9px] font-black tracking-[0.5em] uppercase mb-6 opacity-35">{{ badge }}</span>
|
|
24
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="
|
|
25
|
+
<p :class="[bodyClass, 'leading-relaxed mb-10 max-w-sm']" :style="{ opacity: 0.5 }">{{ subtitle }}</p>
|
|
26
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
27
|
</div>
|
|
28
28
|
<div :class="['aspect-[4/3] overflow-hidden', cardRound]">
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
|
|
35
35
|
<!-- BENTO -->
|
|
36
36
|
<div v-else-if="layout === 'bento'" class="p-3 md:p-5">
|
|
37
|
-
<div class="
|
|
37
|
+
<div :class="[containerClass, 'grid grid-cols-12 gap-3']">
|
|
38
38
|
<div :class="['col-span-12 md:col-span-7 p-8 md:p-10 border flex flex-col justify-between min-h-[320px]', cardRound]" :style="{ borderColor: ca(textColor, '10'), backgroundColor: ca(textColor, '04') }">
|
|
39
39
|
<span v-if="badge" class="text-[9px] font-black uppercase tracking-[0.4em] opacity-30">{{ badge }}</span>
|
|
40
40
|
<div>
|
|
41
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="
|
|
42
|
+
<p :class="[bodyClass, 'leading-relaxed mb-8 max-w-xs']" :style="{ opacity: 0.45 }">{{ subtitle }}</p>
|
|
43
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
44
|
</div>
|
|
45
45
|
</div>
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
<div v-else class="w-full h-full" :style="{ backgroundColor: ca(textColor, '08') }"></div>
|
|
58
58
|
<div class="absolute inset-0" :style="{ background: `linear-gradient(to top, ${bgColor} 20%, ${ca(bgColor, 'cc')} 40%, ${ca(bgColor, '44')} 65%, transparent 100%)` }"></div>
|
|
59
59
|
</div>
|
|
60
|
-
<div class="relative z-10
|
|
60
|
+
<div :class="[containerClass, 'relative z-10 w-full px-6 md:px-12 pb-14']">
|
|
61
61
|
<span v-if="badge" class="inline-block text-[9px] font-black uppercase tracking-[0.5em] opacity-40 mb-4">{{ badge }}</span>
|
|
62
62
|
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.88] mb-6" :style="{ fontSize: 'clamp(3rem,8vw,7rem)' }">{{ title }}</component>
|
|
63
63
|
<div class="flex flex-col sm:flex-row sm:items-center gap-6 sm:gap-10">
|
|
64
|
-
<p class="
|
|
64
|
+
<p :class="[bodyClass, 'leading-relaxed max-w-xs']" :style="{ opacity: 0.6 }">{{ subtitle }}</p>
|
|
65
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: ca(textColor, '50'), color: textColor }">{{ ctaText }}<span>→</span></NuxtLink>
|
|
66
66
|
</div>
|
|
67
67
|
</div>
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
|
|
70
70
|
<!-- MINIMAL -->
|
|
71
71
|
<div v-else-if="layout === 'minimal'" class="px-6 md:px-12 py-14">
|
|
72
|
-
<div class="
|
|
72
|
+
<div :class="[containerClass]">
|
|
73
73
|
<div class="flex items-center gap-4 mb-10 border-b pb-5" :style="{ borderColor: ca(textColor, '10') }">
|
|
74
74
|
<span v-if="badge" class="text-[9px] font-black uppercase tracking-[0.5em] opacity-30">{{ badge }}</span>
|
|
75
75
|
<div class="flex-1 h-px" :style="{ backgroundColor: ca(textColor, '08') }"></div>
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
<img :src="image" class="w-full h-full object-cover" />
|
|
81
81
|
</div>
|
|
82
82
|
<div class="flex flex-col md:flex-row items-start md:items-end justify-between gap-6 border-t pt-8" :style="{ borderColor: ca(textColor, '10') }">
|
|
83
|
-
<p class="
|
|
83
|
+
<p :class="[bodyClass, 'leading-relaxed max-w-xs']" :style="{ opacity: 0.5 }">{{ subtitle }}</p>
|
|
84
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
85
|
</div>
|
|
86
86
|
</div>
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
|
|
89
89
|
<!-- EDITORIAL -->
|
|
90
90
|
<div v-else class="px-6 md:px-12 py-14">
|
|
91
|
-
<div class="
|
|
91
|
+
<div :class="[containerClass]">
|
|
92
92
|
<div class="flex items-center gap-4 mb-8">
|
|
93
93
|
<div class="w-8 h-px" :style="{ backgroundColor: ca(textColor, '30') }"></div>
|
|
94
94
|
<span class="text-[9px] font-black uppercase tracking-[0.5em] opacity-30">{{ badge || '—' }}</span>
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
</div>
|
|
101
101
|
</div>
|
|
102
102
|
<div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-6 border-t pt-8" :style="{ borderColor: ca(textColor, '10') }">
|
|
103
|
-
<p class="
|
|
103
|
+
<p :class="[bodyClass, 'leading-relaxed max-w-sm']" :style="{ opacity: 0.5 }">{{ subtitle }}</p>
|
|
104
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
105
|
</div>
|
|
106
106
|
</div>
|
|
@@ -131,7 +131,12 @@ const props = defineProps({
|
|
|
131
131
|
bgColor: { type: String, default: "rgba(var(--primary-color), 1)" },
|
|
132
132
|
textColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
133
133
|
spacing: { type: String, default: "normal" },
|
|
134
|
-
radius: { type: String, default: "rounded" }
|
|
134
|
+
radius: { type: String, default: "rounded" },
|
|
135
|
+
textDirection: { type: String, default: "ltr" },
|
|
136
|
+
contentWidth: { type: String, default: "normal" },
|
|
137
|
+
textSize: { type: String, default: "md" }
|
|
135
138
|
});
|
|
136
139
|
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
|
|
140
|
+
const containerClass = computed(() => ({ narrow: "max-w-3xl mx-auto", normal: "max-w-6xl mx-auto", wide: "max-w-7xl mx-auto", full: "max-w-full" })[props.contentWidth] || "max-w-6xl mx-auto");
|
|
141
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
137
142
|
</script>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
2
|
+
<section :class="[sectionPad]" :dir="textDirection" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
3
|
|
|
4
4
|
<!-- CENTERED -->
|
|
5
|
-
<div v-if="layout === 'centered'" class="
|
|
5
|
+
<div v-if="layout === 'centered'" :class="[containerClass, 'text-center']">
|
|
6
6
|
<span v-if="badge" class="inline-block text-[10px] font-black uppercase tracking-[0.4em] px-3 py-1 rounded-full border mb-6" :style="{ borderColor: ca(textColor, '25'), color: ca(textColor, 'aa') }">{{ badge }}</span>
|
|
7
|
-
<h2 class="font-black uppercase tracking-tighter leading-[0.9] mb-3" :style="{ fontSize:
|
|
8
|
-
<p v-if="description" class="
|
|
7
|
+
<h2 class="font-black uppercase tracking-tighter leading-[0.9] mb-3" :style="{ fontSize: titleFontSize }">{{ title }}</h2>
|
|
8
|
+
<p v-if="description" :class="[bodyClass, 'leading-relaxed mb-8']" :style="{ opacity: 0.6 }">{{ description }}</p>
|
|
9
9
|
<div class="flex gap-2 max-w-sm mx-auto">
|
|
10
10
|
<input type="email" :placeholder="placeholder || 'your@email.com'" class="flex-1 px-4 py-3 text-sm outline-none rounded-xl border" :style="{ backgroundColor: ca(textColor, '06'), borderColor: ca(textColor, '15'), color: textColor }" />
|
|
11
11
|
<button :class="['px-5 py-3 text-[11px] font-black uppercase tracking-wider shrink-0', cardRound]" :style="{ backgroundColor: textColor, color: bgColor }">{{ buttonText || 'Subscribe' }}</button>
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
16
|
<!-- SPLIT -->
|
|
17
|
-
<div v-else-if="layout === 'split'" :class="['grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-16 items-center
|
|
17
|
+
<div v-else-if="layout === 'split'" :class="[containerClass, 'grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-16 items-center']">
|
|
18
18
|
<div>
|
|
19
19
|
<span v-if="badge" class="inline-block text-[10px] font-black uppercase tracking-[0.4em] mb-4" :style="{ opacity: 0.5 }">{{ badge }}</span>
|
|
20
|
-
<h2 class="font-black uppercase tracking-tighter leading-[0.9] mb-3" :style="{ fontSize:
|
|
21
|
-
<p v-if="description" class="
|
|
20
|
+
<h2 class="font-black uppercase tracking-tighter leading-[0.9] mb-3" :style="{ fontSize: titleFontSize }">{{ title }}</h2>
|
|
21
|
+
<p v-if="description" :class="[bodyClass, 'leading-relaxed']" :style="{ opacity: 0.6 }">{{ description }}</p>
|
|
22
22
|
</div>
|
|
23
23
|
<div>
|
|
24
24
|
<div class="flex gap-2">
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
</div>
|
|
31
31
|
|
|
32
32
|
<!-- MINIMAL -->
|
|
33
|
-
<div v-else class="
|
|
33
|
+
<div v-else :class="[containerClass]">
|
|
34
34
|
<div class="flex flex-col md:flex-row items-start md:items-center gap-6 border-t border-b py-10" :style="{ borderColor: ca(textColor, '15') }">
|
|
35
35
|
<div class="flex-1">
|
|
36
|
-
<h2 class="font-black uppercase tracking-tight leading-[0.92] mb-1" :style="{ fontSize:
|
|
37
|
-
<p v-if="description" class="
|
|
36
|
+
<h2 class="font-black uppercase tracking-tight leading-[0.92] mb-1" :style="{ fontSize: titleFontSize }">{{ title }}</h2>
|
|
37
|
+
<p v-if="description" :class="[bodyClass]" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
38
38
|
</div>
|
|
39
39
|
<div class="flex gap-2 w-full md:w-auto">
|
|
40
40
|
<input type="email" :placeholder="placeholder || 'your@email.com'" class="flex-1 md:w-48 px-4 py-2.5 text-sm outline-none rounded-lg border" :style="{ backgroundColor: ca(textColor, '06'), borderColor: ca(textColor, '15'), color: textColor }" />
|
|
@@ -63,8 +63,14 @@ const props = defineProps({
|
|
|
63
63
|
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
64
64
|
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
65
65
|
spacing: { type: String, default: "normal" },
|
|
66
|
-
radius: { type: String, default: "rounded" }
|
|
66
|
+
radius: { type: String, default: "rounded" },
|
|
67
|
+
textDirection: { type: String, default: "ltr" },
|
|
68
|
+
contentWidth: { type: String, default: "normal" },
|
|
69
|
+
textSize: { type: String, default: "md" }
|
|
67
70
|
});
|
|
68
71
|
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
69
72
|
const cardRound = computed(() => ({ sharp: "rounded-lg", rounded: "rounded-xl", pill: "rounded-full" })[props.radius] || "rounded-xl");
|
|
73
|
+
const containerClass = computed(() => ({ narrow: "max-w-3xl mx-auto", normal: "max-w-5xl mx-auto", wide: "max-w-7xl mx-auto", full: "max-w-full" })[props.contentWidth] || "max-w-5xl mx-auto");
|
|
74
|
+
const titleFontSize = computed(() => ({ sm: "clamp(1rem,2vw,1.5rem)", md: "clamp(1.8rem,5vw,3.5rem)", lg: "clamp(2.2rem,6vw,4.5rem)" })[props.textSize] || "clamp(1.8rem,5vw,3.5rem)");
|
|
75
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
70
76
|
</script>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
-
<div class="
|
|
2
|
+
<section :class="[sectionPad]" :dir="textDirection" :style="{ backgroundColor: bgColor, color: textColor }">
|
|
3
|
+
<div :class="[containerClass]">
|
|
4
4
|
|
|
5
5
|
<div v-if="title || description" class="mb-12 text-center max-w-2xl mx-auto">
|
|
6
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: ca(textColor, '25'), color: ca(textColor, 'aa') }">{{ badge }}</span>
|
|
7
|
-
<component :is="titleTag" class="
|
|
8
|
-
<p v-if="description" class="
|
|
7
|
+
<component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.9] mb-4" :style="{ fontSize: titleFontSize }">{{ title }}</component>
|
|
8
|
+
<p v-if="description" :class="[bodyClass, 'leading-relaxed']" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
11
|
<!-- CARDS -->
|
|
@@ -99,10 +99,16 @@ const props = defineProps({
|
|
|
99
99
|
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
100
100
|
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
101
101
|
spacing: { type: String, default: "normal" },
|
|
102
|
-
radius: { type: String, default: "rounded" }
|
|
102
|
+
radius: { type: String, default: "rounded" },
|
|
103
|
+
textDirection: { type: String, default: "ltr" },
|
|
104
|
+
contentWidth: { type: String, default: "normal" },
|
|
105
|
+
textSize: { type: String, default: "md" }
|
|
103
106
|
});
|
|
104
107
|
const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-6 md:px-12", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-6 md:px-12");
|
|
105
108
|
const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
|
|
109
|
+
const containerClass = computed(() => ({ narrow: "max-w-3xl mx-auto", normal: "max-w-6xl mx-auto", wide: "max-w-7xl mx-auto", full: "max-w-full" })[props.contentWidth] || "max-w-6xl mx-auto");
|
|
110
|
+
const titleFontSize = computed(() => ({ sm: "clamp(1.5rem,3vw,2.5rem)", md: "clamp(2rem,4vw,3.5rem)", lg: "clamp(2.5rem,6vw,5rem)" })[props.textSize] || "clamp(2rem,4vw,3.5rem)");
|
|
111
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
106
112
|
const allFeatures = computed(() => {
|
|
107
113
|
const set = /* @__PURE__ */ new Set();
|
|
108
114
|
props.items.forEach((p) => (p.features || []).forEach((f) => set.add(typeof f === "object" ? f.fr || JSON.stringify(f) : f)));
|