cinqcinqdev-seo 0.1.44 → 0.1.46

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.
@@ -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>
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: any;
@@ -1,12 +1,12 @@
1
1
  <template>
2
- <section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
3
- <div class="max-w-6xl mx-auto">
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="text-3xl md:text-5xl font-black uppercase tracking-tighter leading-[0.9] mb-4">{{ title }}</component>
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="text-3xl md:text-5xl font-black uppercase tracking-tighter leading-[0.9] mb-4">{{ title }}</component>
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="text-3xl md:text-5xl font-black uppercase tracking-tighter leading-[0.9] mb-4">{{ title }}</component>
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 });
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: any;
@@ -0,0 +1,207 @@
1
+ <template>
2
+ <section :class="[sectionPad]" :dir="textDirection" :style="{ backgroundColor: bgColor, color: textColor }">
3
+ <div :class="[containerClass]">
4
+
5
+ <!-- Header row: title + search -->
6
+ <div v-if="badge || title || description || searchable" class="flex flex-col md:flex-row md:items-end gap-6 mb-10">
7
+ <div class="flex-1">
8
+ <span v-if="badge" class="inline-block text-[10px] font-black uppercase tracking-[0.4em] px-3 py-1 rounded-full border mb-4"
9
+ :style="{ borderColor: ca(textColor, '25'), color: ca(textColor, 'aa') }">{{ badge }}</span>
10
+ <component :is="titleTag" v-if="title" class="font-black uppercase tracking-tighter leading-[0.9]"
11
+ :style="{ fontSize: titleFontSize }">{{ title }}</component>
12
+ <p v-if="description" :class="[bodyClass, 'leading-relaxed mt-3']" :style="{ opacity: 0.6 }">{{ description }}</p>
13
+ </div>
14
+ <!-- Search input -->
15
+ <div v-if="searchable" class="shrink-0 relative">
16
+ <input
17
+ v-model="query"
18
+ type="search"
19
+ placeholder="Search…"
20
+ :class="[bodyClass, 'w-full md:w-56 pl-9 pr-4 py-2.5 border outline-none transition-all', cardRound]"
21
+ :style="{ backgroundColor: ca(textColor, '05'), borderColor: ca(textColor, '12'), color: textColor }"
22
+ @input="currentPage = 1"
23
+ />
24
+ <span class="absolute left-3 top-1/2 -translate-y-1/2 text-sm pointer-events-none" :style="{ opacity: 0.35 }">⌕</span>
25
+ </div>
26
+ </div>
27
+
28
+ <!-- ── DEFAULT / STRIPED / MINIMAL ──────────────────────── -->
29
+ <div v-if="layout !== 'cards'" class="overflow-x-auto" :class="layout !== 'minimal' ? [cardRound, 'border'] : ''" :style="layout !== 'minimal' ? { borderColor: ca(textColor, '0d') } : {}">
30
+ <table class="w-full min-w-[400px]" style="border-collapse: collapse;">
31
+ <caption v-if="caption" class="text-left px-4 pb-3 text-xs" :style="{ opacity: 0.45 }">{{ caption }}</caption>
32
+ <thead :class="stickyHeader ? 'sticky top-0 z-10' : ''" :style="{ backgroundColor: bgColor }">
33
+ <tr :style="{ borderBottom: `2px solid ${ca(textColor, '12')}` }">
34
+ <th
35
+ v-for="(col, ci) in parsedCols" :key="ci"
36
+ :class="['px-4 py-3.5 text-[10px] font-black uppercase tracking-[0.3em] select-none', col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : 'text-left', sortable ? 'cursor-pointer hover:opacity-100' : '']"
37
+ :style="{ opacity: 0.45 }"
38
+ @click="sortable && toggleSort(ci)"
39
+ >
40
+ <span class="inline-flex items-center gap-1.5">
41
+ {{ col.label }}
42
+ <span v-if="sortable" class="text-[10px] transition-all" :style="{ opacity: sortCol === ci ? 1 : 0.25 }">
43
+ {{ sortCol === ci ? (sortDir === 'asc' ? '↑' : '↓') : '↕' }}
44
+ </span>
45
+ </span>
46
+ </th>
47
+ </tr>
48
+ </thead>
49
+ <tbody>
50
+ <tr v-if="!pageRows.length">
51
+ <td :colspan="parsedCols.length" class="px-4 py-12 text-center text-sm" :style="{ opacity: 0.3 }">No results found</td>
52
+ </tr>
53
+ <tr
54
+ v-for="(row, ri) in pageRows" :key="ri"
55
+ :style="{
56
+ borderTop: ri > 0 ? `1px solid ${ca(textColor, '07')}` : 'none',
57
+ backgroundColor: layout === 'striped' && ri % 2 === 1 ? ca(textColor, '04') : 'transparent',
58
+ }"
59
+ class="transition-colors hover:bg-black/[0.02]"
60
+ >
61
+ <td
62
+ v-for="(cell, ci) in row.cells" :key="ci"
63
+ :class="['px-4 py-3.5', parsedCols[ci]?.align === 'center' ? 'text-center' : parsedCols[ci]?.align === 'right' ? 'text-right' : 'text-left']"
64
+ >
65
+ <!-- Badge-style cell if value looks like a status -->
66
+ <span v-if="parsedCols[ci]?.badge" :class="['inline-block text-[10px] font-black uppercase tracking-wider px-2.5 py-0.5 rounded-full']"
67
+ :style="{ backgroundColor: ca(textColor, '0c'), color: textColor }">{{ cell }}</span>
68
+ <span v-else :class="[bodyClass]" :style="{ opacity: 0.8 }">{{ cell }}</span>
69
+ </td>
70
+ </tr>
71
+ </tbody>
72
+ </table>
73
+ </div>
74
+
75
+ <!-- ── CARDS layout ─────────────────────────────────────── -->
76
+ <div v-else class="grid gap-4" :class="gridCols">
77
+ <div v-if="!pageRows.length" class="col-span-full py-12 text-center text-sm" :style="{ opacity: 0.3 }">No results found</div>
78
+ <div
79
+ v-for="(row, ri) in pageRows" :key="ri"
80
+ :class="['border p-5', cardRound]"
81
+ :style="{ borderColor: ca(textColor, '0d'), backgroundColor: ca(textColor, '03') }"
82
+ >
83
+ <dl class="space-y-2.5">
84
+ <div v-for="(cell, ci) in row.cells" :key="ci" class="flex items-start justify-between gap-4">
85
+ <dt :class="['text-[10px] font-black uppercase tracking-wider shrink-0']" :style="{ opacity: 0.4 }">{{ parsedCols[ci]?.label }}</dt>
86
+ <dd :class="[bodyClass, 'font-medium text-right']" :style="{ opacity: 0.85 }">{{ cell }}</dd>
87
+ </div>
88
+ </dl>
89
+ </div>
90
+ </div>
91
+
92
+ <!-- Pagination -->
93
+ <div v-if="perPage > 0 && totalPages > 1" class="flex items-center justify-between mt-6 gap-4">
94
+ <span :class="[bodyClass]" :style="{ opacity: 0.45 }">
95
+ {{ (currentPage - 1) * perPage + 1 }}–{{ Math.min(currentPage * perPage, filteredRows.length) }} of {{ filteredRows.length }}
96
+ </span>
97
+ <div class="flex items-center gap-1">
98
+ <button
99
+ v-for="p in totalPages" :key="p"
100
+ :class="['w-8 h-8 rounded-lg text-xs font-black transition-all']"
101
+ :style="p === currentPage
102
+ ? { backgroundColor: textColor, color: bgColor }
103
+ : { backgroundColor: ca(textColor, '08'), color: textColor, opacity: 0.6 }"
104
+ @click="currentPage = p"
105
+ >{{ p }}</button>
106
+ </div>
107
+ </div>
108
+
109
+ </div>
110
+ </section>
111
+ </template>
112
+
113
+ <script setup>
114
+ const ca = (c, h) => {
115
+ const m = c.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
116
+ return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : c + h;
117
+ };
118
+ import { ref, computed } from "vue";
119
+ const props = defineProps({
120
+ layout: { type: String, default: "default" },
121
+ titleTag: { type: String, default: "h2" },
122
+ badge: { type: String, default: "" },
123
+ title: { type: String, default: "" },
124
+ description: { type: String, default: "" },
125
+ caption: { type: String, default: "" },
126
+ searchable: { type: Boolean, default: false },
127
+ sortable: { type: Boolean, default: true },
128
+ stickyHeader: { type: Boolean, default: false },
129
+ perPage: { type: Number, default: 0 },
130
+ cardColumns: { type: String, default: "auto" },
131
+ columns: { type: Array, default: () => [
132
+ { label: "Name", align: "left", badge: false },
133
+ { label: "Role", align: "left", badge: false },
134
+ { label: "Department", align: "left", badge: false },
135
+ { label: "Status", align: "left", badge: true }
136
+ ] },
137
+ rows: { type: Array, default: () => [
138
+ { cells: "Alice Martin | Lead Designer | Product | Active" },
139
+ { cells: "Bob Chen | Senior Developer | Engineering | Active" },
140
+ { cells: "Clara Dumont | Product Manager | Product | On leave" },
141
+ { cells: "David Kim | UX Researcher | Design | Active" },
142
+ { cells: "Emma Laurent | DevOps Engineer | Engineering | Active" },
143
+ { cells: "F\xE9lix Moreau | Data Analyst | Analytics | Active" }
144
+ ] },
145
+ bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
146
+ textColor: { type: String, default: "rgba(var(--text-color), 1)" },
147
+ spacing: { type: String, default: "normal" },
148
+ radius: { type: String, default: "rounded" },
149
+ textDirection: { type: String, default: "ltr" },
150
+ contentWidth: { type: String, default: "normal" },
151
+ textSize: { type: String, default: "md" }
152
+ });
153
+ 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");
154
+ const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-2xl", pill: "rounded-3xl" })[props.radius] || "rounded-2xl");
155
+ 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");
156
+ const titleFontSize = computed(() => ({ sm: "clamp(1.2rem,2.5vw,2rem)", md: "clamp(1.8rem,4vw,3rem)", lg: "clamp(2.2rem,5vw,4rem)" })[props.textSize] || "clamp(1.8rem,4vw,3rem)");
157
+ const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
158
+ const parsedCols = computed(() => (props.columns || []).map((col) => ({
159
+ label: col.label || "",
160
+ align: col.align || "left",
161
+ badge: col.badge || false
162
+ })));
163
+ const parsedRows = computed(() => (props.rows || []).map((row) => ({
164
+ cells: (row.cells || "").split("|").map((c) => c.trim())
165
+ })));
166
+ const query = ref("");
167
+ const sortCol = ref(-1);
168
+ const sortDir = ref("asc");
169
+ const toggleSort = (i) => {
170
+ if (sortCol.value === i) sortDir.value = sortDir.value === "asc" ? "desc" : "asc";
171
+ else {
172
+ sortCol.value = i;
173
+ sortDir.value = "asc";
174
+ }
175
+ };
176
+ const currentPage = ref(1);
177
+ const filteredRows = computed(() => {
178
+ let rows = parsedRows.value;
179
+ if (props.searchable && query.value.trim()) {
180
+ const q = query.value.toLowerCase();
181
+ rows = rows.filter((r) => r.cells.some((c) => c.toLowerCase().includes(q)));
182
+ }
183
+ if (props.sortable && sortCol.value >= 0) {
184
+ rows = [...rows].sort((a, b) => {
185
+ const av = a.cells[sortCol.value] || "";
186
+ const bv = b.cells[sortCol.value] || "";
187
+ const cmp = av.localeCompare(bv, void 0, { numeric: true, sensitivity: "base" });
188
+ return sortDir.value === "asc" ? cmp : -cmp;
189
+ });
190
+ }
191
+ return rows;
192
+ });
193
+ const totalPages = computed(() => props.perPage > 0 ? Math.ceil(filteredRows.value.length / props.perPage) : 1);
194
+ const pageRows = computed(() => {
195
+ if (props.perPage <= 0) return filteredRows.value;
196
+ const start = (currentPage.value - 1) * props.perPage;
197
+ return filteredRows.value.slice(start, start + props.perPage);
198
+ });
199
+ const gridCols = computed(() => {
200
+ if (props.cardColumns === "1") return "grid-cols-1";
201
+ if (props.cardColumns === "2") return "grid-cols-1 md:grid-cols-2";
202
+ if (props.cardColumns === "4") return "grid-cols-1 sm:grid-cols-2 xl:grid-cols-4";
203
+ const n = parsedCols.value.length;
204
+ if (n <= 3) return "grid-cols-1 md:grid-cols-2";
205
+ return "grid-cols-1 md:grid-cols-2 lg:grid-cols-3";
206
+ });
207
+ </script>
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: any;
@@ -1,8 +1,8 @@
1
1
  <template>
2
- <section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
3
- <div class="max-w-3xl mx-auto">
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="text-4xl md:text-6xl font-black uppercase tracking-tighter italic">{{ title }}</component>
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="text-sm leading-relaxed" :style="{ opacity: 0.65 }">{{ item.answer }}</p>
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="max-w-6xl mx-auto">
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="text-4xl md:text-6xl font-black uppercase tracking-tighter leading-none max-w-4xl italic">{{ title }}</component>
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="text-sm leading-relaxed" :style="{ opacity: 0.6 }">{{ item.description || item.text }}</p>
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>