cinqcinqdev-seo 0.1.15 → 0.1.17

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 (36) hide show
  1. package/README.md +554 -113
  2. package/dist/module.d.mts +6 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +14 -6
  5. package/dist/runtime/assets/admin-tw.css +1 -1
  6. package/dist/runtime/components/admin/Navbar.vue +6 -5
  7. package/dist/runtime/components/admin/Sidebar.vue +21 -8
  8. package/dist/runtime/components/sections/ContactForm.vue +14 -10
  9. package/dist/runtime/components/sections/FAQ.vue +10 -6
  10. package/dist/runtime/components/sections/Features.vue +10 -5
  11. package/dist/runtime/components/sections/HeroSection.vue +12 -8
  12. package/dist/runtime/components/sections/Pricing.vue +8 -5
  13. package/dist/runtime/components/sections/Process.vue +9 -4
  14. package/dist/runtime/components/sections/ServiceGrid.vue +10 -5
  15. package/dist/runtime/components/sections/Testimonials.vue +12 -7
  16. package/dist/runtime/components/sections/TextVisual.vue +8 -3
  17. package/dist/runtime/composables/useAdminBasePath.d.ts +1 -0
  18. package/dist/runtime/composables/useAdminBasePath.js +5 -0
  19. package/dist/runtime/composables/useAdminSections.d.ts +0 -7
  20. package/dist/runtime/composables/useAdminSections.js +68 -17
  21. package/dist/runtime/composables/useAdminSettings.d.ts +31 -0
  22. package/dist/runtime/composables/useAdminSettings.js +31 -0
  23. package/dist/runtime/middleware/admin-auth.js +3 -2
  24. package/dist/runtime/pages/admin/editor/[id].vue +10 -5
  25. package/dist/runtime/pages/admin/index.vue +4 -2
  26. package/dist/runtime/pages/admin/pages/[type].vue +5 -3
  27. package/dist/runtime/pages/admin/personas/[id].vue +3 -2
  28. package/dist/runtime/pages/admin/personas/index.vue +3 -2
  29. package/dist/runtime/pages/admin/setup.d.vue.ts +3 -0
  30. package/dist/runtime/pages/admin/setup.vue +220 -0
  31. package/dist/runtime/pages/admin/setup.vue.d.ts +3 -0
  32. package/dist/runtime/pages/admin/strategies/[id].vue +3 -2
  33. package/dist/runtime/pages/admin/strategies/index.vue +3 -2
  34. package/dist/runtime/server/api/ai/generate.post.js +12 -6
  35. package/dist/runtime/server/api/ai/seo-audit.post.js +12 -6
  36. package/package.json +4 -1
@@ -10,7 +10,7 @@
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
11
  <p class="text-sm 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
- <div v-if="image" class="mt-14 mx-auto aspect-video rounded-3xl overflow-hidden max-w-3xl">
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" />
15
15
  </div>
16
16
  </div>
@@ -25,7 +25,7 @@
25
25
  <p class="text-sm 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
- <div class="aspect-[4/3] rounded-3xl overflow-hidden">
28
+ <div :class="['aspect-[4/3] overflow-hidden', cardRound]">
29
29
  <img v-if="image" :src="image" class="w-full h-full object-cover" />
30
30
  <div v-else class="w-full h-full" :style="{ backgroundColor: textColor + '06' }"></div>
31
31
  </div>
@@ -35,7 +35,7 @@
35
35
  <!-- BENTO -->
36
36
  <div v-else-if="layout === 'bento'" class="p-3 md:p-5">
37
37
  <div class="max-w-6xl mx-auto grid grid-cols-12 gap-3">
38
- <div class="col-span-12 md:col-span-7 rounded-[28px] p-8 md:p-10 border flex flex-col justify-between min-h-[320px]" :style="{ borderColor: textColor + '10', backgroundColor: textColor + '04' }">
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: textColor + '10', backgroundColor: 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>
@@ -43,7 +43,7 @@
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>
46
- <div class="col-span-12 md:col-span-5 rounded-[28px] overflow-hidden min-h-[260px] md:min-h-[320px]">
46
+ <div :class="['col-span-12 md:col-span-5 overflow-hidden min-h-[260px] md:min-h-[320px]', cardRound]">
47
47
  <img v-if="image" :src="image" class="w-full h-full object-cover" />
48
48
  <div v-else class="w-full h-full" :style="{ backgroundColor: textColor + '08' }"></div>
49
49
  </div>
