cinqcinqdev-seo 0.1.71 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +39 -2
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/assets/admin-tw.css +1 -1
  4. package/dist/runtime/components/admin-cms/DynamicRenderer.vue +16 -2
  5. package/dist/runtime/components/sections/{BlogHero.vue → cinqcinq/BlogHero.vue} +35 -3
  6. package/dist/runtime/components/sections/{CallToAction.vue → cinqcinq/CallToAction.vue} +35 -3
  7. package/dist/runtime/components/sections/{ContactForm.vue → cinqcinq/ContactForm.vue} +34 -2
  8. package/dist/runtime/components/sections/{FAQ.vue → cinqcinq/FAQ.vue} +35 -3
  9. package/dist/runtime/components/sections/{Features.vue → cinqcinq/Features.vue} +35 -3
  10. package/dist/runtime/components/sections/{HeroSection.vue → cinqcinq/HeroSection.vue} +11 -0
  11. package/dist/runtime/components/sections/{Newsletter.vue → cinqcinq/Newsletter.vue} +35 -3
  12. package/dist/runtime/components/sections/{Pricing.vue → cinqcinq/Pricing.vue} +35 -3
  13. package/dist/runtime/components/sections/{Process.vue → cinqcinq/Process.vue} +35 -3
  14. package/dist/runtime/components/sections/{Quote.vue → cinqcinq/Quote.vue} +35 -3
  15. package/dist/runtime/components/sections/{RichText.vue → cinqcinq/RichText.vue} +32 -4
  16. package/dist/runtime/components/sections/{ServiceGrid.vue → cinqcinq/ServiceGrid.vue} +35 -3
  17. package/dist/runtime/components/sections/{Testimonials.vue → cinqcinq/Testimonials.vue} +35 -3
  18. package/dist/runtime/components/sections/{TextVisual.vue → cinqcinq/TextVisual.vue} +39 -3
  19. package/dist/runtime/components/sections/swiss-style/BlogHero.d.vue.ts +3 -0
  20. package/dist/runtime/components/sections/swiss-style/BlogHero.vue +146 -0
  21. package/dist/runtime/components/sections/swiss-style/BlogHero.vue.d.ts +3 -0
  22. package/dist/runtime/components/sections/swiss-style/CallToAction.d.vue.ts +3 -0
  23. package/dist/runtime/components/sections/swiss-style/CallToAction.vue +138 -0
  24. package/dist/runtime/components/sections/swiss-style/CallToAction.vue.d.ts +3 -0
  25. package/dist/runtime/components/sections/swiss-style/Comparison.d.vue.ts +3 -0
  26. package/dist/runtime/components/sections/swiss-style/Comparison.vue +280 -0
  27. package/dist/runtime/components/sections/swiss-style/Comparison.vue.d.ts +3 -0
  28. package/dist/runtime/components/sections/swiss-style/ContactForm.d.vue.ts +3 -0
  29. package/dist/runtime/components/sections/swiss-style/ContactForm.vue +173 -0
  30. package/dist/runtime/components/sections/swiss-style/ContactForm.vue.d.ts +3 -0
  31. package/dist/runtime/components/sections/swiss-style/DataTable.d.vue.ts +3 -0
  32. package/dist/runtime/components/sections/swiss-style/DataTable.vue +245 -0
  33. package/dist/runtime/components/sections/swiss-style/DataTable.vue.d.ts +3 -0
  34. package/dist/runtime/components/sections/swiss-style/FAQ.d.vue.ts +3 -0
  35. package/dist/runtime/components/sections/swiss-style/FAQ.vue +107 -0
  36. package/dist/runtime/components/sections/swiss-style/FAQ.vue.d.ts +3 -0
  37. package/dist/runtime/components/sections/swiss-style/Features.d.vue.ts +3 -0
  38. package/dist/runtime/components/sections/swiss-style/Features.vue +103 -0
  39. package/dist/runtime/components/sections/swiss-style/Features.vue.d.ts +3 -0
  40. package/dist/runtime/components/sections/swiss-style/HeroFan.d.vue.ts +3 -0
  41. package/dist/runtime/components/sections/swiss-style/HeroFan.vue +108 -0
  42. package/dist/runtime/components/sections/swiss-style/HeroFan.vue.d.ts +3 -0
  43. package/dist/runtime/components/sections/swiss-style/HeroSection.d.vue.ts +3 -0
  44. package/dist/runtime/components/sections/swiss-style/HeroSection.vue +190 -0
  45. package/dist/runtime/components/sections/swiss-style/HeroSection.vue.d.ts +3 -0
  46. package/dist/runtime/components/sections/swiss-style/HeroSlider.d.vue.ts +3 -0
  47. package/dist/runtime/components/sections/swiss-style/HeroSlider.vue +138 -0
  48. package/dist/runtime/components/sections/swiss-style/HeroSlider.vue.d.ts +3 -0
  49. package/dist/runtime/components/sections/swiss-style/Newsletter.d.vue.ts +3 -0
  50. package/dist/runtime/components/sections/swiss-style/Newsletter.vue +125 -0
  51. package/dist/runtime/components/sections/swiss-style/Newsletter.vue.d.ts +3 -0
  52. package/dist/runtime/components/sections/swiss-style/Pricing.d.vue.ts +3 -0
  53. package/dist/runtime/components/sections/swiss-style/Pricing.vue +148 -0
  54. package/dist/runtime/components/sections/swiss-style/Pricing.vue.d.ts +3 -0
  55. package/dist/runtime/components/sections/swiss-style/Process.d.vue.ts +3 -0
  56. package/dist/runtime/components/sections/swiss-style/Process.vue +124 -0
  57. package/dist/runtime/components/sections/swiss-style/Process.vue.d.ts +3 -0
  58. package/dist/runtime/components/sections/swiss-style/Quote.d.vue.ts +3 -0
  59. package/dist/runtime/components/sections/swiss-style/Quote.vue +130 -0
  60. package/dist/runtime/components/sections/swiss-style/Quote.vue.d.ts +3 -0
  61. package/dist/runtime/components/sections/swiss-style/RelatedPages.d.vue.ts +3 -0
  62. package/dist/runtime/components/sections/swiss-style/RelatedPages.vue +212 -0
  63. package/dist/runtime/components/sections/swiss-style/RelatedPages.vue.d.ts +3 -0
  64. package/dist/runtime/components/sections/swiss-style/RichText.d.vue.ts +3 -0
  65. package/dist/runtime/components/sections/swiss-style/RichText.vue +100 -0
  66. package/dist/runtime/components/sections/swiss-style/RichText.vue.d.ts +3 -0
  67. package/dist/runtime/components/sections/swiss-style/ServiceGrid.d.vue.ts +3 -0
  68. package/dist/runtime/components/sections/swiss-style/ServiceGrid.vue +121 -0
  69. package/dist/runtime/components/sections/swiss-style/ServiceGrid.vue.d.ts +3 -0
  70. package/dist/runtime/components/sections/swiss-style/Testimonials.d.vue.ts +3 -0
  71. package/dist/runtime/components/sections/swiss-style/Testimonials.vue +126 -0
  72. package/dist/runtime/components/sections/swiss-style/Testimonials.vue.d.ts +3 -0
  73. package/dist/runtime/components/sections/swiss-style/TextVisual.d.vue.ts +3 -0
  74. package/dist/runtime/components/sections/swiss-style/TextVisual.vue +146 -0
  75. package/dist/runtime/components/sections/swiss-style/TextVisual.vue.d.ts +3 -0
  76. package/dist/runtime/composables/useAdminSections.js +75 -37
  77. package/dist/runtime/composables/useAdminSettings.d.ts +1 -0
  78. package/dist/runtime/pages/admin/editor/[id].vue +15 -1
  79. package/dist/runtime/pages/admin/setup.vue +77 -2
  80. package/package.json +1 -1
  81. /package/dist/runtime/components/sections/{BlogHero.d.vue.ts → cinqcinq/BlogHero.d.vue.ts} +0 -0
  82. /package/dist/runtime/components/sections/{BlogHero.vue.d.ts → cinqcinq/BlogHero.vue.d.ts} +0 -0
  83. /package/dist/runtime/components/sections/{CallToAction.d.vue.ts → cinqcinq/CallToAction.d.vue.ts} +0 -0
  84. /package/dist/runtime/components/sections/{CallToAction.vue.d.ts → cinqcinq/CallToAction.vue.d.ts} +0 -0
  85. /package/dist/runtime/components/sections/{Comparison.d.vue.ts → cinqcinq/Comparison.d.vue.ts} +0 -0
  86. /package/dist/runtime/components/sections/{Comparison.vue → cinqcinq/Comparison.vue} +0 -0
  87. /package/dist/runtime/components/sections/{Comparison.vue.d.ts → cinqcinq/Comparison.vue.d.ts} +0 -0
  88. /package/dist/runtime/components/sections/{ContactForm.d.vue.ts → cinqcinq/ContactForm.d.vue.ts} +0 -0
  89. /package/dist/runtime/components/sections/{ContactForm.vue.d.ts → cinqcinq/ContactForm.vue.d.ts} +0 -0
  90. /package/dist/runtime/components/sections/{DataTable.d.vue.ts → cinqcinq/DataTable.d.vue.ts} +0 -0
  91. /package/dist/runtime/components/sections/{DataTable.vue → cinqcinq/DataTable.vue} +0 -0
  92. /package/dist/runtime/components/sections/{DataTable.vue.d.ts → cinqcinq/DataTable.vue.d.ts} +0 -0
  93. /package/dist/runtime/components/sections/{FAQ.d.vue.ts → cinqcinq/FAQ.d.vue.ts} +0 -0
  94. /package/dist/runtime/components/sections/{FAQ.vue.d.ts → cinqcinq/FAQ.vue.d.ts} +0 -0
  95. /package/dist/runtime/components/sections/{Features.d.vue.ts → cinqcinq/Features.d.vue.ts} +0 -0
  96. /package/dist/runtime/components/sections/{Features.vue.d.ts → cinqcinq/Features.vue.d.ts} +0 -0
  97. /package/dist/runtime/components/sections/{HeroFan.d.vue.ts → cinqcinq/HeroFan.d.vue.ts} +0 -0
  98. /package/dist/runtime/components/sections/{HeroFan.vue → cinqcinq/HeroFan.vue} +0 -0
  99. /package/dist/runtime/components/sections/{HeroFan.vue.d.ts → cinqcinq/HeroFan.vue.d.ts} +0 -0
  100. /package/dist/runtime/components/sections/{HeroSection.d.vue.ts → cinqcinq/HeroSection.d.vue.ts} +0 -0
  101. /package/dist/runtime/components/sections/{HeroSection.vue.d.ts → cinqcinq/HeroSection.vue.d.ts} +0 -0
  102. /package/dist/runtime/components/sections/{HeroSlider.d.vue.ts → cinqcinq/HeroSlider.d.vue.ts} +0 -0
  103. /package/dist/runtime/components/sections/{HeroSlider.vue → cinqcinq/HeroSlider.vue} +0 -0
  104. /package/dist/runtime/components/sections/{HeroSlider.vue.d.ts → cinqcinq/HeroSlider.vue.d.ts} +0 -0
  105. /package/dist/runtime/components/sections/{Newsletter.d.vue.ts → cinqcinq/Newsletter.d.vue.ts} +0 -0
  106. /package/dist/runtime/components/sections/{Newsletter.vue.d.ts → cinqcinq/Newsletter.vue.d.ts} +0 -0
  107. /package/dist/runtime/components/sections/{Pricing.d.vue.ts → cinqcinq/Pricing.d.vue.ts} +0 -0
  108. /package/dist/runtime/components/sections/{Pricing.vue.d.ts → cinqcinq/Pricing.vue.d.ts} +0 -0
  109. /package/dist/runtime/components/sections/{Process.d.vue.ts → cinqcinq/Process.d.vue.ts} +0 -0
  110. /package/dist/runtime/components/sections/{Process.vue.d.ts → cinqcinq/Process.vue.d.ts} +0 -0
  111. /package/dist/runtime/components/sections/{Quote.d.vue.ts → cinqcinq/Quote.d.vue.ts} +0 -0
  112. /package/dist/runtime/components/sections/{Quote.vue.d.ts → cinqcinq/Quote.vue.d.ts} +0 -0
  113. /package/dist/runtime/components/sections/{RelatedPages.d.vue.ts → cinqcinq/RelatedPages.d.vue.ts} +0 -0
  114. /package/dist/runtime/components/sections/{RelatedPages.vue → cinqcinq/RelatedPages.vue} +0 -0
  115. /package/dist/runtime/components/sections/{RelatedPages.vue.d.ts → cinqcinq/RelatedPages.vue.d.ts} +0 -0
  116. /package/dist/runtime/components/sections/{RichText.d.vue.ts → cinqcinq/RichText.d.vue.ts} +0 -0
  117. /package/dist/runtime/components/sections/{RichText.vue.d.ts → cinqcinq/RichText.vue.d.ts} +0 -0
  118. /package/dist/runtime/components/sections/{ServiceGrid.d.vue.ts → cinqcinq/ServiceGrid.d.vue.ts} +0 -0
  119. /package/dist/runtime/components/sections/{ServiceGrid.vue.d.ts → cinqcinq/ServiceGrid.vue.d.ts} +0 -0
  120. /package/dist/runtime/components/sections/{Testimonials.d.vue.ts → cinqcinq/Testimonials.d.vue.ts} +0 -0
  121. /package/dist/runtime/components/sections/{Testimonials.vue.d.ts → cinqcinq/Testimonials.vue.d.ts} +0 -0
  122. /package/dist/runtime/components/sections/{TextVisual.d.vue.ts → cinqcinq/TextVisual.d.vue.ts} +0 -0
  123. /package/dist/runtime/components/sections/{TextVisual.vue.d.ts → cinqcinq/TextVisual.vue.d.ts} +0 -0
