cinqcinqdev-seo 0.1.72 → 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.
- package/README.md +39 -2
- package/dist/module.json +1 -1
- package/dist/runtime/assets/admin-tw.css +1 -1
- package/dist/runtime/components/admin-cms/DynamicRenderer.vue +16 -2
- package/dist/runtime/components/sections/swiss-style/BlogHero.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/BlogHero.vue +146 -0
- package/dist/runtime/components/sections/swiss-style/BlogHero.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/CallToAction.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/CallToAction.vue +138 -0
- package/dist/runtime/components/sections/swiss-style/CallToAction.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Comparison.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Comparison.vue +280 -0
- package/dist/runtime/components/sections/swiss-style/Comparison.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/ContactForm.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/ContactForm.vue +173 -0
- package/dist/runtime/components/sections/swiss-style/ContactForm.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/DataTable.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/DataTable.vue +245 -0
- package/dist/runtime/components/sections/swiss-style/DataTable.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/FAQ.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/FAQ.vue +107 -0
- package/dist/runtime/components/sections/swiss-style/FAQ.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Features.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Features.vue +103 -0
- package/dist/runtime/components/sections/swiss-style/Features.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/HeroFan.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/HeroFan.vue +108 -0
- package/dist/runtime/components/sections/swiss-style/HeroFan.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/HeroSection.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/HeroSection.vue +190 -0
- package/dist/runtime/components/sections/swiss-style/HeroSection.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/HeroSlider.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/HeroSlider.vue +138 -0
- package/dist/runtime/components/sections/swiss-style/HeroSlider.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Newsletter.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Newsletter.vue +125 -0
- package/dist/runtime/components/sections/swiss-style/Newsletter.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Pricing.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Pricing.vue +148 -0
- package/dist/runtime/components/sections/swiss-style/Pricing.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Process.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Process.vue +124 -0
- package/dist/runtime/components/sections/swiss-style/Process.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Quote.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Quote.vue +130 -0
- package/dist/runtime/components/sections/swiss-style/Quote.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/RelatedPages.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/RelatedPages.vue +212 -0
- package/dist/runtime/components/sections/swiss-style/RelatedPages.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/RichText.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/RichText.vue +100 -0
- package/dist/runtime/components/sections/swiss-style/RichText.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/ServiceGrid.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/ServiceGrid.vue +121 -0
- package/dist/runtime/components/sections/swiss-style/ServiceGrid.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Testimonials.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/Testimonials.vue +126 -0
- package/dist/runtime/components/sections/swiss-style/Testimonials.vue.d.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/TextVisual.d.vue.ts +3 -0
- package/dist/runtime/components/sections/swiss-style/TextVisual.vue +146 -0
- package/dist/runtime/components/sections/swiss-style/TextVisual.vue.d.ts +3 -0
- package/dist/runtime/composables/useAdminSettings.d.ts +1 -0
- package/dist/runtime/pages/admin/editor/[id].vue +15 -1
- package/dist/runtime/pages/admin/setup.vue +77 -2
- package/package.json +1 -1
- /package/dist/runtime/components/sections/{BlogHero.d.vue.ts → cinqcinq/BlogHero.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{BlogHero.vue → cinqcinq/BlogHero.vue} +0 -0
- /package/dist/runtime/components/sections/{BlogHero.vue.d.ts → cinqcinq/BlogHero.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{CallToAction.d.vue.ts → cinqcinq/CallToAction.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{CallToAction.vue → cinqcinq/CallToAction.vue} +0 -0
- /package/dist/runtime/components/sections/{CallToAction.vue.d.ts → cinqcinq/CallToAction.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{Comparison.d.vue.ts → cinqcinq/Comparison.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{Comparison.vue → cinqcinq/Comparison.vue} +0 -0
- /package/dist/runtime/components/sections/{Comparison.vue.d.ts → cinqcinq/Comparison.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{ContactForm.d.vue.ts → cinqcinq/ContactForm.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{ContactForm.vue → cinqcinq/ContactForm.vue} +0 -0
- /package/dist/runtime/components/sections/{ContactForm.vue.d.ts → cinqcinq/ContactForm.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{DataTable.d.vue.ts → cinqcinq/DataTable.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{DataTable.vue → cinqcinq/DataTable.vue} +0 -0
- /package/dist/runtime/components/sections/{DataTable.vue.d.ts → cinqcinq/DataTable.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{FAQ.d.vue.ts → cinqcinq/FAQ.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{FAQ.vue → cinqcinq/FAQ.vue} +0 -0
- /package/dist/runtime/components/sections/{FAQ.vue.d.ts → cinqcinq/FAQ.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{Features.d.vue.ts → cinqcinq/Features.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{Features.vue → cinqcinq/Features.vue} +0 -0
- /package/dist/runtime/components/sections/{Features.vue.d.ts → cinqcinq/Features.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{HeroFan.d.vue.ts → cinqcinq/HeroFan.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{HeroFan.vue → cinqcinq/HeroFan.vue} +0 -0
- /package/dist/runtime/components/sections/{HeroFan.vue.d.ts → cinqcinq/HeroFan.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{HeroSection.d.vue.ts → cinqcinq/HeroSection.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{HeroSection.vue → cinqcinq/HeroSection.vue} +0 -0
- /package/dist/runtime/components/sections/{HeroSection.vue.d.ts → cinqcinq/HeroSection.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{HeroSlider.d.vue.ts → cinqcinq/HeroSlider.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{HeroSlider.vue → cinqcinq/HeroSlider.vue} +0 -0
- /package/dist/runtime/components/sections/{HeroSlider.vue.d.ts → cinqcinq/HeroSlider.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{Newsletter.d.vue.ts → cinqcinq/Newsletter.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{Newsletter.vue → cinqcinq/Newsletter.vue} +0 -0
- /package/dist/runtime/components/sections/{Newsletter.vue.d.ts → cinqcinq/Newsletter.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{Pricing.d.vue.ts → cinqcinq/Pricing.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{Pricing.vue → cinqcinq/Pricing.vue} +0 -0
- /package/dist/runtime/components/sections/{Pricing.vue.d.ts → cinqcinq/Pricing.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{Process.d.vue.ts → cinqcinq/Process.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{Process.vue → cinqcinq/Process.vue} +0 -0
- /package/dist/runtime/components/sections/{Process.vue.d.ts → cinqcinq/Process.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{Quote.d.vue.ts → cinqcinq/Quote.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{Quote.vue → cinqcinq/Quote.vue} +0 -0
- /package/dist/runtime/components/sections/{Quote.vue.d.ts → cinqcinq/Quote.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{RelatedPages.d.vue.ts → cinqcinq/RelatedPages.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{RelatedPages.vue → cinqcinq/RelatedPages.vue} +0 -0
- /package/dist/runtime/components/sections/{RelatedPages.vue.d.ts → cinqcinq/RelatedPages.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{RichText.d.vue.ts → cinqcinq/RichText.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{RichText.vue → cinqcinq/RichText.vue} +0 -0
- /package/dist/runtime/components/sections/{RichText.vue.d.ts → cinqcinq/RichText.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{ServiceGrid.d.vue.ts → cinqcinq/ServiceGrid.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{ServiceGrid.vue → cinqcinq/ServiceGrid.vue} +0 -0
- /package/dist/runtime/components/sections/{ServiceGrid.vue.d.ts → cinqcinq/ServiceGrid.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{Testimonials.d.vue.ts → cinqcinq/Testimonials.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{Testimonials.vue → cinqcinq/Testimonials.vue} +0 -0
- /package/dist/runtime/components/sections/{Testimonials.vue.d.ts → cinqcinq/Testimonials.vue.d.ts} +0 -0
- /package/dist/runtime/components/sections/{TextVisual.d.vue.ts → cinqcinq/TextVisual.d.vue.ts} +0 -0
- /package/dist/runtime/components/sections/{TextVisual.vue → cinqcinq/TextVisual.vue} +0 -0
- /package/dist/runtime/components/sections/{TextVisual.vue.d.ts → cinqcinq/TextVisual.vue.d.ts} +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
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 v-if="title || description" class="border-b pb-6 mb-0" :style="{ borderColor: ca(textColor,'15') }">
|
|
7
|
+
<span v-if="badge" class="block text-[9px] font-black uppercase tracking-[0.5em] mb-3" :style="{ opacity: 0.35 }">{{ badge }}</span>
|
|
8
|
+
<component :is="titleTag" class="font-black uppercase leading-[0.88]" :style="{ fontSize: titleFontSize, letterSpacing: '-0.03em' }">{{ title }}</component>
|
|
9
|
+
<p v-if="description" :class="[bodyClass, 'leading-relaxed mt-3 max-w-xl']" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- GRID -->
|
|
13
|
+
<div v-if="layout === 'grid'" class="grid grid-cols-1 md:grid-cols-3 border divide-x divide-y" :style="{ borderColor: ca(textColor,'12') }">
|
|
14
|
+
<div v-for="(service, idx) in items" :key="idx" class="p-8 hover:bg-black/[0.02] transition-colors" :style="{ borderColor: ca(textColor,'12') }">
|
|
15
|
+
<div class="font-black tabular-nums mb-6" :style="{ fontSize: '1.8rem', opacity: 0.1, letterSpacing: '-0.05em' }">{{ String(idx+1).padStart(2,'0') }}</div>
|
|
16
|
+
<component :is="itemTag" class="text-base font-black uppercase tracking-tight mb-3 leading-tight">{{ service.title }}</component>
|
|
17
|
+
<p class="text-sm leading-relaxed mb-4" :style="{ opacity: 0.5 }">{{ service.description }}</p>
|
|
18
|
+
<p v-if="service.price" class="text-xl font-black tracking-tight mb-4">{{ service.price }}</p>
|
|
19
|
+
<NuxtLink v-if="service.ctaText || service.link" :to="service.link || '#'" class="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-widest transition-all hover:gap-4" :style="{ color: textColor }">{{ service.ctaText || 'En savoir +' }}<span>→</span></NuxtLink>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- LIST -->
|
|
24
|
+
<div v-else-if="layout === 'list'" class="divide-y" :style="{ borderColor: ca(textColor,'10') }">
|
|
25
|
+
<div v-for="(service, idx) in items" :key="idx" class="flex items-start gap-8 py-8" :style="{ borderColor: ca(textColor,'10') }">
|
|
26
|
+
<span class="text-[11px] font-black tabular-nums pt-1 shrink-0" :style="{ opacity: 0.2 }">{{ String(idx+1).padStart(2,'0') }}</span>
|
|
27
|
+
<div class="flex-1 min-w-0">
|
|
28
|
+
<component :is="itemTag" class="text-xl font-black uppercase tracking-tight mb-2">{{ service.title }}</component>
|
|
29
|
+
<p class="text-sm leading-relaxed" :style="{ opacity: 0.5 }">{{ service.description }}</p>
|
|
30
|
+
<p v-if="service.price" class="text-xl font-black tracking-tight mt-3">{{ service.price }}</p>
|
|
31
|
+
</div>
|
|
32
|
+
<NuxtLink v-if="service.ctaText || service.link" :to="service.link || '#'" class="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-widest shrink-0 mt-1 transition-all hover:gap-4" :style="{ color: textColor }">{{ service.ctaText || '→' }}</NuxtLink>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- MINIMAL (default) -->
|
|
37
|
+
<div v-else class="divide-y" :style="{ borderColor: ca(textColor,'10') }">
|
|
38
|
+
<div v-for="(service, idx) in items" :key="idx" class="flex items-start justify-between gap-6 py-6" :style="{ borderColor: ca(textColor,'10') }">
|
|
39
|
+
<div>
|
|
40
|
+
<component :is="itemTag" class="font-black uppercase tracking-tight mb-1">{{ service.title }}</component>
|
|
41
|
+
<p v-if="service.description" class="text-sm" :style="{ opacity: 0.5 }">{{ service.description }}</p>
|
|
42
|
+
</div>
|
|
43
|
+
<p v-if="service.price" class="text-xl font-black tracking-tighter shrink-0">{{ service.price }}</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup>
|
|
52
|
+
const ca = (c, h) => {
|
|
53
|
+
const m = c?.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
|
|
54
|
+
return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : (c || "") + h;
|
|
55
|
+
};
|
|
56
|
+
import { ref, computed, onMounted } from "vue";
|
|
57
|
+
const props = defineProps({
|
|
58
|
+
layout: { type: String, default: "grid" },
|
|
59
|
+
badge: { type: String, default: "" },
|
|
60
|
+
title: { type: String, default: "" },
|
|
61
|
+
titleTag: { type: String, default: "h2" },
|
|
62
|
+
itemTag: { type: String, default: "h3" },
|
|
63
|
+
description: { type: String, default: "" },
|
|
64
|
+
headerAlign: { type: String, default: "left" },
|
|
65
|
+
ctas: { type: Array, default: () => [] },
|
|
66
|
+
items: { type: Array, default: () => [] },
|
|
67
|
+
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
68
|
+
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
69
|
+
spacing: { type: String, default: "normal" },
|
|
70
|
+
radius: { type: String, default: "sharp" },
|
|
71
|
+
textDirection: { type: String, default: "ltr" },
|
|
72
|
+
contentWidth: { type: String, default: "normal" },
|
|
73
|
+
textSize: { type: String, default: "md" },
|
|
74
|
+
paddingTop: { type: String, default: "" },
|
|
75
|
+
paddingBottom: { type: String, default: "" },
|
|
76
|
+
marginTop: { type: String, default: "" },
|
|
77
|
+
marginBottom: { type: String, default: "" },
|
|
78
|
+
paddingX: { type: String, default: "" }
|
|
79
|
+
});
|
|
80
|
+
const sectionEl = ref(null);
|
|
81
|
+
const isVisible = ref(false);
|
|
82
|
+
onMounted(() => {
|
|
83
|
+
const obs = new IntersectionObserver(([e]) => {
|
|
84
|
+
if (e.isIntersecting) {
|
|
85
|
+
isVisible.value = true;
|
|
86
|
+
obs.disconnect();
|
|
87
|
+
}
|
|
88
|
+
}, { threshold: 0.06 });
|
|
89
|
+
if (sectionEl.value) obs.observe(sectionEl.value);
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
if (!isVisible.value) isVisible.value = true;
|
|
92
|
+
}, 300);
|
|
93
|
+
});
|
|
94
|
+
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");
|
|
95
|
+
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");
|
|
96
|
+
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)");
|
|
97
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
98
|
+
const SM = { none: "0", xs: "8px", sm: "24px", md: "48px", lg: "80px", xl: "128px", "-xs": "-8px", "-sm": "-24px", "-md": "-48px", "-lg": "-80px", "-xl": "-128px" };
|
|
99
|
+
const spacingStyle = computed(() => {
|
|
100
|
+
const s = {};
|
|
101
|
+
if (props.paddingTop && SM[props.paddingTop]) s.paddingTop = SM[props.paddingTop];
|
|
102
|
+
if (props.paddingBottom && SM[props.paddingBottom]) s.paddingBottom = SM[props.paddingBottom];
|
|
103
|
+
if (props.marginTop && SM[props.marginTop]) s.marginTop = SM[props.marginTop];
|
|
104
|
+
if (props.marginBottom && SM[props.marginBottom]) s.marginBottom = SM[props.marginBottom];
|
|
105
|
+
if (props.paddingX && SM[props.paddingX]) {
|
|
106
|
+
const v = SM[props.paddingX];
|
|
107
|
+
if (v.startsWith("-")) {
|
|
108
|
+
s.marginLeft = v;
|
|
109
|
+
s.marginRight = v;
|
|
110
|
+
} else {
|
|
111
|
+
s.paddingLeft = v;
|
|
112
|
+
s.paddingRight = v;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return s;
|
|
116
|
+
});
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<style scoped>
|
|
120
|
+
.sv-out{opacity:0;transform:translateY(16px)}.sv-in{opacity:1;transform:none;transition:opacity .6s ease,transform .6s ease}
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,126 @@
|
|
|
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 v-if="title || description" class="border-b pb-6 mb-0" :style="{ borderColor: ca(textColor,'15') }">
|
|
7
|
+
<span v-if="badge" class="block text-[9px] font-black uppercase tracking-[0.5em] mb-3" :style="{ opacity: 0.35 }">{{ badge }}</span>
|
|
8
|
+
<component :is="titleTag" class="font-black uppercase leading-[0.88]" :style="{ fontSize: titleFontSize, letterSpacing: '-0.03em' }">{{ title }}</component>
|
|
9
|
+
<p v-if="description" :class="[bodyClass, 'leading-relaxed mt-3 max-w-xl']" :style="{ opacity: 0.55 }">{{ description }}</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- GRID -->
|
|
13
|
+
<div v-if="layout === 'grid'" class="divide-y" :style="{ borderColor: ca(textColor,'10') }">
|
|
14
|
+
<div v-for="(item, i) in items" :key="i" class="grid md:grid-cols-4 gap-8 py-10" :style="{ borderColor: ca(textColor,'10') }">
|
|
15
|
+
<div class="md:col-span-3">
|
|
16
|
+
<div v-if="item.rating" class="flex gap-1 mb-4">
|
|
17
|
+
<span v-for="s in 5" :key="s" class="text-xs" :style="{ opacity: s <= item.rating ? 1 : 0.12 }">★</span>
|
|
18
|
+
</div>
|
|
19
|
+
<p class="text-base md:text-lg leading-relaxed italic mb-0" :style="{ opacity: 0.75 }">"{{ item.quote }}"</p>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="flex md:flex-col items-start gap-3 md:border-l md:pl-8" :style="{ borderColor: ca(textColor,'10') }">
|
|
22
|
+
<img v-if="item.avatar" :src="item.avatar" class="w-10 h-10 object-cover shrink-0" style="border-radius:0" />
|
|
23
|
+
<div v-else class="w-10 h-10 flex items-center justify-center text-[11px] font-black shrink-0" :style="{ backgroundColor: ca(textColor,'15') }">{{ (item.name || '?')[0] }}</div>
|
|
24
|
+
<div>
|
|
25
|
+
<p class="text-[11px] font-black uppercase tracking-wide leading-none">{{ item.name }}</p>
|
|
26
|
+
<p v-if="item.role || item.company" class="text-[9px] mt-1" :style="{ opacity: 0.4 }">{{ [item.role, item.company].filter(Boolean).join(' · ') }}</p>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- FEATURED or default -->
|
|
33
|
+
<div v-else>
|
|
34
|
+
<div v-if="items[0]" class="border-b py-12" :style="{ borderColor: ca(textColor,'10') }">
|
|
35
|
+
<div class="text-[4rem] font-black leading-none mb-6" :style="{ opacity: 0.08 }">"</div>
|
|
36
|
+
<p class="text-xl md:text-3xl font-black leading-snug mb-10 max-w-4xl" :style="{ opacity: 0.82 }">{{ items[0].quote }}</p>
|
|
37
|
+
<div class="flex items-center gap-4">
|
|
38
|
+
<img v-if="items[0].avatar" :src="items[0].avatar" class="w-10 h-10 object-cover" style="border-radius:0" />
|
|
39
|
+
<div v-else class="w-10 h-10 flex items-center justify-center text-sm font-black" :style="{ backgroundColor: ca(textColor,'15') }">{{ (items[0].name || '?')[0] }}</div>
|
|
40
|
+
<div class="h-px w-8" :style="{ backgroundColor: ca(textColor,'25') }"></div>
|
|
41
|
+
<div>
|
|
42
|
+
<p class="text-sm font-black uppercase tracking-wide">{{ items[0].name }}</p>
|
|
43
|
+
<p v-if="items[0].role || items[0].company" class="text-[10px]" :style="{ opacity: 0.4 }">{{ [items[0].role, items[0].company].filter(Boolean).join(' · ') }}</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div v-if="items.length > 1" class="grid md:grid-cols-2 divide-x" :style="{ borderColor: ca(textColor,'10') }">
|
|
48
|
+
<div v-for="(item, i) in items.slice(1)" :key="i" class="py-8 first:pr-8 last:pl-8">
|
|
49
|
+
<p class="text-sm italic leading-relaxed mb-5" :style="{ opacity: 0.68 }">"{{ item.quote }}"</p>
|
|
50
|
+
<p class="text-[10px] font-black uppercase tracking-wider">{{ item.name }}</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script setup>
|
|
60
|
+
const ca = (c, h) => {
|
|
61
|
+
const m = c?.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
|
|
62
|
+
return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : (c || "") + h;
|
|
63
|
+
};
|
|
64
|
+
import { ref, computed, onMounted } from "vue";
|
|
65
|
+
const props = defineProps({
|
|
66
|
+
layout: { type: String, default: "featured" },
|
|
67
|
+
badge: { type: String, default: "" },
|
|
68
|
+
title: { type: String, default: "" },
|
|
69
|
+
titleTag: { type: String, default: "h2" },
|
|
70
|
+
description: { type: String, default: "" },
|
|
71
|
+
items: { type: Array, default: () => [] },
|
|
72
|
+
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
73
|
+
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
74
|
+
spacing: { type: String, default: "normal" },
|
|
75
|
+
radius: { type: String, default: "sharp" },
|
|
76
|
+
textDirection: { type: String, default: "ltr" },
|
|
77
|
+
contentWidth: { type: String, default: "normal" },
|
|
78
|
+
textSize: { type: String, default: "md" },
|
|
79
|
+
paddingTop: { type: String, default: "" },
|
|
80
|
+
paddingBottom: { type: String, default: "" },
|
|
81
|
+
marginTop: { type: String, default: "" },
|
|
82
|
+
marginBottom: { type: String, default: "" },
|
|
83
|
+
paddingX: { type: String, default: "" }
|
|
84
|
+
});
|
|
85
|
+
const sectionEl = ref(null);
|
|
86
|
+
const isVisible = ref(false);
|
|
87
|
+
onMounted(() => {
|
|
88
|
+
const obs = new IntersectionObserver(([e]) => {
|
|
89
|
+
if (e.isIntersecting) {
|
|
90
|
+
isVisible.value = true;
|
|
91
|
+
obs.disconnect();
|
|
92
|
+
}
|
|
93
|
+
}, { threshold: 0.06 });
|
|
94
|
+
if (sectionEl.value) obs.observe(sectionEl.value);
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
if (!isVisible.value) isVisible.value = true;
|
|
97
|
+
}, 300);
|
|
98
|
+
});
|
|
99
|
+
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");
|
|
100
|
+
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");
|
|
101
|
+
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)");
|
|
102
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
103
|
+
const SM = { none: "0", xs: "8px", sm: "24px", md: "48px", lg: "80px", xl: "128px", "-xs": "-8px", "-sm": "-24px", "-md": "-48px", "-lg": "-80px", "-xl": "-128px" };
|
|
104
|
+
const spacingStyle = computed(() => {
|
|
105
|
+
const s = {};
|
|
106
|
+
if (props.paddingTop && SM[props.paddingTop]) s.paddingTop = SM[props.paddingTop];
|
|
107
|
+
if (props.paddingBottom && SM[props.paddingBottom]) s.paddingBottom = SM[props.paddingBottom];
|
|
108
|
+
if (props.marginTop && SM[props.marginTop]) s.marginTop = SM[props.marginTop];
|
|
109
|
+
if (props.marginBottom && SM[props.marginBottom]) s.marginBottom = SM[props.marginBottom];
|
|
110
|
+
if (props.paddingX && SM[props.paddingX]) {
|
|
111
|
+
const v = SM[props.paddingX];
|
|
112
|
+
if (v.startsWith("-")) {
|
|
113
|
+
s.marginLeft = v;
|
|
114
|
+
s.marginRight = v;
|
|
115
|
+
} else {
|
|
116
|
+
s.paddingLeft = v;
|
|
117
|
+
s.paddingRight = v;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return s;
|
|
121
|
+
});
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<style scoped>
|
|
125
|
+
.sv-out{opacity:0;transform:translateY(16px)}.sv-in{opacity:1;transform:none;transition:opacity .6s ease,transform .6s ease}
|
|
126
|
+
</style>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section ref="sectionEl" :class="[sectionPad, isVisible ? 'sv-in' : 'sv-out']" :dir="textDirection" :style="[{ backgroundColor: bgColor, color: textColor }, spacingStyle]">
|
|
3
|
+
|
|
4
|
+
<!-- SPLIT -->
|
|
5
|
+
<div v-if="layout === 'split'" :class="[containerClass, 'grid grid-cols-1 md:grid-cols-2 border divide-x', reverse ? 'md:[direction:rtl]' : '']" :style="{ borderColor: ca(textColor,'15') }">
|
|
6
|
+
<div class="p-10 md:p-14 flex flex-col justify-between" :style="{ direction: 'ltr' }">
|
|
7
|
+
<div>
|
|
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 :class="[bodyClass, 'leading-relaxed max-w-md']" :style="{ opacity: 0.55 }">{{ content }}</p>
|
|
11
|
+
</div>
|
|
12
|
+
<div v-if="ctas && ctas.length" class="flex flex-wrap gap-3 mt-10 pt-8 border-t" :style="{ borderColor: ca(textColor,'12') }">
|
|
13
|
+
<NuxtLink v-for="(cta, i) in ctas" :key="i" :to="cta.link || '#'"
|
|
14
|
+
class="inline-flex items-center gap-3 px-6 py-3 text-[11px] font-black uppercase tracking-widest transition-all hover:gap-6 border"
|
|
15
|
+
:style="i === 0 ? { backgroundColor: textColor, color: bgColor, borderColor: textColor } : { borderColor: ca(textColor,'40'), color: textColor }">
|
|
16
|
+
{{ cta.text }}<span>→</span>
|
|
17
|
+
</NuxtLink>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="overflow-hidden min-h-[300px]" :style="{ direction: 'ltr' }">
|
|
21
|
+
<img v-if="image" :src="image" :alt="imageAlt || title" class="w-full h-full object-cover" />
|
|
22
|
+
<div v-else class="w-full h-full" :style="{ backgroundColor: ca(textColor,'06') }"></div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- STACKED -->
|
|
27
|
+
<div v-else-if="layout === 'stacked'" :class="[containerClass]">
|
|
28
|
+
<div class="border overflow-hidden mb-8" :style="{ borderColor: ca(textColor,'15'), height: '380px' }">
|
|
29
|
+
<img v-if="image" :src="image" class="w-full h-full object-cover" />
|
|
30
|
+
<div v-else class="w-full h-full" :style="{ backgroundColor: ca(textColor,'06') }"></div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="grid md:grid-cols-2 gap-10 border-t pt-8" :style="{ borderColor: ca(textColor,'12') }">
|
|
33
|
+
<div>
|
|
34
|
+
<span v-if="badge" class="block text-[9px] font-black uppercase tracking-[0.5em] mb-4" :style="{ opacity: 0.35 }">{{ badge }}</span>
|
|
35
|
+
<component :is="titleTag" class="font-black uppercase leading-[0.88]" :style="{ fontSize: titleFontSize, letterSpacing: '-0.03em' }">{{ title }}</component>
|
|
36
|
+
</div>
|
|
37
|
+
<div>
|
|
38
|
+
<p :class="[bodyClass, 'leading-relaxed mb-6']" :style="{ opacity: 0.55 }">{{ content }}</p>
|
|
39
|
+
<div v-if="ctas && ctas.length" class="flex flex-wrap gap-3">
|
|
40
|
+
<NuxtLink v-for="(cta, i) in ctas" :key="i" :to="cta.link || '#'"
|
|
41
|
+
class="inline-flex items-center gap-3 px-6 py-3 text-[11px] font-black uppercase tracking-widest transition-all hover:gap-6 border"
|
|
42
|
+
:style="i === 0 ? { backgroundColor: textColor, color: bgColor, borderColor: textColor } : { borderColor: ca(textColor,'40'), color: textColor }">
|
|
43
|
+
{{ cta.text }}<span>→</span>
|
|
44
|
+
</NuxtLink>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- WIDE (default) -->
|
|
51
|
+
<div v-else :class="[containerClass, 'grid grid-cols-1 md:grid-cols-2 border divide-x', reverse ? 'md:[direction:rtl]' : '']" :style="{ borderColor: ca(textColor,'15') }">
|
|
52
|
+
<div class="overflow-hidden min-h-[280px]" :style="{ direction: 'ltr' }">
|
|
53
|
+
<img v-if="image" :src="image" class="w-full h-full object-cover" />
|
|
54
|
+
<div v-else class="w-full h-full" :style="{ backgroundColor: ca(textColor,'06') }"></div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="p-10 md:p-14 flex flex-col justify-between" :style="{ direction: 'ltr' }">
|
|
57
|
+
<div>
|
|
58
|
+
<span v-if="badge" class="block text-[9px] font-black uppercase tracking-[0.5em] mb-6" :style="{ opacity: 0.35 }">{{ badge }}</span>
|
|
59
|
+
<component :is="titleTag" class="font-black uppercase leading-[0.88] mb-6" :style="{ fontSize: titleFontSize, letterSpacing: '-0.03em' }">{{ title }}</component>
|
|
60
|
+
<p :class="[bodyClass, 'leading-relaxed']" :style="{ opacity: 0.55 }">{{ content }}</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div v-if="ctas && ctas.length" class="flex flex-wrap gap-3 mt-8 pt-8 border-t" :style="{ borderColor: ca(textColor,'12') }">
|
|
63
|
+
<NuxtLink v-for="(cta, i) in ctas" :key="i" :to="cta.link || '#'"
|
|
64
|
+
class="inline-flex items-center gap-3 px-6 py-3 text-[11px] font-black uppercase tracking-widest transition-all hover:gap-6 border"
|
|
65
|
+
:style="i === 0 ? { backgroundColor: textColor, color: bgColor, borderColor: textColor } : { borderColor: ca(textColor,'40'), color: textColor }">
|
|
66
|
+
{{ cta.text }}<span>→</span>
|
|
67
|
+
</NuxtLink>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
</section>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<script setup>
|
|
76
|
+
const ca = (c, h) => {
|
|
77
|
+
const m = c?.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
|
|
78
|
+
return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : (c || "") + h;
|
|
79
|
+
};
|
|
80
|
+
import { ref, computed, onMounted } from "vue";
|
|
81
|
+
const props = defineProps({
|
|
82
|
+
layout: { type: String, default: "split" },
|
|
83
|
+
badge: { type: String, default: "" },
|
|
84
|
+
title: { type: String, default: "" },
|
|
85
|
+
titleTag: { type: String, default: "h2" },
|
|
86
|
+
content: { type: String, default: "" },
|
|
87
|
+
image: { type: String, default: "" },
|
|
88
|
+
imageAlt: { type: String, default: "" },
|
|
89
|
+
reverse: { type: Boolean, default: false },
|
|
90
|
+
ctas: { type: Array, default: () => [] },
|
|
91
|
+
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
92
|
+
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
93
|
+
spacing: { type: String, default: "normal" },
|
|
94
|
+
radius: { type: String, default: "sharp" },
|
|
95
|
+
textDirection: { type: String, default: "ltr" },
|
|
96
|
+
contentWidth: { type: String, default: "normal" },
|
|
97
|
+
textSize: { type: String, default: "md" },
|
|
98
|
+
paddingTop: { type: String, default: "" },
|
|
99
|
+
paddingBottom: { type: String, default: "" },
|
|
100
|
+
marginTop: { type: String, default: "" },
|
|
101
|
+
marginBottom: { type: String, default: "" },
|
|
102
|
+
paddingX: { type: String, default: "" }
|
|
103
|
+
});
|
|
104
|
+
const sectionEl = ref(null);
|
|
105
|
+
const isVisible = ref(false);
|
|
106
|
+
onMounted(() => {
|
|
107
|
+
const obs = new IntersectionObserver(([e]) => {
|
|
108
|
+
if (e.isIntersecting) {
|
|
109
|
+
isVisible.value = true;
|
|
110
|
+
obs.disconnect();
|
|
111
|
+
}
|
|
112
|
+
}, { threshold: 0.06 });
|
|
113
|
+
if (sectionEl.value) obs.observe(sectionEl.value);
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
if (!isVisible.value) isVisible.value = true;
|
|
116
|
+
}, 300);
|
|
117
|
+
});
|
|
118
|
+
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");
|
|
119
|
+
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");
|
|
120
|
+
const titleFontSize = computed(() => ({ sm: "clamp(1rem,1.5vw,1.3rem)", md: "clamp(1.5rem,2.5vw,2rem)", lg: "clamp(2rem,4vw,3.5rem)" })[props.textSize] || "clamp(1.5rem,2.5vw,2rem)");
|
|
121
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
122
|
+
const imageContainerClass = computed(() => ({ sm: "aspect-video", md: "aspect-[4/3]", lg: "aspect-square" })["md"] || "aspect-[4/3]");
|
|
123
|
+
const SM = { none: "0", xs: "8px", sm: "24px", md: "48px", lg: "80px", xl: "128px", "-xs": "-8px", "-sm": "-24px", "-md": "-48px", "-lg": "-80px", "-xl": "-128px" };
|
|
124
|
+
const spacingStyle = computed(() => {
|
|
125
|
+
const s = {};
|
|
126
|
+
if (props.paddingTop && SM[props.paddingTop]) s.paddingTop = SM[props.paddingTop];
|
|
127
|
+
if (props.paddingBottom && SM[props.paddingBottom]) s.paddingBottom = SM[props.paddingBottom];
|
|
128
|
+
if (props.marginTop && SM[props.marginTop]) s.marginTop = SM[props.marginTop];
|
|
129
|
+
if (props.marginBottom && SM[props.marginBottom]) s.marginBottom = SM[props.marginBottom];
|
|
130
|
+
if (props.paddingX && SM[props.paddingX]) {
|
|
131
|
+
const v = SM[props.paddingX];
|
|
132
|
+
if (v.startsWith("-")) {
|
|
133
|
+
s.marginLeft = v;
|
|
134
|
+
s.marginRight = v;
|
|
135
|
+
} else {
|
|
136
|
+
s.paddingLeft = v;
|
|
137
|
+
s.paddingRight = v;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return s;
|
|
141
|
+
});
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<style scoped>
|
|
145
|
+
.sv-out{opacity:0;transform:translateY(16px)}.sv-in{opacity:1;transform:none;transition:opacity .6s ease,transform .6s ease}
|
|
146
|
+
</style>
|
|
@@ -62,7 +62,21 @@ const selectPage = (item, key, page2) => {
|
|
|
62
62
|
pagePickerOpen.value[key] = false;
|
|
63
63
|
trigger();
|
|
64
64
|
};
|
|
65
|
-
const
|
|
65
|
+
const themePrefix = computed(() => {
|
|
66
|
+
const t = adminSettings.settings.value?.theme || "";
|
|
67
|
+
if (!t) return "";
|
|
68
|
+
return t.split("-").map((s) => s[0].toUpperCase() + s.slice(1)).join("");
|
|
69
|
+
});
|
|
70
|
+
const resolveSection = (type) => {
|
|
71
|
+
const flat = vueApp.component(`Sections${type}`);
|
|
72
|
+
if (flat) return flat;
|
|
73
|
+
const prefix = themePrefix.value;
|
|
74
|
+
if (prefix) {
|
|
75
|
+
const themed = vueApp.component(`Sections${prefix}${type}`);
|
|
76
|
+
if (themed) return themed;
|
|
77
|
+
}
|
|
78
|
+
return vueApp.component(type) || null;
|
|
79
|
+
};
|
|
66
80
|
const { data: _rawPage } = await useAsyncData(`admin-edit-${pageId}`, async () => {
|
|
67
81
|
const { data } = await supabase.from(table).select("*").eq("id", pageId).maybeSingle();
|
|
68
82
|
return data;
|
|
@@ -7,11 +7,13 @@ const { settings, loading, saving, load, save } = useAdminSettings();
|
|
|
7
7
|
const sections = useAdminSections();
|
|
8
8
|
const aiContext = ref("");
|
|
9
9
|
const presets = ref({});
|
|
10
|
+
const selectedTheme = ref("");
|
|
10
11
|
const saved = ref(false);
|
|
11
12
|
onMounted(async () => {
|
|
12
13
|
await load();
|
|
13
14
|
aiContext.value = settings.value?.ai_context || "";
|
|
14
15
|
presets.value = JSON.parse(JSON.stringify(settings.value?.section_presets || {}));
|
|
16
|
+
selectedTheme.value = settings.value?.theme || "";
|
|
15
17
|
});
|
|
16
18
|
const setPreset = (sectionType, field, value) => {
|
|
17
19
|
if (!presets.value[sectionType]) presets.value[sectionType] = {};
|
|
@@ -20,8 +22,10 @@ const setPreset = (sectionType, field, value) => {
|
|
|
20
22
|
const getPreset = (sectionType, field, fallback) => {
|
|
21
23
|
return presets.value[sectionType]?.[field] ?? fallback ?? "";
|
|
22
24
|
};
|
|
25
|
+
const canSave = computed(() => !!selectedTheme.value);
|
|
23
26
|
const saveAll = async () => {
|
|
24
|
-
|
|
27
|
+
if (!canSave.value) return;
|
|
28
|
+
await save({ ai_context: aiContext.value, section_presets: presets.value, theme: selectedTheme.value });
|
|
25
29
|
saved.value = true;
|
|
26
30
|
setTimeout(() => {
|
|
27
31
|
saved.value = false;
|
|
@@ -43,7 +47,7 @@ const hasStyleOptions = (conf) => styleFields.some((f) => conf.fields?.[f]?.opti
|
|
|
43
47
|
</div>
|
|
44
48
|
<button
|
|
45
49
|
@click="saveAll"
|
|
46
|
-
:disabled="saving"
|
|
50
|
+
:disabled="saving || !canSave"
|
|
47
51
|
class="px-5 py-2.5 bg-slate-900 text-white text-sm font-semibold rounded-xl hover:bg-slate-700 disabled:opacity-50 transition-all flex items-center gap-2"
|
|
48
52
|
>
|
|
49
53
|
<svg v-if="!saving && !saved" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -61,6 +65,77 @@ const hasStyleOptions = (conf) => styleFields.some((f) => conf.fields?.[f]?.opti
|
|
|
61
65
|
|
|
62
66
|
<div v-else class="space-y-6">
|
|
63
67
|
|
|
68
|
+
<!-- ── Theme Selection (required) ─────────────────────────────── -->
|
|
69
|
+
<div class="bg-white border rounded-2xl p-8" :class="!selectedTheme ? 'border-amber-300 ring-1 ring-amber-200' : 'border-slate-200'">
|
|
70
|
+
<div class="flex items-center gap-3 mb-2">
|
|
71
|
+
<span class="text-xl">🎨</span>
|
|
72
|
+
<h2 class="text-base font-bold text-slate-900">Thème de design <span class="text-amber-500 text-sm font-normal ml-1">(requis)</span></h2>
|
|
73
|
+
</div>
|
|
74
|
+
<p class="text-xs text-slate-500 mb-6 leading-relaxed">
|
|
75
|
+
Choisissez le thème visuel qui s'appliquera à toutes les sections de votre site. Cette sélection est obligatoire.
|
|
76
|
+
</p>
|
|
77
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
78
|
+
<!-- CinqCinq theme -->
|
|
79
|
+
<button
|
|
80
|
+
@click="selectedTheme = 'cinqcinq'"
|
|
81
|
+
:class="[
|
|
82
|
+
'text-left p-5 border-2 rounded-xl transition-all',
|
|
83
|
+
selectedTheme === 'cinqcinq' ? 'border-slate-900 bg-slate-50' : 'border-slate-200 hover:border-slate-400'
|
|
84
|
+
]"
|
|
85
|
+
>
|
|
86
|
+
<div class="flex items-center gap-3 mb-3">
|
|
87
|
+
<div class="w-8 h-8 rounded-lg bg-slate-900 flex items-center justify-center">
|
|
88
|
+
<span class="text-white text-xs font-black">55</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<div class="font-bold text-slate-900 text-sm">CinqCinq</div>
|
|
92
|
+
<div class="text-[10px] text-slate-400 uppercase tracking-wider">Original</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div v-if="selectedTheme === 'cinqcinq'" class="ml-auto w-5 h-5 rounded-full bg-slate-900 flex items-center justify-center">
|
|
95
|
+
<svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<p class="text-xs text-slate-500 leading-relaxed">Design contemporain avec coins arrondis, cartes avec ombres et animations fluides. Moderne et accessible.</p>
|
|
99
|
+
<!-- Mini preview -->
|
|
100
|
+
<div class="mt-4 flex gap-1.5">
|
|
101
|
+
<div class="h-6 flex-1 rounded-lg bg-slate-100"></div>
|
|
102
|
+
<div class="h-6 flex-1 rounded-lg bg-slate-200"></div>
|
|
103
|
+
<div class="h-6 w-12 rounded-lg bg-slate-900"></div>
|
|
104
|
+
</div>
|
|
105
|
+
</button>
|
|
106
|
+
|
|
107
|
+
<!-- Swiss Style theme -->
|
|
108
|
+
<button
|
|
109
|
+
@click="selectedTheme = 'swiss-style'"
|
|
110
|
+
:class="[
|
|
111
|
+
'text-left p-5 border-2 rounded-xl transition-all',
|
|
112
|
+
selectedTheme === 'swiss-style' ? 'border-slate-900 bg-slate-50' : 'border-slate-200 hover:border-slate-400'
|
|
113
|
+
]"
|
|
114
|
+
>
|
|
115
|
+
<div class="flex items-center gap-3 mb-3">
|
|
116
|
+
<div class="w-8 h-8 bg-black flex items-center justify-center">
|
|
117
|
+
<span class="text-white text-xs font-black tracking-tighter">SW</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div>
|
|
120
|
+
<div class="font-bold text-slate-900 text-sm">Swiss Style</div>
|
|
121
|
+
<div class="text-[10px] text-slate-400 uppercase tracking-wider">Typographic</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div v-if="selectedTheme === 'swiss-style'" class="ml-auto w-5 h-5 rounded-full bg-slate-900 flex items-center justify-center">
|
|
124
|
+
<svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<p class="text-xs text-slate-500 leading-relaxed">Style typographique suisse. Grilles strictes, bordures 1px, angles droits et hiérarchie typographique forte.</p>
|
|
128
|
+
<!-- Mini preview -->
|
|
129
|
+
<div class="mt-4 flex gap-1.5">
|
|
130
|
+
<div class="h-6 flex-1 border border-slate-300"></div>
|
|
131
|
+
<div class="h-6 flex-1 border border-slate-300 bg-slate-50"></div>
|
|
132
|
+
<div class="h-6 w-12 bg-black"></div>
|
|
133
|
+
</div>
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
<p v-if="!selectedTheme" class="text-xs text-amber-600 mt-3">Sélectionnez un thème pour pouvoir enregistrer.</p>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
64
139
|
<!-- ── AI Context ───────────────────────────────────────────────── -->
|
|
65
140
|
<div class="bg-white border border-slate-200 rounded-2xl p-8">
|
|
66
141
|
<div class="flex items-center gap-3 mb-2">
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/dist/runtime/components/sections/{CallToAction.d.vue.ts → cinqcinq/CallToAction.d.vue.ts}
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/runtime/components/sections/{CallToAction.vue.d.ts → cinqcinq/CallToAction.vue.d.ts}
RENAMED
|
File without changes
|
/package/dist/runtime/components/sections/{Comparison.d.vue.ts → cinqcinq/Comparison.d.vue.ts}
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/runtime/components/sections/{Comparison.vue.d.ts → cinqcinq/Comparison.vue.d.ts}
RENAMED
|
File without changes
|
/package/dist/runtime/components/sections/{ContactForm.d.vue.ts → cinqcinq/ContactForm.d.vue.ts}
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/runtime/components/sections/{ContactForm.vue.d.ts → cinqcinq/ContactForm.vue.d.ts}
RENAMED
|
File without changes
|
/package/dist/runtime/components/sections/{DataTable.d.vue.ts → cinqcinq/DataTable.d.vue.ts}
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/runtime/components/sections/{DataTable.vue.d.ts → cinqcinq/DataTable.vue.d.ts}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|