@@ -76,7 +76,7 @@
76
76
  <span class="text-[9px] font-black uppercase tracking-[0.5em] opacity-20">{{ ctaText }}</span>
77
77
  </div>
78
78
  <component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.85] mb-10" :style="{ fontSize: 'clamp(3.5rem,10vw,9rem)' }">{{ title }}</component>
79
- <div v-if="image" class="mb-10 overflow-hidden rounded-2xl aspect-video">
79
+ <div v-if="image" :class="['mb-10 overflow-hidden aspect-video', cardRound]">
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: textColor + '10' }">
@@ -95,7 +95,7 @@
95
95
  </div>
96
96
  <div class="flex flex-col md:flex-row md:items-end gap-8 mb-8">
97
97
  <component :is="titleTag" class="font-black uppercase tracking-tighter leading-[0.85] flex-1" :style="{ fontSize: 'clamp(3rem,9vw,9rem)' }">{{ title }}</component>
98
- <div v-if="image" class="md:w-56 md:shrink-0 aspect-[3/4] rounded-3xl overflow-hidden">
98
+ <div v-if="image" :class="['md:w-56 md:shrink-0 aspect-[3/4] overflow-hidden', cardRound]">
99
99
  <img :src="image" class="w-full h-full object-cover" />
100
100
  </div>
101
101
  </div>
@@ -110,7 +110,8 @@
110
110
  </template>
111
111
 
112
112
  <script setup>