@@ -0,0 +1,173 @@
1
+ <template>
2
+ <section ref="sectionEl" :class="[sectionPad, isVisible ? 'sv-in' : 'sv-out']" :dir="textDirection" :style="[{ backgroundColor: bgColor, color: textColor }, spacingStyle]">
3
+ <div :class="[containerClass]">
4
+
5
+ <!-- SPLIT layout -->
6
+ <div v-if="layout === 'split'" class="grid grid-cols-1 md:grid-cols-2 border divide-x" :style="{ borderColor: ca(textColor,'15') }">
7
+ <div class="p-10 md:p-14">
8
+ <span v-if="badge" class="block text-[9px] font-black uppercase tracking-[0.5em] mb-6" :style="{ opacity: 0.35 }">{{ badge }}</span>
9
+ <component :is="titleTag" class="font-black uppercase leading-[0.88] mb-6" :style="{ fontSize: titleFontSize, letterSpacing: '-0.03em' }">{{ title }}</component>
10
+ <p v-if="description" class="text-sm leading-relaxed mb-8" :style="{ opacity: 0.55 }">{{ description }}</p>
11
+ <div v-if="infos && infos.length" class="space-y-4">
12
+ <div v-for="(info, i) in infos" :key="i" class="flex items-start gap-3 border-b pb-4" :style="{ borderColor: ca(textColor,'08') }">
13
+ <div>
14
+ <p class="text-[11px] font-black uppercase tracking-widest mb-0.5" :style="{ opacity: 0.4 }">{{ info.label }}</p>
15
+ <p class="text-sm">{{ info.value }}</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ <div class="p-10 md:p-14">
21
+ <form @submit.prevent="handleSubmit" class="space-y-6">
22
+ <div v-for="field in fields" :key="field.name" class="flex flex-col gap-1.5 border-b pb-5" :style="{ borderColor: ca(textColor,'10') }">
23
+ <label class="text-[10px] font-black uppercase tracking-widest" :style="{ opacity: 0.4 }">{{ field.label }}<span v-if="field.required" class="ml-0.5">*</span></label>
24
+ <textarea v-if="field.type === 'textarea'" v-model="formData[field.name]" :placeholder="field.placeholder" :required="field.required" rows="3"
25
+ class="w-full bg-transparent text-sm resize-none outline-none" :style="{ color: textColor }"></textarea>
26
+ <input v-else v-model="formData[field.name]" :type="field.type || 'text'" :placeholder="field.placeholder" :required="field.required"
27
+ class="w-full bg-transparent text-sm outline-none" :style="{ color: textColor }" />
28
+ </div>
29
+ <button type="submit" :disabled="submitting"
30
+ class="w-full py-4 text-[11px] font-black uppercase tracking-widest transition-all hover:opacity-80 disabled:opacity-50 border"
31
+ :style="{ backgroundColor: textColor, color: bgColor, borderColor: textColor }">
32
+ {{ submitting ? '...' : (ctaText || 'Envoyer') }}
33
+ </button>
34
+ <p v-if="sent" class="text-xs" :style="{ opacity: 0.6 }">{{ successMessage }}</p>
35
+ </form>
36
+ </div>
37
+ </div>
38
+
39
+ <!-- CENTERED layout -->
40
+ <div v-else-if="layout === 'centered'" class="max-w-xl mx-auto">
41
+ <div class="border-b pb-6 mb-10" :style="{ borderColor: ca(textColor,'15') }">
42
+ <span v-if="badge" class="block text-[9px] font-black uppercase tracking-[0.5em] mb-3" :style="{ opacity: 0.35 }">{{ badge }}</span>
43
+ <component :is="titleTag" class="font-black uppercase leading-[0.88]" :style="{ fontSize: titleFontSize, letterSpacing: '-0.03em' }">{{ title }}</component>
44
+ <p v-if="description" class="text-sm leading-relaxed mt-4" :style="{ opacity: 0.55 }">{{ description }}</p>
45
+ </div>
46
+ <form @submit.prevent="handleSubmit" class="space-y-6">
47
+ <div v-for="field in fields" :key="field.name" class="flex flex-col gap-1.5 border-b pb-5" :style="{ borderColor: ca(textColor,'10') }">
48
+ <label class="text-[10px] font-black uppercase tracking-widest" :style="{ opacity: 0.4 }">{{ field.label }}<span v-if="field.required" class="ml-0.5">*</span></label>
49
+ <textarea v-if="field.type === 'textarea'" v-model="formData[field.name]" :placeholder="field.placeholder" :required="field.required" rows="3"
50
+ class="w-full bg-transparent text-sm resize-none outline-none" :style="{ color: textColor }"></textarea>
51
+ <input v-else v-model="formData[field.name]" :type="field.type || 'text'" :placeholder="field.placeholder" :required="field.required"
52
+ class="w-full bg-transparent text-sm outline-none" :style="{ color: textColor }" />
53
+ </div>
54
+ <button type="submit" :disabled="submitting"
55
+ class="w-full py-4 text-[11px] font-black uppercase tracking-widest border transition-all hover:opacity-80 disabled:opacity-50"
56
+ :style="{ backgroundColor: textColor, color: bgColor, borderColor: textColor }">
57
+ {{ submitting ? '...' : (ctaText || 'Envoyer') }}
58
+ </button>
59
+ <p v-if="sent" class="text-xs" :style="{ opacity: 0.6 }">{{ successMessage }}</p>
60
+ </form>
61
+ </div>
62
+
63
+ <!-- MINIMAL layout (default) -->
64
+ <div v-else class="max-w-2xl">
65
+ <div class="border-b pb-6 mb-10" :style="{ borderColor: ca(textColor,'15') }">
66
+ <span v-if="badge" class="block text-[9px] font-black uppercase tracking-[0.5em] mb-3" :style="{ opacity: 0.35 }">{{ badge }}</span>
67
+ <component :is="titleTag" class="font-black uppercase leading-[0.88]" :style="{ fontSize: titleFontSize, letterSpacing: '-0.03em' }">{{ title }}</component>
68
+ <p v-if="description" class="text-sm leading-relaxed mt-4" :style="{ opacity: 0.55 }">{{ description }}</p>
69
+ </div>
70
+ <form @submit.prevent="handleSubmit" class="space-y-0">
71
+ <div v-for="field in fields" :key="field.name" class="flex flex-col gap-2 border-b py-5" :style="{ borderColor: ca(textColor,'10') }">
72
+ <label class="text-[10px] font-black uppercase tracking-widest" :style="{ opacity: 0.4 }">{{ field.label }}<span v-if="field.required" class="ml-0.5">*</span></label>
73
+ <textarea v-if="field.type === 'textarea'" v-model="formData[field.name]" :placeholder="field.placeholder" :required="field.required" rows="3"
74
+ class="w-full bg-transparent text-sm resize-none outline-none" :style="{ color: textColor }"></textarea>
75
+ <input v-else v-model="formData[field.name]" :type="field.type || 'text'" :placeholder="field.placeholder" :required="field.required"
76
+ class="w-full bg-transparent text-sm outline-none" :style="{ color: textColor }" />
77
+ </div>
78
+ <button type="submit" :disabled="submitting"
79
+ class="mt-8 px-8 py-4 text-[11px] font-black uppercase tracking-widest border transition-all hover:opacity-80 disabled:opacity-50"
80
+ :style="{ backgroundColor: textColor, color: bgColor, borderColor: textColor }">
81
+ {{ submitting ? '...' : (ctaText || 'Envoyer') }}
82
+ </button>
83
+ <p v-if="sent" class="text-xs mt-4" :style="{ opacity: 0.6 }">{{ successMessage }}</p>
84
+ </form>
85
+ </div>
86
+
87
+ </div>
88
+ </section>
89
+ </template>
90
+
91
+ <script setup>
92
+ const ca = (c, h) => {
93
+ const m = c?.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
94
+ return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : (c || "") + h;
95
+ };
96
+ import { ref, computed, onMounted } from "vue";
97
+ const props = defineProps({
98
+ layout: { type: String, default: "split" },
99
+ badge: { type: String, default: "" },
100
+ title: { type: String, default: "Contactez-nous" },
101
+ titleTag: { type: String, default: "h2" },
102
+ description: { type: String, default: "" },
103
+ ctaText: { type: String, default: "Envoyer" },
104
+ successMessage: { type: String, default: "Message envoy\xE9 \u2713" },
105
+ infos: { type: Array, default: () => [] },
106
+ fields: { type: Array, default: () => [
107
+ { name: "name", label: "Nom", type: "text", required: true },
108
+ { name: "email", label: "Email", type: "email", required: true },
109
+ { name: "message", label: "Message", type: "textarea", required: true }
110
+ ] },
111
+ bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
112
+ textColor: { type: String, default: "rgba(var(--text-color), 1)" },
113
+ spacing: { type: String, default: "normal" },
114
+ radius: { type: String, default: "sharp" },
115
+ textDirection: { type: String, default: "ltr" },
116
+ contentWidth: { type: String, default: "normal" },
117
+ textSize: { type: String, default: "md" },
118
+ paddingTop: { type: String, default: "" },
119
+ paddingBottom: { type: String, default: "" },
120
+ marginTop: { type: String, default: "" },
121
+ marginBottom: { type: String, default: "" },
122
+ paddingX: { type: String, default: "" }
123
+ });
124
+ const sectionEl = ref(null);
125
+ const isVisible = ref(false);
126
+ onMounted(() => {
127
+ const obs = new IntersectionObserver(([e]) => {
128
+ if (e.isIntersecting) {
129
+ isVisible.value = true;
130
+ obs.disconnect();
131
+ }
132
+ }, { threshold: 0.06 });
133
+ if (sectionEl.value) obs.observe(sectionEl.value);
134
+ setTimeout(() => {
135
+ if (!isVisible.value) isVisible.value = true;
136
+ }, 300);
137
+ });
138
+ const formData = ref({});
139
+ const submitting = ref(false);
140
+ const sent = ref(false);
141
+ const handleSubmit = async () => {
142
+ submitting.value = true;
143
+ await new Promise((r) => setTimeout(r, 800));
144
+ submitting.value = false;
145
+ sent.value = true;
146
+ };
147
+ const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-8 md:px-16", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-8 md:px-16");
148
+ 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");
149
+ const titleFontSize = computed(() => ({ sm: "clamp(1rem,1.5vw,1.3rem)", md: "clamp(1.5rem,2.5vw,2rem)", lg: "clamp(2.5rem,6vw,5rem)" })[props.textSize] || "clamp(1.5rem,2.5vw,2rem)");
150
+ const SM = { none: "0", xs: "8px", sm: "24px", md: "48px", lg: "80px", xl: "128px", "-xs": "-8px", "-sm": "-24px", "-md": "-48px", "-lg": "-80px", "-xl": "-128px" };
151
+ const spacingStyle = computed(() => {
152
+ const s = {};
153
+ if (props.paddingTop && SM[props.paddingTop]) s.paddingTop = SM[props.paddingTop];
154
+ if (props.paddingBottom && SM[props.paddingBottom]) s.paddingBottom = SM[props.paddingBottom];
155
+ if (props.marginTop && SM[props.marginTop]) s.marginTop = SM[props.marginTop];
156
+ if (props.marginBottom && SM[props.marginBottom]) s.marginBottom = SM[props.marginBottom];
157
+ if (props.paddingX && SM[props.paddingX]) {
158
+ const v = SM[props.paddingX];
159
+ if (v.startsWith("-")) {
160
+ s.marginLeft = v;
161
+ s.marginRight = v;
162
+ } else {
163
+ s.paddingLeft = v;
164
+ s.paddingRight = v;
165
+ }
166
+ }
167
+ return s;
168
+ });
169
+ </script>
170
+
171
+ <style scoped>
172
+ .sv-out{opacity:0;transform:translateY(16px)}.sv-in{opacity:1;transform:none;transition:opacity .6s ease,transform .6s ease}
173
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: any;
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: any;
@@ -0,0 +1,245 @@
1
+ <template>
2
+ <section ref="sectionEl" :class="[sectionPad, isVisible ? 'sv-in' : 'sv-out']" :dir="textDirection" :style="[{ backgroundColor: bgColor, color: textColor }, spacingStyle]">
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 pb-6 border-b" :style="{ borderColor: ca(textColor,'12') }">
7
+ <div class="flex-1">
8
+ <span v-if="badge" class="block text-[9px] font-black uppercase tracking-[0.5em] mb-3" :style="{ opacity: 0.35 }">{{ badge }}</span>
9
+ <component :is="titleTag" v-if="title" class="font-black uppercase leading-[0.88]"
10
+ :style="{ fontSize: titleFontSize, letterSpacing: '-0.03em' }">{{ title }}</component>
11
+ <p v-if="description" :class="[bodyClass, 'leading-relaxed mt-3']" :style="{ opacity: 0.55 }">{{ description }}</p>
12
+ </div>
13
+ <!-- Search input -->
14
+ <div v-if="searchable" class="shrink-0 relative">
15
+ <input
16
+ v-model="query"
17
+ type="search"
18
+ placeholder="Search…"
19
+ :class="[bodyClass, 'w-full md:w-56 pl-9 pr-4 py-2.5 border outline-none transition-all bg-transparent']"
20
+ :style="{ borderColor: ca(textColor,'20'), color: textColor }"
21
+ @input="currentPage = 1"
22
+ />
23
+ <span class="absolute left-3 top-1/2 -translate-y-1/2 text-sm pointer-events-none" :style="{ opacity: 0.35 }">⌕</span>
24
+ </div>
25
+ </div>
26
+
27
+ <!-- DEFAULT / STRIPED / MINIMAL table -->
28
+ <div v-if="layout !== 'cards'" class="overflow-x-auto border" :style="{ borderColor: ca(textColor,'12') }">
29
+ <table class="w-full min-w-[400px]" style="border-collapse: collapse;">
30
+ <caption v-if="caption" class="text-left px-6 pb-3 text-xs" :style="{ opacity: 0.4 }">{{ caption }}</caption>
31
+ <thead :class="stickyHeader ? 'sticky top-0 z-10' : ''" :style="{ backgroundColor: bgColor }">
32
+ <tr :style="{ borderBottom: `2px solid ${ca(textColor,'15')}` }">
33
+ <th
34
+ v-for="(col, ci) in parsedCols" :key="ci"
35
+ :class="['px-6 py-4 text-[9px] font-black uppercase tracking-[0.4em] select-none', col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : 'text-left', sortable ? 'cursor-pointer hover:opacity-100' : '']"
36
+ :style="{ opacity: 0.4, borderLeft: ci > 0 ? `1px solid ${ca(textColor,'08')}` : 'none' }"
37
+ @click="sortable && toggleSort(ci)"
38
+ >
39
+ <span class="inline-flex items-center gap-1.5">
40
+ {{ col.label }}
41
+ <span v-if="sortable" class="text-[10px] transition-all" :style="{ opacity: sortCol === ci ? 1 : 0.25 }">
42
+ {{ sortCol === ci ? (sortDir === 'asc' ? '↑' : '↓') : '↕' }}
43
+ </span>
44
+ </span>
45
+ </th>
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ <tr v-if="!pageRows.length">
50
+ <td :colspan="parsedCols.length" class="px-6 py-12 text-center text-sm" :style="{ opacity: 0.3 }">No results found</td>
51
+ </tr>
52
+ <tr
53
+ v-for="(row, ri) in pageRows" :key="ri"
54
+ :style="{
55
+ borderTop: `1px solid ${ca(textColor,'07')}`,
56
+ backgroundColor: layout === 'striped' && ri % 2 === 1 ? ca(textColor,'03') : 'transparent',
57
+ }"
58
+ >
59
+ <td
60
+ v-for="(cell, ci) in row.cells" :key="ci"
61
+ :class="['px-6 py-4', parsedCols[ci]?.align === 'center' ? 'text-center' : parsedCols[ci]?.align === 'right' ? 'text-right' : 'text-left']"
62
+ :style="{ borderLeft: ci > 0 ? `1px solid ${ca(textColor,'06')}` : 'none' }"
63
+ >
64
+ <span v-if="parsedCols[ci]?.badge" class="inline-block text-[9px] font-black uppercase tracking-wider px-2.5 py-0.5 border"
65
+ :style="{ borderColor: ca(textColor,'18'), color: textColor }">{{ cell }}</span>
66
+ <span v-else :class="[bodyClass]" :style="{ opacity: 0.8 }">{{ cell }}</span>
67
+ </td>
68
+ </tr>
69
+ </tbody>
70
+ </table>
71
+ </div>
72
+
73
+ <!-- CARDS layout -->
74
+ <div v-else class="grid gap-0 border divide-x divide-y" :class="gridCols" :style="{ borderColor: ca(textColor,'12') }">
75
+ <div v-if="!pageRows.length" class="col-span-full py-12 text-center text-sm" :style="{ opacity: 0.3 }">No results found</div>
76
+ <div
77
+ v-for="(row, ri) in pageRows" :key="ri"
78
+ class="p-6"
79
+ >
80
+ <dl class="space-y-3">
81
+ <div v-for="(cell, ci) in row.cells" :key="ci" class="flex items-start justify-between gap-4 border-b pb-2.5 last:border-b-0" :style="{ borderColor: ca(textColor,'07') }">
82
+ <dt class="text-[9px] font-black uppercase tracking-[0.4em] shrink-0" :style="{ opacity: 0.35 }">{{ parsedCols[ci]?.label }}</dt>
83
+ <dd :class="[bodyClass, 'font-medium text-right']" :style="{ opacity: 0.8 }">{{ cell }}</dd>
84
+ </div>
85
+ </dl>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Pagination -->
90
+ <div v-if="perPage > 0 && totalPages > 1" class="flex items-center justify-between mt-6 border-t pt-4" :style="{ borderColor: ca(textColor,'10') }">
91
+ <span :class="[bodyClass]" :style="{ opacity: 0.4 }">
92
+ {{ (currentPage - 1) * perPage + 1 }}–{{ Math.min(currentPage * perPage, filteredRows.length) }} of {{ filteredRows.length }}
93
+ </span>
94
+ <div class="flex items-center gap-1">
95
+ <button
96
+ v-for="p in totalPages" :key="p"
97
+ class="w-8 h-8 text-xs font-black border transition-all"
98
+ :style="p === currentPage
99
+ ? { backgroundColor: textColor, color: bgColor, borderColor: textColor }
100
+ : { borderColor: ca(textColor,'15'), color: textColor, opacity: 0.5 }"
101
+ @click="currentPage = p"
102
+ >{{ p }}</button>
103
+ </div>
104
+ </div>
105
+
106
+ </div>
107
+ </section>
108
+ </template>
109
+
110
+ <script setup>
111
+ const ca = (c, h) => {
112
+ const m = c?.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
113
+ return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : (c || "") + h;
114
+ };
115
+ import { ref, computed, onMounted } from "vue";
116
+ const props = defineProps({
117
+ layout: { type: String, default: "default" },
118
+ titleTag: { type: String, default: "h2" },
119
+ badge: { type: String, default: "" },
120
+ title: { type: String, default: "" },
121
+ description: { type: String, default: "" },
122
+ caption: { type: String, default: "" },
123
+ searchable: { type: Boolean, default: false },
124
+ sortable: { type: Boolean, default: true },
125
+ stickyHeader: { type: Boolean, default: false },
126
+ perPage: { type: Number, default: 0 },
127
+ cardColumns: { type: String, default: "auto" },
128
+ columns: { type: Array, default: () => [
129
+ { label: "Name", align: "left", badge: false },
130
+ { label: "Role", align: "left", badge: false },
131
+ { label: "Department", align: "left", badge: false },
132
+ { label: "Status", align: "left", badge: true }
133
+ ] },
134
+ items: { type: Array, default: () => [
135
+ { cells: "Alice Martin | Lead Designer | Product | Active" },
136
+ { cells: "Bob Chen | Senior Developer | Engineering | Active" },
137
+ { cells: "Clara Dumont | Product Manager | Product | On leave" },
138
+ { cells: "David Kim | UX Researcher | Design | Active" },
139
+ { cells: "Emma Laurent | DevOps Engineer | Engineering | Active" },
140
+ { cells: "F\xE9lix Moreau | Data Analyst | Analytics | Active" }
141
+ ] },
142
+ bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
143
+ textColor: { type: String, default: "rgba(var(--text-color), 1)" },
144
+ spacing: { type: String, default: "normal" },
145
+ radius: { type: String, default: "sharp" },
146
+ textDirection: { type: String, default: "ltr" },
147
+ contentWidth: { type: String, default: "normal" },
148
+ textSize: { type: String, default: "md" },
149
+ paddingTop: { type: String, default: "" },
150
+ paddingBottom: { type: String, default: "" },
151
+ marginTop: { type: String, default: "" },
152
+ marginBottom: { type: String, default: "" },
153
+ paddingX: { type: String, default: "" }
154
+ });
155
+ const sectionEl = ref(null);
156
+ const isVisible = ref(false);
157
+ onMounted(() => {
158
+ const obs = new IntersectionObserver(([e]) => {
159
+ if (e.isIntersecting) {
160
+ isVisible.value = true;
161
+ obs.disconnect();
162
+ }
163
+ }, { threshold: 0.05 });
164
+ if (sectionEl.value) obs.observe(sectionEl.value);
165
+ setTimeout(() => {
166
+ if (!isVisible.value) isVisible.value = true;
167
+ }, 300);
168
+ });
169
+ const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-8 md:px-16", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-8 md:px-16");
170
+ 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");
171
+ const titleFontSize = computed(() => ({ sm: "clamp(0.9rem,1vw,1.1rem)", md: "clamp(1rem,1.5vw,1.4rem)", lg: "clamp(2.2rem,5vw,4rem)" })[props.textSize] || "clamp(1rem,1.5vw,1.4rem)");
172
+ const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
173
+ const parsedCols = computed(() => (props.columns || []).map((col) => ({
174
+ label: col.label || "",
175
+ align: col.align || "left",
176
+ badge: col.badge || false
177
+ })));
178
+ const parsedRows = computed(() => (props.items || []).map((row) => ({
179
+ cells: (row.cells || "").split("|").map((c) => c.trim())
180
+ })));
181
+ const query = ref("");
182
+ const sortCol = ref(-1);
183
+ const sortDir = ref("asc");
184
+ const toggleSort = (i) => {
185
+ if (sortCol.value === i) sortDir.value = sortDir.value === "asc" ? "desc" : "asc";
186
+ else {
187
+ sortCol.value = i;
188
+ sortDir.value = "asc";
189
+ }
190
+ };
191
+ const currentPage = ref(1);
192
+ const filteredRows = computed(() => {
193
+ let rows = parsedRows.value;
194
+ if (props.searchable && query.value.trim()) {
195
+ const q = query.value.toLowerCase();
196
+ rows = rows.filter((r) => r.cells.some((c) => c.toLowerCase().includes(q)));
197
+ }
198
+ if (props.sortable && sortCol.value >= 0) {
199
+ rows = [...rows].sort((a, b) => {
200
+ const av = a.cells[sortCol.value] || "";
201
+ const bv = b.cells[sortCol.value] || "";
202
+ const cmp = av.localeCompare(bv, void 0, { numeric: true, sensitivity: "base" });
203
+ return sortDir.value === "asc" ? cmp : -cmp;
204
+ });
205
+ }
206
+ return rows;
207
+ });
208
+ const totalPages = computed(() => props.perPage > 0 ? Math.ceil(filteredRows.value.length / props.perPage) : 1);
209
+ const pageRows = computed(() => {
210
+ if (props.perPage <= 0) return filteredRows.value;
211
+ const start = (currentPage.value - 1) * props.perPage;
212
+ return filteredRows.value.slice(start, start + props.perPage);
213
+ });
214
+ const gridCols = computed(() => {
215
+ if (props.cardColumns === "1") return "grid-cols-1";
216
+ if (props.cardColumns === "2") return "grid-cols-1 md:grid-cols-2";
217
+ if (props.cardColumns === "4") return "grid-cols-1 sm:grid-cols-2 xl:grid-cols-4";
218
+ const n = parsedCols.value.length;
219
+ if (n <= 3) return "grid-cols-1 md:grid-cols-2";
220
+ return "grid-cols-1 md:grid-cols-2 lg:grid-cols-3";
221
+ });
222
+ const SM = { none: "0", xs: "8px", sm: "24px", md: "48px", lg: "80px", xl: "128px", "-xs": "-8px", "-sm": "-24px", "-md": "-48px", "-lg": "-80px", "-xl": "-128px" };
223
+ const spacingStyle = computed(() => {
224
+ const s = {};
225
+ if (props.paddingTop && SM[props.paddingTop]) s.paddingTop = SM[props.paddingTop];
226
+ if (props.paddingBottom && SM[props.paddingBottom]) s.paddingBottom = SM[props.paddingBottom];
227
+ if (props.marginTop && SM[props.marginTop]) s.marginTop = SM[props.marginTop];
228
+ if (props.marginBottom && SM[props.marginBottom]) s.marginBottom = SM[props.marginBottom];
229
+ if (props.paddingX && SM[props.paddingX]) {
230
+ const v = SM[props.paddingX];
231
+ if (v.startsWith("-")) {
232
+ s.marginLeft = v;
233
+ s.marginRight = v;
234
+ } else {
235
+ s.paddingLeft = v;
236
+ s.paddingRight = v;
237
+ }
238
+ }
239
+ return s;
240
+ });
241
+ </script>
242
+
243
+ <style scoped>
244
+ .sv-out{opacity:0;transform:translateY(16px)}.sv-in{opacity:1;transform:none;transition:opacity .6s ease,transform .6s ease}
245
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: any;
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: any;
@@ -0,0 +1,107 @@
1
+ <template>
2
+ <section ref="sectionEl" :class="[sectionPad, isVisible ? 'sv-in' : 'sv-out']" :dir="textDirection" :style="[{ backgroundColor: bgColor, color: textColor }, spacingStyle]">
3
+ <div :class="[containerClass]">
4
+
5
+ <!-- Header -->
6
+ <div class="border-b pb-6 mb-0 flex items-end gap-8 justify-between" :style="{ borderColor: ca(textColor,'15') }">
7
+ <component :is="titleTag" class="font-black uppercase leading-[0.88]" :style="{ fontSize: titleFontSize, letterSpacing: '-0.03em' }">{{ title }}</component>
8
+ <span class="text-[9px] font-black uppercase tracking-[0.4em] pb-1 shrink-0" :style="{ opacity: 0.3 }">Q&amp;A</span>
9
+ </div>
10
+
11
+ <!-- Items -->
12
+ <div class="divide-y" :style="{ borderColor: ca(textColor,'10') }">
13
+ <div v-for="(item, index) in items" :key="index" :style="{ borderColor: ca(textColor,'10') }">
14
+ <button @click="toggle(index)" class="w-full flex items-start justify-between py-7 text-left cursor-pointer gap-8">
15
+ <div class="flex items-start gap-6">
16
+ <span class="text-[11px] font-black tabular-nums pt-0.5 shrink-0" :style="{ opacity: 0.25 }">{{ String(index + 1).padStart(2, '0') }}</span>
17
+ <span class="font-black uppercase tracking-tight leading-snug">{{ item.question }}</span>
18
+ </div>
19
+ <span class="text-xl font-light shrink-0 mt-0.5 transition-transform duration-200" :style="{ transform: openIndex === index ? 'rotate(45deg)' : 'none', opacity: 0.4 }">+</span>
20
+ </button>
21
+ <div v-if="openIndex === index" class="pl-14 pb-7">
22
+ <p :class="[bodyClass, 'leading-relaxed']" :style="{ opacity: 0.6 }">{{ item.answer }}</p>
23
+ </div>
24
+ </div>
25
+
26
+ <!-- Placeholders -->
27
+ <div v-if="!items.length" v-for="n in 3" :key="n" :style="{ borderColor: ca(textColor,'10') }">
28
+ <div class="flex items-center gap-6 py-7">
29
+ <span class="text-[11px] font-black tabular-nums" :style="{ opacity: 0.15 }">0{{ n }}</span>
30
+ <div class="h-4 flex-1" :style="{ backgroundColor: ca(textColor,'10'), width: n === 1 ? '70%' : n === 2 ? '55%' : '65%' }"></div>
31
+ </div>
32
+ </div>
33
+ </div>
34
+
35
+ </div>
36
+ </section>
37
+ </template>
38
+
39
+ <script setup>
40
+ const ca = (c, h) => {
41
+ const m = c?.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
42
+ return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : (c || "") + h;
43
+ };
44
+ import { ref, computed, onMounted } from "vue";
45
+ const props = defineProps({
46
+ title: { type: String, default: "FAQ" },
47
+ titleTag: { type: String, default: "h2" },
48
+ items: { type: Array, default: () => [] },
49
+ bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
50
+ textColor: { type: String, default: "rgba(var(--text-color), 1)" },
51
+ spacing: { type: String, default: "normal" },
52
+ radius: { type: String, default: "sharp" },
53
+ textDirection: { type: String, default: "ltr" },
54
+ contentWidth: { type: String, default: "narrow" },
55
+ textSize: { type: String, default: "md" },
56
+ paddingTop: { type: String, default: "" },
57
+ paddingBottom: { type: String, default: "" },
58
+ marginTop: { type: String, default: "" },
59
+ marginBottom: { type: String, default: "" },
60
+ paddingX: { type: String, default: "" }
61
+ });
62
+ const sectionEl = ref(null);
63
+ const isVisible = ref(false);
64
+ onMounted(() => {
65
+ const obs = new IntersectionObserver(([e]) => {
66
+ if (e.isIntersecting) {
67
+ isVisible.value = true;
68
+ obs.disconnect();
69
+ }
70
+ }, { threshold: 0.06 });
71
+ if (sectionEl.value) obs.observe(sectionEl.value);
72
+ setTimeout(() => {
73
+ if (!isVisible.value) isVisible.value = true;
74
+ }, 300);
75
+ });
76
+ const sectionPad = computed(() => ({ compact: "py-8 px-4 md:px-8", normal: "py-14 px-8 md:px-16", spacious: "py-24 px-8 md:px-16" })[props.spacing] || "py-14 px-8 md:px-16");
77
+ 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");
78
+ const titleFontSize = computed(() => ({ sm: "clamp(1rem,1.5vw,1.3rem)", md: "clamp(1.5rem,2.5vw,2rem)", lg: "clamp(2.5rem,6vw,5rem)" })[props.textSize] || "clamp(1.5rem,2.5vw,2rem)");
79
+ const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
80
+ const openIndex = ref(null);
81
+ const toggle = (index) => {
82
+ openIndex.value = openIndex.value === index ? null : index;
83
+ };
84
+ const SM = { none: "0", xs: "8px", sm: "24px", md: "48px", lg: "80px", xl: "128px", "-xs": "-8px", "-sm": "-24px", "-md": "-48px", "-lg": "-80px", "-xl": "-128px" };
85
+ const spacingStyle = computed(() => {
86
+ const s = {};
87
+ if (props.paddingTop && SM[props.paddingTop]) s.paddingTop = SM[props.paddingTop];
88
+ if (props.paddingBottom && SM[props.paddingBottom]) s.paddingBottom = SM[props.paddingBottom];
89
+ if (props.marginTop && SM[props.marginTop]) s.marginTop = SM[props.marginTop];
90
+ if (props.marginBottom && SM[props.marginBottom]) s.marginBottom = SM[props.marginBottom];
91
+ if (props.paddingX && SM[props.paddingX]) {
92
+ const v = SM[props.paddingX];
93
+ if (v.startsWith("-")) {
94
+ s.marginLeft = v;
95
+ s.marginRight = v;
96
+ } else {
97
+ s.paddingLeft = v;
98
+ s.paddingRight = v;
99
+ }
100
+ }
101
+ return s;
102
+ });
103
+ </script>
104
+
105
+ <style scoped>
106
+ .sv-out{opacity:0;transform:translateY(16px)}.sv-in{opacity:1;transform:none;transition:opacity .6s ease,transform .6s ease}
107
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: any;
@@ -0,0 +1,3 @@
1
+ declare const _default: typeof __VLS_export;
2
+ export default _default;
3
+ declare const __VLS_export: any;