113
- defineProps({
113
+ import { computed } from "vue";
114
+ const props = defineProps({
114
115
  layout: { type: String, default: "centered" },
115
116
  animation: { type: String, default: "fadeUp" },
116
117
  titleSize: { type: String, default: "md" },
@@ -124,6 +125,9 @@ defineProps({
124
125
  imageStyle: { type: String, default: "cover" },
125
126
  imagePosition: { type: String, default: "center" },
126
127
  bgColor: { type: String, default: "#3d35ff" },
127
- textColor: { type: String, default: "#ffffff" }
128
+ textColor: { type: String, default: "#ffffff" },
129
+ spacing: { type: String, default: "normal" },
130
+ radius: { type: String, default: "rounded" }
128
131
  });
132
+ const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
129
133
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <section class="py-14 px-6 md:px-12" :style="{ backgroundColor: bgColor, color: textColor }">
2
+ <section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
3
3
  <div class="max-w-6xl mx-auto">
4
4
 
5
5
  <div v-if="title || description" class="mb-12 text-center max-w-2xl mx-auto">
@@ -11,8 +11,7 @@
11
11
  <!-- CARDS -->
12
12
  <div v-if="layout === 'cards'" class="grid grid-cols-1 md:grid-cols-3 gap-6 items-end">
13
13
  <div v-for="(plan, i) in items" :key="i"
14
- class="rounded-3xl border p-8 flex flex-col transition-all duration-300 relative"
15
- :class="plan.highlighted ? 'md:-translate-y-4 shadow-2xl' : ''"
14
+ :class="['border p-8 flex flex-col transition-all duration-300 relative', cardRound, plan.highlighted ? 'md:-translate-y-4 shadow-2xl' : '']"
16
15
  :style="plan.highlighted ? { backgroundColor: textColor, color: bgColor, borderColor: textColor } : { backgroundColor: textColor + '04', borderColor: textColor + '12' }">
17
16
  <div v-if="plan.highlighted" class="absolute -top-3 left-1/2 -translate-x-1/2">
18
17
  <span class="px-4 py-1 rounded-full text-[9px] font-black uppercase tracking-widest" :style="{ backgroundColor: bgColor, color: textColor }">Populaire</span>
@@ -63,7 +62,7 @@
63
62
  <thead>
64
63
  <tr class="border-b" :style="{ borderColor: textColor + '12' }">
65
64
  <th class="text-left py-4 pr-6 font-black uppercase tracking-widest text-[10px]" :style="{ opacity: 0.4 }">Plan</th>
66
- <th v-for="(plan, i) in items" :key="i" class="text-center py-4 px-4 font-black uppercase tracking-tight text-sm" :class="plan.highlighted ? '' : ''" :style="plan.highlighted ? { color: textColor } : { opacity: 0.7 }">
65
+ <th v-for="(plan, i) in items" :key="i" class="text-center py-4 px-4 font-black uppercase tracking-tight text-sm" :style="plan.highlighted ? { color: textColor } : { opacity: 0.7 }">
67
66
  {{ plan.name }}<br/><span class="text-2xl font-black tracking-tighter">{{ plan.price }}</span>
68
67
  </th>
69
68
  </tr>
@@ -94,8 +93,12 @@ const props = defineProps({
94
93
  description: { type: String, default: "" },
95
94
  items: { type: Array, default: () => [] },
96
95
  bgColor: { type: String, default: "#ffffff" },
97
- textColor: { type: String, default: "#000000" }
96
+ textColor: { type: String, default: "#000000" },
97
+ spacing: { type: String, default: "normal" },
98
+ radius: { type: String, default: "rounded" }
98
99
  });
100
+ 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");
101
+ const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
99
102
  const allFeatures = computed(() => {
100
103
  const set = /* @__PURE__ */ new Set();
101
104
  props.items.forEach((p) => (p.features || []).forEach((f) => set.add(typeof f === "object" ? f.fr || JSON.stringify(f) : f)));
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <section class="py-14 px-6 md:px-12" :style="{ backgroundColor: bgColor, color: textColor }">
2
+ <section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
3
3
  <div class="max-w-6xl mx-auto">
4
4
 
5
5
  <div v-if="title || description" class="mb-12" :class="['horizontal','cards'].includes(layout) ? 'text-center max-w-2xl mx-auto' : 'max-w-xl'">
@@ -36,7 +36,7 @@
36
36
 
37
37
  <!-- CARDS -->
38
38
  <div v-else-if="layout === 'cards'" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
39
- <div v-for="(step, i) in items" :key="i" class="p-7 rounded-3xl border" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '10' }">
39
+ <div v-for="(step, i) in items" :key="i" :class="['p-7 border', cardRound]" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '10' }">
40
40
  <div class="w-10 h-10 rounded-full flex items-center justify-center text-[11px] font-black mb-5" :style="{ backgroundColor: textColor, color: bgColor }">
41
41
  {{ step.icon || String(i+1).padStart(2,'0') }}
42
42
  </div>
@@ -76,7 +76,8 @@
76
76
  </template>
77
77
 
78
78
  <script setup>
79
- defineProps({
79
+ import { computed } from "vue";
80
+ const props = defineProps({
80
81
  layout: { type: String, default: "steps" },
81
82
  title: { type: String, default: "How it works" },
82
83
  titleTag: { type: String, default: "h2" },
@@ -85,6 +86,10 @@ defineProps({
85
86
  itemTag: { type: String, default: "h3" },
86
87
  items: { type: Array, default: () => [] },
87
88
  bgColor: { type: String, default: "#ffffff" },
88
- textColor: { type: String, default: "#000000" }
89
+ textColor: { type: String, default: "#000000" },
90
+ spacing: { type: String, default: "normal" },
91
+ radius: { type: String, default: "rounded" }
89
92
  });
93
+ 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");
94
+ const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
90
95
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <section class="py-14 px-6 md:px-12" :style="{ backgroundColor: bgColor, color: textColor }">
2
+ <section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
3
3
  <div class="max-w-6xl mx-auto">
4
4
 
5
5
  <div v-if="title || description" class="mb-12" :class="headerAlign === 'center' ? 'text-center max-w-2xl mx-auto' : 'max-w-xl'">
@@ -18,7 +18,7 @@
18
18
  <!-- GRID -->
19
19
  <div v-if="layout === 'grid'" class="grid grid-cols-1 md:grid-cols-3 gap-6">
20
20
  <div v-for="(service, idx) in items" :key="idx"
21
- class="group relative p-8 rounded-3xl border transition-all duration-300 overflow-hidden hover:-translate-y-1 hover:shadow-lg"
21
+ :class="['group relative p-8 border transition-all duration-300 overflow-hidden hover:-translate-y-1 hover:shadow-lg', cardRound]"
22
22
  :style="{ backgroundColor: textColor + '04', borderColor: textColor + '12' }">
23
23
  <span class="absolute -top-6 -right-2 text-[100px] font-black italic opacity-[0.04] select-none">{{ String(idx+1).padStart(2,'0') }}</span>
24
24
  <img v-if="service.image" :src="service.image" class="w-10 h-10 object-contain mb-5 rounded-xl" />
@@ -46,7 +46,7 @@
46
46
  <!-- CARDS -->
47
47
  <div v-else-if="layout === 'cards'" class="grid grid-cols-1 md:grid-cols-2 gap-6">
48
48
  <div v-for="(service, idx) in items" :key="idx"
49
- class="flex gap-6 p-7 rounded-3xl border transition-all hover:shadow-md"
49
+ :class="['flex gap-6 p-7 border transition-all hover:shadow-md', cardRound]"
50
50
  :style="{ backgroundColor: textColor + '03', borderColor: textColor + '10' }">
51
51
  <div class="w-12 h-12 rounded-2xl flex items-center justify-center shrink-0" :style="{ backgroundColor: textColor + '10' }">
52
52
  <img v-if="service.image" :src="service.image" class="w-7 h-7 object-contain" />
@@ -76,7 +76,8 @@
76
76
  </template>
77
77
 
78
78
  <script setup>
79
- defineProps({
79
+ import { computed } from "vue";
80
+ const props = defineProps({
80
81
  layout: { type: String, default: "grid" },
81
82
  title: { type: String, default: "Our Services" },
82
83
  titleTag: { type: String, default: "h2" },
@@ -87,6 +88,10 @@ defineProps({
87
88
  items: { type: Array, default: () => [] },
88
89
  ctas: { type: Array, default: () => [] },
89
90
  bgColor: { type: String, default: "#ffffff" },
90
- textColor: { type: String, default: "#000000" }
91
+ textColor: { type: String, default: "#000000" },
92
+ spacing: { type: String, default: "normal" },
93
+ radius: { type: String, default: "rounded" }
91
94
  });
95
+ 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");
96
+ const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
92
97
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <section class="py-14 px-6 md:px-12" :style="{ backgroundColor: bgColor, color: textColor }">
2
+ <section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
3
3
  <div class="max-w-6xl mx-auto">
4
4
 
5
5
  <div v-if="title || description" class="mb-12 max-w-xl">
@@ -10,7 +10,7 @@
10
10
 
11
11
  <!-- GRID -->
12
12
  <div v-if="layout === 'grid'" class="grid grid-cols-1 md:grid-cols-3 gap-6">
13
- <div v-for="(item, i) in items" :key="i" class="flex flex-col p-7 rounded-3xl border transition-all hover:-translate-y-1" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '10' }">
13
+ <div v-for="(item, i) in items" :key="i" :class="['flex flex-col p-7 border transition-all hover:-translate-y-1', cardRound]" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '10' }">
14
14
  <div v-if="item.rating" class="flex gap-0.5 mb-4">
15
15
  <span v-for="s in 5" :key="s" class="text-xs" :style="{ opacity: s <= item.rating ? 1 : 0.15 }">★</span>
16
16
  </div>
@@ -28,7 +28,7 @@
28
28
 
29
29
  <!-- FEATURED -->
30
30
  <div v-else-if="layout === 'featured'">
31
- <div v-if="items[0]" class="p-10 rounded-[32px] border mb-6" :style="{ backgroundColor: textColor + '05', borderColor: textColor + '10' }">
31
+ <div v-if="items[0]" :class="['p-10 border mb-6', cardRound]" :style="{ backgroundColor: textColor + '05', borderColor: textColor + '10' }">
32
32
  <div v-if="items[0].rating" class="flex gap-0.5 mb-6">
33
33
  <span v-for="s in 5" :key="s" class="text-base" :style="{ opacity: s <= items[0].rating ? 1 : 0.15 }">★</span>
34
34
  </div>
@@ -43,7 +43,7 @@
43
43
  </div>
44
44
  </div>
45
45
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
46
- <div v-for="(item, i) in items.slice(1)" :key="i" class="p-7 rounded-3xl border" :style="{ backgroundColor: textColor + '03', borderColor: textColor + '08' }">
46
+ <div v-for="(item, i) in items.slice(1)" :key="i" :class="['p-7 border', cardRound]" :style="{ backgroundColor: textColor + '03', borderColor: textColor + '08' }">
47
47
  <p class="text-sm italic leading-relaxed mb-5" :style="{ opacity: 0.7 }">"{{ item.quote }}"</p>
48
48
  <div class="flex items-center gap-3">
49
49
  <img v-if="item.avatar" :src="item.avatar" class="w-8 h-8 rounded-full object-cover" />
@@ -59,7 +59,7 @@
59
59
 
60
60
  <!-- WALL / MINIMAL -->
61
61
  <div v-else class="columns-1 md:columns-2 lg:columns-3 gap-6 space-y-6">
62
- <div v-for="(item, i) in items" :key="i" class="break-inside-avoid p-6 rounded-2xl border mb-6" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '08' }">
62
+ <div v-for="(item, i) in items" :key="i" :class="['break-inside-avoid p-6 border mb-6', cardRound]" :style="{ backgroundColor: textColor + '04', borderColor: textColor + '08' }">
63
63
  <div v-if="item.rating" class="flex gap-0.5 mb-3">
64
64
  <span v-for="s in 5" :key="s" class="text-[10px]" :style="{ opacity: s <= item.rating ? 1 : 0.15 }">★</span>
65
65
  </div>
@@ -77,7 +77,8 @@
77
77
  </template>
78
78
 
79
79
  <script setup>
80
- defineProps({
80
+ import { computed } from "vue";
81
+ const props = defineProps({
81
82
  layout: { type: String, default: "grid" },
82
83
  title: { type: String, default: "What our clients say" },
83
84
  titleTag: { type: String, default: "h2" },
@@ -85,6 +86,10 @@ defineProps({
85
86
  description: { type: String, default: "" },
86
87
  items: { type: Array, default: () => [] },
87
88
  bgColor: { type: String, default: "#ffffff" },
88
- textColor: { type: String, default: "#000000" }
89
+ textColor: { type: String, default: "#000000" },
90
+ spacing: { type: String, default: "normal" },
91
+ radius: { type: String, default: "rounded" }
89
92
  });
93
+ 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");
94
+ const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
90
95
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <section class="py-14 px-6 md:px-12" :style="{ backgroundColor: bgColor, color: textColor }">
2
+ <section :class="[sectionPad]" :style="{ backgroundColor: bgColor, color: textColor }">
3
3
 
4
4
  <!-- SPLIT -->
5
5
  <div v-if="layout === 'split'" :class="['max-w-6xl mx-auto flex flex-col md:flex-row gap-12 items-center', reverse ? 'md:flex-row-reverse' : '']">
@@ -80,10 +80,15 @@ const props = defineProps({
80
80
  reverse: { type: Boolean, default: false },
81
81
  bgColor: { type: String, default: "#ffffff" },
82
82
  textColor: { type: String, default: "#000000" },
83
- ctas: { type: Array, default: () => [] }
83
+ ctas: { type: Array, default: () => [] },
84
+ spacing: { type: String, default: "normal" },
85
+ radius: { type: String, default: "rounded" }
84
86
  });
87
+ 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");
88
+ const cardRound = computed(() => ({ sharp: "rounded-xl", rounded: "rounded-3xl", pill: "rounded-[40px]" })[props.radius] || "rounded-3xl");
85
89
  const imageContainerClass = computed(() => {
86
- const styles = { rounded: "aspect-[4/3] rounded-3xl", square: "aspect-square rounded-xl", circle: "aspect-square rounded-full max-w-[360px] mx-auto", portrait: "aspect-[3/4] rounded-3xl max-w-sm mx-auto" };
90
+ const r = cardRound.value;
91
+ const styles = { rounded: `aspect-[4/3] ${r}`, square: "aspect-square rounded-xl", circle: "aspect-square rounded-full max-w-[360px] mx-auto", portrait: `aspect-[3/4] ${r} max-w-sm mx-auto` };
87
92
  return styles[props.imageStyle] ?? styles.rounded;
88
93
  });
89
94
  </script>
@@ -0,0 +1 @@
1
+ export declare const useAdminBasePath: () => string;
@@ -0,0 +1,5 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ export const useAdminBasePath = () => {
3
+ const config = useRuntimeConfig().public.adminCms;
4
+ return config?.basePath || "/admin";
5
+ };
@@ -1,10 +1,3 @@
1
- /**
2
- * Default section definitions for the visual editor.
3
- * Keys must match component names in the consuming app's components/sections/ directory.
4
- * e.g. HeroSection → components/sections/HeroSection.vue → <SectionsHeroSection />
5
- *
6
- * Add your own sections via the `extraSections` module option in nuxt.config.ts.
7
- */
8
1
  export declare const DEFAULT_SECTIONS: Record<string, any>;
9
2
  export declare const useAdminSections: () => {
10
3
  [x: string]: any;
@@ -23,6 +23,16 @@ const animationOptions = [
23
23
  { value: "typewriter", label: "Typewriter", icon: "\u258C" },
24
24
  { value: "float", label: "Float", icon: "\u301C" }
25
25
  ];
26
+ const spacingOptions = [
27
+ { value: "compact", label: "Compact", icon: "\u2194" },
28
+ { value: "normal", label: "Normal", icon: "\u27F7" },
29
+ { value: "spacious", label: "Large", icon: "\u2195" }
30
+ ];
31
+ const radiusOptions = [
32
+ { value: "sharp", label: "Sharp", icon: "\u25A1" },
33
+ { value: "rounded", label: "Rounded", icon: "\u25A2" },
34
+ { value: "pill", label: "Pill", icon: "\u25EF" }
35
+ ];
26
36
  const i18n0 = (fr = "") => ({ fr, ar: "", en: "" });
27
37
  export const DEFAULT_SECTIONS = {
28
38
  HeroSection: {
@@ -42,7 +52,9 @@ export const DEFAULT_SECTIONS = {
42
52
  bgColor: "#3d35ff",
43
53
  textColor: "#ffffff",
44
54
  ctaText: i18n0("Get Started"),
45
- ctaLink: "/"
55
+ ctaLink: "/",
56
+ spacing: "normal",
57
+ radius: "rounded"
46
58
  },
47
59
  groups: [
48
60
  { key: "design", label: "Design" },
@@ -71,6 +83,7 @@ export const DEFAULT_SECTIONS = {
71
83
  ctaLink: { type: "text", label: "Button URL", group: "design" },
72
84
  bgColor: { type: "color", label: "Background", group: "design" },
73
85
  textColor: { type: "color", label: "Text Color", group: "design" },
86
+ radius: { type: "cards", label: "Arrondis", group: "design", options: radiusOptions },
74
87
  animation: { type: "cards", label: "Animation", group: "animation", options: animationOptions }
75
88
  }
76
89
  },
@@ -91,7 +104,9 @@ export const DEFAULT_SECTIONS = {
91
104
  reverse: false,
92
105
  bgColor: "#ffffff",
93
106
  textColor: "#000000",
94
- ctas: []
107
+ ctas: [],
108
+ spacing: "normal",
109
+ radius: "rounded"
95
110
  },
96
111
  fields: {
97
112
  layout: { type: "cards", label: "Layout", options: [
@@ -121,7 +136,9 @@ export const DEFAULT_SECTIONS = {
121
136
  link: { type: "text", label: "URL" }
122
137
  } },
123
138
  bgColor: { type: "color", label: "Background" },
124
- textColor: { type: "color", label: "Text Color" }
139
+ textColor: { type: "color", label: "Text Color" },
140
+ spacing: { type: "cards", label: "Espacement", options: spacingOptions },
141
+ radius: { type: "cards", label: "Arrondis", options: radiusOptions }
125
142
  }
126
143
  },
127
144
  ServiceGrid: {
@@ -138,7 +155,9 @@ export const DEFAULT_SECTIONS = {
138
155
  itemTag: "h3",
139
156
  items: [],
140
157
  bgColor: "#ffffff",
141
- textColor: "#000000"
158
+ textColor: "#000000",
159
+ spacing: "normal",
160
+ radius: "rounded"
142
161
  },
143
162
  fields: {
144
163
  layout: { type: "cards", label: "Layout", options: [
@@ -161,7 +180,9 @@ export const DEFAULT_SECTIONS = {
161
180
  image: { type: "image", label: "Icon/Image" }
162
181
  } },
163
182
  bgColor: { type: "color", label: "Background" },
164
- textColor: { type: "color", label: "Text Color" }
183
+ textColor: { type: "color", label: "Text Color" },
184
+ spacing: { type: "cards", label: "Espacement", options: spacingOptions },
185
+ radius: { type: "cards", label: "Arrondis", options: radiusOptions }
165
186
  }
166
187
  },
167
188
  Features: {
@@ -171,7 +192,9 @@ export const DEFAULT_SECTIONS = {
171
192
  title: i18n0("Why choose us?"),
172
193
  items: [],
173
194
  bgColor: "#ffffff",
174
- textColor: "#000000"
195
+ textColor: "#000000",
196
+ spacing: "normal",
197
+ radius: "rounded"
175
198
  },
176
199
  fields: {
177
200
  title: { type: "text", label: "Section Title", i18n: true },
@@ -180,7 +203,9 @@ export const DEFAULT_SECTIONS = {
180
203
  description: { type: "textarea", label: "Description", i18n: true }
181
204
  } },
182
205
  bgColor: { type: "color", label: "Background" },
183
- textColor: { type: "color", label: "Text Color" }
206
+ textColor: { type: "color", label: "Text Color" },
207
+ spacing: { type: "cards", label: "Espacement", options: spacingOptions },
208
+ radius: { type: "cards", label: "Arrondis", options: radiusOptions }
184
209
  }
185
210
  },
186
211
  FAQ: {
@@ -190,14 +215,18 @@ export const DEFAULT_SECTIONS = {
190
215
  title: i18n0("FAQ"),
191
216
  items: [],
192
217
  bgColor: "#F9F9FB",
193
- textColor: "#000000"
218
+ textColor: "#000000",
219
+ spacing: "normal",
220
+ radius: "rounded"
194
221
  },
195
222
  fields: {
196
223
  title: { type: "text", label: "Title", i18n: true },
197
224
  items: { type: "list", label: "Questions", itemFields: {
198
225
  question: { type: "text", label: "Question", i18n: true },
199
226
  answer: { type: "textarea", label: "Answer", i18n: true }
200
- } }
227
+ } },
228
+ spacing: { type: "cards", label: "Espacement", options: spacingOptions },
229
+ radius: { type: "cards", label: "Arrondis", options: radiusOptions }
201
230
  }
202
231
  },
203
232
  Process: {
@@ -211,7 +240,9 @@ export const DEFAULT_SECTIONS = {
211
240
  description: i18n0(),
212
241
  items: [],
213
242
  bgColor: "#ffffff",
214
- textColor: "#000000"
243
+ textColor: "#000000",
244
+ spacing: "normal",
245
+ radius: "rounded"
215
246
  },
216
247
  fields: {
217
248
  layout: { type: "cards", label: "Layout", options: [
@@ -231,7 +262,9 @@ export const DEFAULT_SECTIONS = {
231
262
  description: { type: "textarea", label: "Description", i18n: true }
232
263
  } },
233
264
  bgColor: { type: "color", label: "Background" },
234
- textColor: { type: "color", label: "Text Color" }
265
+ textColor: { type: "color", label: "Text Color" },
266
+ spacing: { type: "cards", label: "Espacement", options: spacingOptions },
267
+ radius: { type: "cards", label: "Arrondis", options: radiusOptions }
235
268
  }
236
269
  },
237
270
  Testimonials: {
@@ -245,7 +278,9 @@ export const DEFAULT_SECTIONS = {
245
278
  description: i18n0(),
246
279
  items: [],
247
280
  bgColor: "#ffffff",
248
- textColor: "#000000"
281
+ textColor: "#000000",
282
+ spacing: "normal",
283
+ radius: "rounded"
249
284
  },
250
285
  fields: {
251
286
  layout: { type: "cards", label: "Layout", options: [
@@ -267,7 +302,9 @@ export const DEFAULT_SECTIONS = {
267
302
  avatar: { type: "image", label: "Photo" }
268
303
  } },
269
304
  bgColor: { type: "color", label: "Background" },
270
- textColor: { type: "color", label: "Text Color" }
305
+ textColor: { type: "color", label: "Text Color" },
306
+ spacing: { type: "cards", label: "Espacement", options: spacingOptions },
307
+ radius: { type: "cards", label: "Arrondis", options: radiusOptions }
271
308
  }
272
309
  },
273
310
  Pricing: {
@@ -281,7 +318,9 @@ export const DEFAULT_SECTIONS = {
281
318
  description: i18n0(),
282
319
  items: [],
283
320
  bgColor: "#ffffff",
284
- textColor: "#000000"
321
+ textColor: "#000000",
322
+ spacing: "normal",
323
+ radius: "rounded"
285
324
  },
286
325
  fields: {
287
326
  layout: { type: "cards", label: "Layout", options: [
@@ -303,27 +342,39 @@ export const DEFAULT_SECTIONS = {
303
342
  highlighted: { type: "select", label: "Highlight", options: [{ value: false, label: "No" }, { value: true, label: "Yes" }] }
304
343
  } },
305
344
  bgColor: { type: "color", label: "Background" },
306
- textColor: { type: "color", label: "Text Color" }
345
+ textColor: { type: "color", label: "Text Color" },
346
+ spacing: { type: "cards", label: "Espacement", options: spacingOptions },
347
+ radius: { type: "cards", label: "Arrondis", options: radiusOptions }
307
348
  }
308
349
  },
309
350
  ContactForm: {
310
351
  label: "Contact Form",
311
352
  icon: "\u{1F4E9}",
312
353
  defaultProps: {
354
+ layout: "split",
313
355
  title: i18n0("Let's talk"),
314
356
  subtitle: i18n0("Describe your project and we'll get back to you within 24h."),
315
357
  badge: i18n0(),
316
358
  button_text: i18n0("Send"),
317
359
  bgColor: "#ffffff",
318
- textColor: "#000000"
360
+ textColor: "#000000",
361
+ spacing: "normal",
362
+ radius: "rounded"
319
363
  },
320
364
  fields: {
365
+ layout: { type: "cards", label: "Layout", options: [
366
+ { value: "split", label: "Split", icon: "\u25E7" },
367
+ { value: "centered", label: "Centered", icon: "\u25C9" },
368
+ { value: "minimal", label: "Minimal", icon: "\u2014" }
369
+ ] },
321
370
  badge: { type: "text", label: "Badge", i18n: true },
322
371
  title: { type: "text", label: "Title", i18n: true },
323
372
  subtitle: { type: "text", label: "Subtitle", i18n: true },
324
373
  button_text: { type: "text", label: "Button", i18n: true },
325
374
  bgColor: { type: "color", label: "Background" },
326
- textColor: { type: "color", label: "Text Color" }
375
+ textColor: { type: "color", label: "Text Color" },
376
+ spacing: { type: "cards", label: "Espacement", options: spacingOptions },
377
+ radius: { type: "cards", label: "Arrondis", options: radiusOptions }
327
378
  }
328
379
  }
329
380
  };
@@ -0,0 +1,31 @@
1
+ export interface SectionPreset {
2
+ layout?: string;
3
+ spacing?: string;
4
+ radius?: string;
5
+ animation?: string;
6
+ bgColor?: string;
7
+ textColor?: string;
8
+ }
9
+ export interface BrandSettings {
10
+ id: number;
11
+ brand_name?: string;
12
+ tagline?: string;
13
+ vision?: string;
14
+ logo_url?: string;
15
+ primary_color?: string;
16
+ secondary_color?: string;
17
+ accent_color?: string;
18
+ font_headline?: string;
19
+ font_body?: string;
20
+ ai_context?: string;
21
+ section_presets?: Record<string, SectionPreset>;
22
+ updated_at?: string;
23
+ }
24
+ export declare const useAdminSettings: () => {
25
+ settings: any;
26
+ loading: any;
27
+ saving: any;
28
+ load: () => Promise<void>;
29
+ save: (updates: Partial<BrandSettings>) => Promise<void>;
30
+ applyPresets: (sectionType: string, defaultProps: Record<string, any>) => Record<string, any>;
31
+ };
@@ -0,0 +1,31 @@
1
+ import { ref } from "#imports";
2
+ import { useSupabaseClient } from "#imports";
3
+ export const useAdminSettings = () => {
4
+ const supabase = useSupabaseClient();
5
+ const settings = ref(null);
6
+ const loading = ref(false);
7
+ const saving = ref(false);
8
+ const load = async () => {
9
+ loading.value = true;
10
+ const { data } = await supabase.from("brand_settings").select("*").eq("id", 1).maybeSingle();
11
+ settings.value = data || { id: 1 };
12
+ loading.value = false;
13
+ };
14
+ const save = async (updates) => {
15
+ saving.value = true;
16
+ await supabase.from("brand_settings").upsert({ id: 1, ...updates });
17
+ settings.value = { ...settings.value || { id: 1 }, ...updates };
18
+ saving.value = false;
19
+ };
20
+ const applyPresets = (sectionType, defaultProps) => {
21
+ const presets = settings.value?.section_presets?.[sectionType];
22
+ if (!presets) return defaultProps;
23
+ const merged = { ...defaultProps };
24
+ const styleKeys = ["layout", "spacing", "radius", "animation", "bgColor", "textColor"];
25
+ for (const key of styleKeys) {
26
+ if (presets[key] !== void 0) merged[key] = presets[key];
27
+ }
28
+ return merged;
29
+ };
30
+ return { settings, loading, saving, load, save, applyPresets };
31
+ };
@@ -4,10 +4,11 @@ export default defineNuxtRouteMiddleware((to) => {
4
4
  const config = useRuntimeConfig().public.adminCms;
5
5
  const user = useSupabaseUser();
6
6
  const loginRoute = config?.loginRoute || "/login";
7
- if (to.path.startsWith("/admin") && !user.value) {
7
+ const basePath = config?.basePath || "/admin";
8
+ if (to.path.startsWith(basePath) && !user.value) {
8
9
  return navigateTo(loginRoute);
9
10
  }
10
11
  if (to.path === loginRoute && user.value) {
11
- return navigateTo("/admin");
12
+ return navigateTo(basePath);
12
13
  }
13
14
  });