cinqcinqdev-seo 0.1.72 → 1.0.0
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 +94 -11
- 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,124 @@
|
|
|
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
|
+
<!-- STEPS (vertical) -->
|
|
13
|
+
<div v-if="layout === 'steps'" class="divide-y" :style="{ borderColor: ca(textColor,'10') }">
|
|
14
|
+
<div v-for="(step, i) in items" :key="i" class="grid grid-cols-[3rem_1fr] gap-8 py-8 items-start" :style="{ borderColor: ca(textColor,'10') }">
|
|
15
|
+
<div class="font-black tabular-nums leading-none pt-1" :style="{ fontSize: '2.5rem', opacity: 0.1, letterSpacing: '-0.05em' }">{{ String(i+1).padStart(2,'0') }}</div>
|
|
16
|
+
<div>
|
|
17
|
+
<component :is="itemTag" class="font-black uppercase tracking-tight mb-2 leading-tight">{{ step.title }}</component>
|
|
18
|
+
<p class="text-sm leading-relaxed" :style="{ opacity: 0.55 }">{{ step.description }}</p>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- HORIZONTAL -->
|
|
24
|
+
<div v-else-if="layout === 'horizontal'" class="grid grid-cols-1 md:grid-cols-4 divide-y md:divide-y-0 md:divide-x border-t border-b" :style="{ borderColor: ca(textColor,'12') }">
|
|
25
|
+
<div v-for="(step, i) in items" :key="i" class="p-8" :style="{ borderColor: ca(textColor,'12') }">
|
|
26
|
+
<div class="font-black tabular-nums mb-6" :style="{ fontSize: '2.5rem', opacity: 0.1, letterSpacing: '-0.05em' }">{{ String(i+1).padStart(2,'0') }}</div>
|
|
27
|
+
<component :is="itemTag" class="text-sm font-black uppercase tracking-tight mb-2 leading-tight">{{ step.title }}</component>
|
|
28
|
+
<p class="text-xs leading-relaxed" :style="{ opacity: 0.5 }">{{ step.description }}</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- CARDS -->
|
|
33
|
+
<div v-else-if="layout === 'cards'" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 border divide-x divide-y" :style="{ borderColor: ca(textColor,'12') }">
|
|
34
|
+
<div v-for="(step, i) in items" :key="i" class="p-8" :style="{ borderColor: ca(textColor,'12') }">
|
|
35
|
+
<div class="font-black tabular-nums mb-6" :style="{ fontSize: '2rem', opacity: 0.1, letterSpacing: '-0.05em' }">{{ String(i+1).padStart(2,'0') }}</div>
|
|
36
|
+
<component :is="itemTag" class="font-black uppercase tracking-tight mb-3 leading-tight">{{ step.title }}</component>
|
|
37
|
+
<p class="text-sm leading-relaxed" :style="{ opacity: 0.55 }">{{ step.description }}</p>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- TIMELINE (default) -->
|
|
42
|
+
<div v-else class="divide-y" :style="{ borderColor: ca(textColor,'10') }">
|
|
43
|
+
<div v-for="(step, i) in items" :key="i" class="grid md:grid-cols-[8rem_1fr] gap-8 py-8 items-start" :style="{ borderColor: ca(textColor,'10') }">
|
|
44
|
+
<div class="font-black tabular-nums text-right hidden md:block" :style="{ fontSize: '3rem', opacity: 0.08, letterSpacing: '-0.05em' }">{{ String(i+1).padStart(2,'0') }}</div>
|
|
45
|
+
<div class="border-l pl-8" :style="{ borderColor: ca(textColor,'15') }">
|
|
46
|
+
<component :is="itemTag" class="font-black uppercase tracking-tight mb-2 leading-tight">{{ step.title }}</component>
|
|
47
|
+
<p class="text-sm leading-relaxed" :style="{ opacity: 0.55 }">{{ step.description }}</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script setup>
|
|
57
|
+
const ca = (c, h) => {
|
|
58
|
+
const m = c?.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
|
|
59
|
+
return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : (c || "") + h;
|
|
60
|
+
};
|
|
61
|
+
import { ref, computed, onMounted } from "vue";
|
|
62
|
+
const props = defineProps({
|
|
63
|
+
layout: { type: String, default: "steps" },
|
|
64
|
+
badge: { type: String, default: "" },
|
|
65
|
+
title: { type: String, default: "" },
|
|
66
|
+
titleTag: { type: String, default: "h2" },
|
|
67
|
+
itemTag: { type: String, default: "h3" },
|
|
68
|
+
description: { type: String, default: "" },
|
|
69
|
+
items: { type: Array, default: () => [] },
|
|
70
|
+
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
71
|
+
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
72
|
+
spacing: { type: String, default: "normal" },
|
|
73
|
+
radius: { type: String, default: "sharp" },
|
|
74
|
+
textDirection: { type: String, default: "ltr" },
|
|
75
|
+
contentWidth: { type: String, default: "normal" },
|
|
76
|
+
textSize: { type: String, default: "md" },
|
|
77
|
+
paddingTop: { type: String, default: "" },
|
|
78
|
+
paddingBottom: { type: String, default: "" },
|
|
79
|
+
marginTop: { type: String, default: "" },
|
|
80
|
+
marginBottom: { type: String, default: "" },
|
|
81
|
+
paddingX: { type: String, default: "" }
|
|
82
|
+
});
|
|
83
|
+
const sectionEl = ref(null);
|
|
84
|
+
const isVisible = ref(false);
|
|
85
|
+
onMounted(() => {
|
|
86
|
+
const obs = new IntersectionObserver(([e]) => {
|
|
87
|
+
if (e.isIntersecting) {
|
|
88
|
+
isVisible.value = true;
|
|
89
|
+
obs.disconnect();
|
|
90
|
+
}
|
|
91
|
+
}, { threshold: 0.06 });
|
|
92
|
+
if (sectionEl.value) obs.observe(sectionEl.value);
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
if (!isVisible.value) isVisible.value = true;
|
|
95
|
+
}, 300);
|
|
96
|
+
});
|
|
97
|
+
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");
|
|
98
|
+
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");
|
|
99
|
+
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)");
|
|
100
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
101
|
+
const SM = { none: "0", xs: "8px", sm: "24px", md: "48px", lg: "80px", xl: "128px", "-xs": "-8px", "-sm": "-24px", "-md": "-48px", "-lg": "-80px", "-xl": "-128px" };
|
|
102
|
+
const spacingStyle = computed(() => {
|
|
103
|
+
const s = {};
|
|
104
|
+
if (props.paddingTop && SM[props.paddingTop]) s.paddingTop = SM[props.paddingTop];
|
|
105
|
+
if (props.paddingBottom && SM[props.paddingBottom]) s.paddingBottom = SM[props.paddingBottom];
|
|
106
|
+
if (props.marginTop && SM[props.marginTop]) s.marginTop = SM[props.marginTop];
|
|
107
|
+
if (props.marginBottom && SM[props.marginBottom]) s.marginBottom = SM[props.marginBottom];
|
|
108
|
+
if (props.paddingX && SM[props.paddingX]) {
|
|
109
|
+
const v = SM[props.paddingX];
|
|
110
|
+
if (v.startsWith("-")) {
|
|
111
|
+
s.marginLeft = v;
|
|
112
|
+
s.marginRight = v;
|
|
113
|
+
} else {
|
|
114
|
+
s.paddingLeft = v;
|
|
115
|
+
s.paddingRight = v;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return s;
|
|
119
|
+
});
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<style scoped>
|
|
123
|
+
.sv-out{opacity:0;transform:translateY(16px)}.sv-in{opacity:1;transform:none;transition:opacity .6s ease,transform .6s ease}
|
|
124
|
+
</style>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section ref="sectionEl" :class="[sectionPad, isVisible ? 'sv-in' : 'sv-out']" :dir="textDirection" :style="[{ backgroundColor: bgColor, color: textColor }, spacingStyle]">
|
|
3
|
+
|
|
4
|
+
<!-- CENTERED -->
|
|
5
|
+
<div v-if="layout === 'centered'" :class="[containerClass]">
|
|
6
|
+
<div class="border-t border-b py-14" :style="{ borderColor: ca(textColor,'20') }">
|
|
7
|
+
<div class="grid md:grid-cols-[2rem_1fr] gap-8 items-start">
|
|
8
|
+
<div class="font-black leading-none hidden md:block" style="font-size:4rem; opacity:0.12">"</div>
|
|
9
|
+
<div>
|
|
10
|
+
<blockquote class="font-black uppercase leading-[0.88] mb-8" :style="{ fontSize: quoteFontSize, letterSpacing: '-0.03em' }">{{ quote }}</blockquote>
|
|
11
|
+
<div class="flex items-center gap-4">
|
|
12
|
+
<div class="w-8 h-px" :style="{ backgroundColor: textColor }"></div>
|
|
13
|
+
<img v-if="avatar" :src="avatar" class="w-8 h-8 object-cover" style="border-radius:0" />
|
|
14
|
+
<div>
|
|
15
|
+
<div v-if="author" class="text-xs font-black uppercase tracking-wider">{{ author }}</div>
|
|
16
|
+
<div v-if="role" class="text-[11px]" :style="{ opacity: 0.45 }">{{ role }}</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<!-- LEFT -->
|
|
25
|
+
<div v-else-if="layout === 'left'" :class="[containerClass, 'grid md:grid-cols-[1px_1fr] gap-8']">
|
|
26
|
+
<div class="hidden md:block" :style="{ backgroundColor: textColor, opacity: 0.15 }"></div>
|
|
27
|
+
<div class="pl-0 md:pl-8">
|
|
28
|
+
<blockquote class="font-black uppercase leading-[0.88] mb-8" :style="{ fontSize: quoteFontSize, letterSpacing: '-0.03em' }">{{ quote }}</blockquote>
|
|
29
|
+
<div class="flex items-center gap-4">
|
|
30
|
+
<img v-if="avatar" :src="avatar" class="w-8 h-8 object-cover" style="border-radius:0" />
|
|
31
|
+
<div v-else-if="author" class="w-8 h-8 flex items-center justify-center text-xs font-black" :style="{ backgroundColor: ca(textColor,'15') }">{{ author[0] }}</div>
|
|
32
|
+
<div>
|
|
33
|
+
<div v-if="author" class="text-xs font-black uppercase tracking-wider">{{ author }}</div>
|
|
34
|
+
<div v-if="role" class="text-[11px]" :style="{ opacity: 0.45 }">{{ role }}</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- LARGE (default) -->
|
|
41
|
+
<div v-else :class="[containerClass]">
|
|
42
|
+
<div class="border-t border-b py-12 md:py-16" :style="{ borderColor: ca(textColor,'20') }">
|
|
43
|
+
<blockquote class="font-black uppercase leading-[0.88] mb-10" :style="{ fontSize: quoteFontSize, letterSpacing: '-0.03em' }">{{ quote }}</blockquote>
|
|
44
|
+
<div class="flex items-center gap-4">
|
|
45
|
+
<div class="w-10 h-px" :style="{ backgroundColor: textColor }"></div>
|
|
46
|
+
<img v-if="avatar" :src="avatar" class="w-8 h-8 object-cover" style="border-radius:0" />
|
|
47
|
+
<div>
|
|
48
|
+
<span v-if="author" class="text-xs font-black uppercase tracking-wider">{{ author }}</span>
|
|
49
|
+
<span v-if="role" class="text-[11px] ml-3" :style="{ opacity: 0.45 }">{{ role }}</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- Placeholder -->
|
|
56
|
+
<div v-if="!quote" :class="[containerClass, 'border-t border-b py-10']" :style="{ borderColor: ca(textColor,'12') }">
|
|
57
|
+
<div class="h-6 mb-4" :style="{ backgroundColor: ca(textColor,'08'), width: '80%' }"></div>
|
|
58
|
+
<div class="h-6 mb-8" :style="{ backgroundColor: ca(textColor,'08'), width: '60%' }"></div>
|
|
59
|
+
<div class="h-3" :style="{ backgroundColor: ca(textColor,'12'), width: '20%' }"></div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
</section>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<script setup>
|
|
66
|
+
const ca = (c, h) => {
|
|
67
|
+
const m = c?.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
|
|
68
|
+
return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : (c || "") + h;
|
|
69
|
+
};
|
|
70
|
+
import { ref, computed, onMounted } from "vue";
|
|
71
|
+
const props = defineProps({
|
|
72
|
+
layout: { type: String, default: "centered" },
|
|
73
|
+
quote: { type: String, default: "" },
|
|
74
|
+
author: { type: String, default: "" },
|
|
75
|
+
role: { type: String, default: "" },
|
|
76
|
+
avatar: { type: String, default: "" },
|
|
77
|
+
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
78
|
+
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
79
|
+
spacing: { type: String, default: "normal" },
|
|
80
|
+
radius: { type: String, default: "sharp" },
|
|
81
|
+
textDirection: { type: String, default: "ltr" },
|
|
82
|
+
contentWidth: { type: String, default: "narrow" },
|
|
83
|
+
textSize: { type: String, default: "md" },
|
|
84
|
+
paddingTop: { type: String, default: "" },
|
|
85
|
+
paddingBottom: { type: String, default: "" },
|
|
86
|
+
marginTop: { type: String, default: "" },
|
|
87
|
+
marginBottom: { type: String, default: "" },
|
|
88
|
+
paddingX: { type: String, default: "" }
|
|
89
|
+
});
|
|
90
|
+
const sectionEl = ref(null);
|
|
91
|
+
const isVisible = ref(false);
|
|
92
|
+
onMounted(() => {
|
|
93
|
+
const obs = new IntersectionObserver(([e]) => {
|
|
94
|
+
if (e.isIntersecting) {
|
|
95
|
+
isVisible.value = true;
|
|
96
|
+
obs.disconnect();
|
|
97
|
+
}
|
|
98
|
+
}, { threshold: 0.06 });
|
|
99
|
+
if (sectionEl.value) obs.observe(sectionEl.value);
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
if (!isVisible.value) isVisible.value = true;
|
|
102
|
+
}, 300);
|
|
103
|
+
});
|
|
104
|
+
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");
|
|
105
|
+
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");
|
|
106
|
+
const quoteFontSize = computed(() => ({ sm: "clamp(1.2rem,2vw,1.8rem)", md: "clamp(1.5rem,3vw,2.5rem)", lg: "clamp(2rem,5vw,4rem)" })[props.textSize] || "clamp(1.5rem,3vw,2.5rem)");
|
|
107
|
+
const SM = { none: "0", xs: "8px", sm: "24px", md: "48px", lg: "80px", xl: "128px", "-xs": "-8px", "-sm": "-24px", "-md": "-48px", "-lg": "-80px", "-xl": "-128px" };
|
|
108
|
+
const spacingStyle = computed(() => {
|
|
109
|
+
const s = {};
|
|
110
|
+
if (props.paddingTop && SM[props.paddingTop]) s.paddingTop = SM[props.paddingTop];
|
|
111
|
+
if (props.paddingBottom && SM[props.paddingBottom]) s.paddingBottom = SM[props.paddingBottom];
|
|
112
|
+
if (props.marginTop && SM[props.marginTop]) s.marginTop = SM[props.marginTop];
|
|
113
|
+
if (props.marginBottom && SM[props.marginBottom]) s.marginBottom = SM[props.marginBottom];
|
|
114
|
+
if (props.paddingX && SM[props.paddingX]) {
|
|
115
|
+
const v = SM[props.paddingX];
|
|
116
|
+
if (v.startsWith("-")) {
|
|
117
|
+
s.marginLeft = v;
|
|
118
|
+
s.marginRight = v;
|
|
119
|
+
} else {
|
|
120
|
+
s.paddingLeft = v;
|
|
121
|
+
s.paddingRight = v;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return s;
|
|
125
|
+
});
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<style scoped>
|
|
129
|
+
.sv-out{opacity:0;transform:translateY(16px)}.sv-in{opacity:1;transform:none;transition:opacity .6s ease,transform .6s ease}
|
|
130
|
+
</style>
|
|
@@ -0,0 +1,212 @@
|
|
|
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="badge || title" class="flex items-end justify-between gap-6 flex-wrap mb-10 pb-6 border-b" :style="{ borderColor: ca(textColor,'12') }">
|
|
7
|
+
<div>
|
|
8
|
+
<span v-if="badge" class="block text-[9px] font-black uppercase tracking-[0.5em] mb-2" :style="{ opacity: 0.35 }">{{ badge }}</span>
|
|
9
|
+
<component :is="titleTag" class="font-black uppercase leading-[0.88]" :style="{ fontSize: titleFontSize, letterSpacing: '-0.03em' }">{{ title }}</component>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- CARDS layout -->
|
|
14
|
+
<div v-if="layout === 'cards'" :class="['grid border divide-x', gridCols]" :style="{ borderColor: ca(textColor,'12') }">
|
|
15
|
+
<NuxtLink
|
|
16
|
+
v-for="(item, i) in items" :key="i"
|
|
17
|
+
:to="item.slug ? '/' + item.slug : '#'"
|
|
18
|
+
class="group flex flex-col overflow-hidden transition-all hover:bg-opacity-5"
|
|
19
|
+
:style="{ backgroundColor: 'transparent' }"
|
|
20
|
+
>
|
|
21
|
+
<!-- Image -->
|
|
22
|
+
<div v-if="item.image" class="overflow-hidden aspect-video shrink-0 border-b" :style="{ borderColor: ca(textColor,'08') }">
|
|
23
|
+
<img :src="item.image" :alt="item.title" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
|
|
24
|
+
</div>
|
|
25
|
+
<!-- No-image placeholder -->
|
|
26
|
+
<div v-else class="aspect-video shrink-0 flex items-center justify-center border-b" :style="{ backgroundColor: ca(textColor,'04'), borderColor: ca(textColor,'08') }">
|
|
27
|
+
<span class="text-5xl font-black" :style="{ opacity: 0.06 }">{{ (item.title || '?')[0] }}</span>
|
|
28
|
+
</div>
|
|
29
|
+
<!-- Content -->
|
|
30
|
+
<div class="flex flex-col flex-1 p-6">
|
|
31
|
+
<span v-if="item.category" class="text-[9px] font-black uppercase tracking-[0.4em] mb-3 border-l-2 pl-2" :style="{ borderColor: textColor, opacity: 0.4 }">{{ item.category }}</span>
|
|
32
|
+
<div class="font-black leading-snug mb-2 flex-1 uppercase" :class="bodyClass">{{ item.title }}</div>
|
|
33
|
+
<p v-if="item.description" :class="[bodyClass, 'leading-relaxed mt-1']" :style="{ opacity: 0.5, fontSize: '0.85em' }">{{ item.description }}</p>
|
|
34
|
+
<div class="flex items-center gap-1.5 mt-5 text-[11px] font-black uppercase tracking-wider transition-all group-hover:gap-3" :style="{ opacity: 0.4 }">
|
|
35
|
+
Lire <span>→</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</NuxtLink>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- LIST layout -->
|
|
42
|
+
<div v-else-if="layout === 'list'" class="border divide-y" :style="{ borderColor: ca(textColor,'12') }">
|
|
43
|
+
<NuxtLink
|
|
44
|
+
v-for="(item, i) in items" :key="i"
|
|
45
|
+
:to="item.slug ? '/' + item.slug : '#'"
|
|
46
|
+
class="group flex items-center gap-5 py-4 px-4 transition-all hover:pl-6"
|
|
47
|
+
>
|
|
48
|
+
<!-- Thumbnail -->
|
|
49
|
+
<div v-if="item.image" class="shrink-0 overflow-hidden border" style="width: 72px; height: 54px;" :style="{ borderColor: ca(textColor,'08') }">
|
|
50
|
+
<img :src="item.image" :alt="item.title" class="w-full h-full object-cover" />
|
|
51
|
+
</div>
|
|
52
|
+
<div v-else class="shrink-0 flex items-center justify-center border" style="width: 72px; height: 54px;" :style="{ backgroundColor: ca(textColor,'05'), borderColor: ca(textColor,'08') }">
|
|
53
|
+
<span class="text-lg font-black" :style="{ opacity: 0.15 }">{{ (item.title || '?')[0] }}</span>
|
|
54
|
+
</div>
|
|
55
|
+
<!-- Text -->
|
|
56
|
+
<div class="flex-1 min-w-0">
|
|
57
|
+
<span v-if="item.category" class="text-[9px] font-black uppercase tracking-[0.4em] block mb-0.5" :style="{ opacity: 0.35 }">{{ item.category }}</span>
|
|
58
|
+
<div class="font-black leading-snug truncate uppercase" :class="[bodyClass]">{{ item.title }}</div>
|
|
59
|
+
<p v-if="item.description" class="text-[11px] leading-relaxed mt-0.5 truncate" :style="{ opacity: 0.45 }">{{ item.description }}</p>
|
|
60
|
+
</div>
|
|
61
|
+
<!-- Arrow -->
|
|
62
|
+
<span class="shrink-0 text-sm transition-all group-hover:translate-x-1" :style="{ opacity: 0.3 }">→</span>
|
|
63
|
+
</NuxtLink>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- MINIMAL layout -->
|
|
67
|
+
<div v-else-if="layout === 'minimal'" class="border divide-y" :style="{ borderColor: ca(textColor,'10') }">
|
|
68
|
+
<NuxtLink
|
|
69
|
+
v-for="(item, i) in items" :key="i"
|
|
70
|
+
:to="item.slug ? '/' + item.slug : '#'"
|
|
71
|
+
class="group flex items-center justify-between gap-4 py-4 px-4 transition-all hover:pl-6"
|
|
72
|
+
>
|
|
73
|
+
<div class="flex items-center gap-4 min-w-0">
|
|
74
|
+
<span class="text-[10px] font-black tabular-nums" :style="{ opacity: 0.2 }">{{ String(i + 1).padStart(2, '0') }}</span>
|
|
75
|
+
<span v-if="item.category" class="shrink-0 text-[9px] font-black uppercase tracking-[0.35em] px-2 py-0.5 border" :style="{ borderColor: ca(textColor,'15') }">{{ item.category }}</span>
|
|
76
|
+
<span class="font-black truncate uppercase" :class="[bodyClass]">{{ item.title }}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<span class="shrink-0 text-sm transition-all group-hover:translate-x-1 group-hover:opacity-100" :style="{ opacity: 0.3 }">→</span>
|
|
79
|
+
</NuxtLink>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- EDITORIAL layout (default) -->
|
|
83
|
+
<div v-else class="grid grid-cols-1 md:grid-cols-3 border divide-x" :style="{ borderColor: ca(textColor,'12') }">
|
|
84
|
+
<!-- Featured (first item) -->
|
|
85
|
+
<NuxtLink
|
|
86
|
+
v-if="items[0]"
|
|
87
|
+
:to="items[0].slug ? '/' + items[0].slug : '#'"
|
|
88
|
+
class="group col-span-1 md:col-span-2 flex flex-col overflow-hidden transition-all"
|
|
89
|
+
>
|
|
90
|
+
<div class="overflow-hidden border-b" style="aspect-ratio: 16/9;" :style="{ borderColor: ca(textColor,'08') }">
|
|
91
|
+
<img v-if="items[0].image" :src="items[0].image" :alt="items[0].title" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
|
|
92
|
+
<div v-else class="w-full h-full flex items-center justify-center" :style="{ backgroundColor: ca(textColor,'04') }">
|
|
93
|
+
<span class="text-6xl font-black" :style="{ opacity: 0.06 }">{{ (items[0].title || '?')[0] }}</span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="p-8 flex-1 flex flex-col">
|
|
97
|
+
<span v-if="items[0].category" class="text-[9px] font-black uppercase tracking-[0.4em] mb-4 border-l-2 pl-2" :style="{ borderColor: textColor, opacity: 0.4 }">{{ items[0].category }}</span>
|
|
98
|
+
<div class="font-black leading-snug mb-3 flex-1 uppercase" :style="{ fontSize: titleFontSize }">{{ items[0].title }}</div>
|
|
99
|
+
<p v-if="items[0].description" :class="[bodyClass, 'leading-relaxed mt-1']" :style="{ opacity: 0.5 }">{{ items[0].description }}</p>
|
|
100
|
+
<div class="flex items-center gap-1.5 mt-6 text-[11px] font-black uppercase tracking-wider transition-all group-hover:gap-3" :style="{ opacity: 0.4 }">Lire <span>→</span></div>
|
|
101
|
+
</div>
|
|
102
|
+
</NuxtLink>
|
|
103
|
+
|
|
104
|
+
<!-- Side items (2+) -->
|
|
105
|
+
<div class="flex flex-col divide-y" :style="{ borderColor: ca(textColor,'10') }">
|
|
106
|
+
<NuxtLink
|
|
107
|
+
v-for="(item, i) in items.slice(1, 3)" :key="i"
|
|
108
|
+
:to="item.slug ? '/' + item.slug : '#'"
|
|
109
|
+
class="group flex flex-col overflow-hidden flex-1 transition-all"
|
|
110
|
+
>
|
|
111
|
+
<div v-if="item.image" class="overflow-hidden shrink-0 border-b" style="aspect-ratio: 16/9;" :style="{ borderColor: ca(textColor,'08') }">
|
|
112
|
+
<img :src="item.image" :alt="item.title" class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105" />
|
|
113
|
+
</div>
|
|
114
|
+
<div v-else class="shrink-0 flex items-center justify-center border-b" style="aspect-ratio: 16/9;" :style="{ backgroundColor: ca(textColor,'03'), borderColor: ca(textColor,'08') }">
|
|
115
|
+
<span class="text-2xl font-black" :style="{ opacity: 0.06 }">{{ (item.title || '?')[0] }}</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="p-5 flex flex-col flex-1">
|
|
118
|
+
<span v-if="item.category" class="text-[9px] font-black uppercase tracking-[0.4em] mb-2" :style="{ opacity: 0.35 }">{{ item.category }}</span>
|
|
119
|
+
<div class="font-black leading-snug flex-1 uppercase" :class="[bodyClass]">{{ item.title }}</div>
|
|
120
|
+
<div class="flex items-center gap-1 mt-3 text-[11px] font-black uppercase tracking-wider transition-all group-hover:gap-2" :style="{ opacity: 0.35 }">Lire <span>→</span></div>
|
|
121
|
+
</div>
|
|
122
|
+
</NuxtLink>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- Empty state -->
|
|
127
|
+
<div v-if="!items?.length" class="py-12 text-center border" :style="{ borderColor: ca(textColor,'08'), opacity: 0.2 }">
|
|
128
|
+
<div class="text-3xl mb-2 font-black">→</div>
|
|
129
|
+
<div class="text-xs font-black uppercase tracking-widest">Add related pages</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
</div>
|
|
133
|
+
</section>
|
|
134
|
+
</template>
|
|
135
|
+
|
|
136
|
+
<script setup>
|
|
137
|
+
const ca = (c, h) => {
|
|
138
|
+
const m = c?.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
|
|
139
|
+
return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : (c || "") + h;
|
|
140
|
+
};
|
|
141
|
+
import { ref, computed, onMounted } from "vue";
|
|
142
|
+
const props = defineProps({
|
|
143
|
+
layout: { type: String, default: "cards" },
|
|
144
|
+
titleTag: { type: String, default: "h2" },
|
|
145
|
+
badge: { type: String, default: "See Also" },
|
|
146
|
+
title: { type: String, default: "" },
|
|
147
|
+
items: { type: Array, default: () => [
|
|
148
|
+
{ title: "Getting Started", slug: "getting-started", description: "Everything you need to know to get up and running.", image: "", category: "Guide" },
|
|
149
|
+
{ title: "Our Services", slug: "services", description: "Discover the full range of services we offer.", image: "", category: "Services" },
|
|
150
|
+
{ title: "Contact Us", slug: "contact", description: "Reach out to our team \u2014 we reply within 24h.", image: "", category: "Support" }
|
|
151
|
+
] },
|
|
152
|
+
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
153
|
+
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
154
|
+
spacing: { type: String, default: "normal" },
|
|
155
|
+
radius: { type: String, default: "sharp" },
|
|
156
|
+
textDirection: { type: String, default: "ltr" },
|
|
157
|
+
contentWidth: { type: String, default: "normal" },
|
|
158
|
+
textSize: { type: String, default: "md" },
|
|
159
|
+
paddingTop: { type: String, default: "" },
|
|
160
|
+
paddingBottom: { type: String, default: "" },
|
|
161
|
+
marginTop: { type: String, default: "" },
|
|
162
|
+
marginBottom: { type: String, default: "" },
|
|
163
|
+
paddingX: { type: String, default: "" }
|
|
164
|
+
});
|
|
165
|
+
const sectionEl = ref(null);
|
|
166
|
+
const isVisible = ref(false);
|
|
167
|
+
onMounted(() => {
|
|
168
|
+
const obs = new IntersectionObserver(([e]) => {
|
|
169
|
+
if (e.isIntersecting) {
|
|
170
|
+
isVisible.value = true;
|
|
171
|
+
obs.disconnect();
|
|
172
|
+
}
|
|
173
|
+
}, { threshold: 0.05 });
|
|
174
|
+
if (sectionEl.value) obs.observe(sectionEl.value);
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
if (!isVisible.value) isVisible.value = true;
|
|
177
|
+
}, 300);
|
|
178
|
+
});
|
|
179
|
+
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");
|
|
180
|
+
const containerClass = computed(() => ({ narrow: "max-w-3xl mx-auto", normal: "max-w-5xl mx-auto", wide: "max-w-7xl mx-auto", full: "max-w-full" })[props.contentWidth] || "max-w-5xl mx-auto");
|
|
181
|
+
const titleFontSize = computed(() => ({ sm: "clamp(0.9rem,1vw,1.1rem)", md: "clamp(1rem,1.5vw,1.4rem)", lg: "clamp(1.5rem,2.5vw,2rem)" })[props.textSize] || "clamp(1rem,1.5vw,1.4rem)");
|
|
182
|
+
const bodyClass = computed(() => ({ sm: "text-xs", md: "text-sm", lg: "text-base" })[props.textSize] || "text-sm");
|
|
183
|
+
const gridCols = computed(() => {
|
|
184
|
+
const n = (props.items || []).length;
|
|
185
|
+
if (n === 1) return "grid-cols-1 max-w-sm";
|
|
186
|
+
if (n === 2) return "grid-cols-1 sm:grid-cols-2";
|
|
187
|
+
return "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3";
|
|
188
|
+
});
|
|
189
|
+
const SM = { none: "0", xs: "8px", sm: "24px", md: "48px", lg: "80px", xl: "128px", "-xs": "-8px", "-sm": "-24px", "-md": "-48px", "-lg": "-80px", "-xl": "-128px" };
|
|
190
|
+
const spacingStyle = computed(() => {
|
|
191
|
+
const s = {};
|
|
192
|
+
if (props.paddingTop && SM[props.paddingTop]) s.paddingTop = SM[props.paddingTop];
|
|
193
|
+
if (props.paddingBottom && SM[props.paddingBottom]) s.paddingBottom = SM[props.paddingBottom];
|
|
194
|
+
if (props.marginTop && SM[props.marginTop]) s.marginTop = SM[props.marginTop];
|
|
195
|
+
if (props.marginBottom && SM[props.marginBottom]) s.marginBottom = SM[props.marginBottom];
|
|
196
|
+
if (props.paddingX && SM[props.paddingX]) {
|
|
197
|
+
const v = SM[props.paddingX];
|
|
198
|
+
if (v.startsWith("-")) {
|
|
199
|
+
s.marginLeft = v;
|
|
200
|
+
s.marginRight = v;
|
|
201
|
+
} else {
|
|
202
|
+
s.paddingLeft = v;
|
|
203
|
+
s.paddingRight = v;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return s;
|
|
207
|
+
});
|
|
208
|
+
</script>
|
|
209
|
+
|
|
210
|
+
<style scoped>
|
|
211
|
+
.sv-out{opacity:0;transform:translateY(16px)}.sv-in{opacity:1;transform:none;transition:opacity .6s ease,transform .6s ease}
|
|
212
|
+
</style>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section ref="sectionEl" :class="[sectionPad, isVisible ? 'sv-in' : 'sv-out']" :dir="textDirection" :style="[{ backgroundColor: bgColor, color: textColor }, spacingStyle]">
|
|
3
|
+
<div :class="maxW">
|
|
4
|
+
|
|
5
|
+
<!-- Optional title -->
|
|
6
|
+
<component v-if="title" :is="titleTag"
|
|
7
|
+
:class="['font-black uppercase leading-[0.9]', titleBorder ? 'pb-6 mb-6 border-b' : 'mb-10']"
|
|
8
|
+
:style="{ fontSize: titleFontSize, letterSpacing: '-0.03em', borderColor: titleBorder ? ca(textColor,'18') : undefined }">{{ title }}</component>
|
|
9
|
+
|
|
10
|
+
<!-- Content -->
|
|
11
|
+
<div class="prose-content text-sm md:text-base leading-relaxed space-y-5" :style="{ opacity: 0.85 }" v-html="rendered"></div>
|
|
12
|
+
|
|
13
|
+
<!-- CTA buttons -->
|
|
14
|
+
<div v-if="ctas && ctas.length" class="flex flex-wrap gap-3 mt-10 pt-8 border-t" :style="{ borderColor: ca(textColor,'12') }">
|
|
15
|
+
<NuxtLink v-for="(cta, i) in ctas" :key="i" :to="cta.link || '#'"
|
|
16
|
+
class="inline-flex items-center gap-3 px-6 py-3 text-[11px] font-black uppercase tracking-widest transition-all hover:gap-6 border"
|
|
17
|
+
:style="i === 0 ? { backgroundColor: textColor, color: bgColor, borderColor: textColor } : { borderColor: ca(textColor,'30'), color: textColor }">
|
|
18
|
+
{{ cta.text }}<span>→</span>
|
|
19
|
+
</NuxtLink>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Placeholder when empty -->
|
|
23
|
+
<div v-if="!content && !title" class="space-y-3">
|
|
24
|
+
<div v-for="n in 6" :key="n" class="h-3" :style="{ backgroundColor: ca(textColor,'0c'), width: n % 3 === 0 ? '60%' : '100%' }"></div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup>
|
|
31
|
+
const ca = (c, h) => {
|
|
32
|
+
const m = c?.match(/rgba\((var\([^)]+\)),\s*[\d.]+\)/);
|
|
33
|
+
return m ? `rgba(${m[1]}, ${Math.round(parseInt(h, 16) / 255 * 1e3) / 1e3})` : (c || "") + h;
|
|
34
|
+
};
|
|
35
|
+
import { ref, computed, onMounted } from "vue";
|
|
36
|
+
const props = defineProps({
|
|
37
|
+
layout: { type: String, default: "article" },
|
|
38
|
+
titleTag: { type: String, default: "h2" },
|
|
39
|
+
titleBorder: { type: Boolean, default: false },
|
|
40
|
+
title: { type: String, default: "" },
|
|
41
|
+
content: { type: String, default: "" },
|
|
42
|
+
ctas: { type: Array, default: () => [] },
|
|
43
|
+
bgColor: { type: String, default: "rgba(var(--body-bg), 1)" },
|
|
44
|
+
textColor: { type: String, default: "rgba(var(--text-color), 1)" },
|
|
45
|
+
spacing: { type: String, default: "normal" },
|
|
46
|
+
radius: { type: String, default: "sharp" },
|
|
47
|
+
textDirection: { type: String, default: "ltr" },
|
|
48
|
+
contentWidth: { type: String, default: "normal" },
|
|
49
|
+
textSize: { type: String, default: "md" },
|
|
50
|
+
paddingTop: { type: String, default: "" },
|
|
51
|
+
paddingBottom: { type: String, default: "" },
|
|
52
|
+
marginTop: { type: String, default: "" },
|
|
53
|
+
marginBottom: { type: String, default: "" },
|
|
54
|
+
paddingX: { type: String, default: "" }
|
|
55
|
+
});
|
|
56
|
+
const sectionEl = ref(null);
|
|
57
|
+
const isVisible = ref(false);
|
|
58
|
+
onMounted(() => {
|
|
59
|
+
const obs = new IntersectionObserver(([e]) => {
|
|
60
|
+
if (e.isIntersecting) {
|
|
61
|
+
isVisible.value = true;
|
|
62
|
+
obs.disconnect();
|
|
63
|
+
}
|
|
64
|
+
}, { threshold: 0.06 });
|
|
65
|
+
if (sectionEl.value) obs.observe(sectionEl.value);
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
if (!isVisible.value) isVisible.value = true;
|
|
68
|
+
}, 300);
|
|
69
|
+
});
|
|
70
|
+
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");
|
|
71
|
+
const maxW = computed(() => ({ narrow: "max-w-2xl mx-auto", normal: "max-w-3xl mx-auto", wide: "max-w-5xl mx-auto", full: "max-w-full" })[props.contentWidth] || "max-w-3xl mx-auto");
|
|
72
|
+
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)");
|
|
73
|
+
const rendered = computed(() => {
|
|
74
|
+
if (!props.content) return "";
|
|
75
|
+
return props.content.replace(/\n\n/g, "</p><p>").replace(/\n/g, "<br>");
|
|
76
|
+
});
|
|
77
|
+
const SM = { none: "0", xs: "8px", sm: "24px", md: "48px", lg: "80px", xl: "128px", "-xs": "-8px", "-sm": "-24px", "-md": "-48px", "-lg": "-80px", "-xl": "-128px" };
|
|
78
|
+
const spacingStyle = computed(() => {
|
|
79
|
+
const s = {};
|
|
80
|
+
if (props.paddingTop && SM[props.paddingTop]) s.paddingTop = SM[props.paddingTop];
|
|
81
|
+
if (props.paddingBottom && SM[props.paddingBottom]) s.paddingBottom = SM[props.paddingBottom];
|
|
82
|
+
if (props.marginTop && SM[props.marginTop]) s.marginTop = SM[props.marginTop];
|
|
83
|
+
if (props.marginBottom && SM[props.marginBottom]) s.marginBottom = SM[props.marginBottom];
|
|
84
|
+
if (props.paddingX && SM[props.paddingX]) {
|
|
85
|
+
const v = SM[props.paddingX];
|
|
86
|
+
if (v.startsWith("-")) {
|
|
87
|
+
s.marginLeft = v;
|
|
88
|
+
s.marginRight = v;
|
|
89
|
+
} else {
|
|
90
|
+
s.paddingLeft = v;
|
|
91
|
+
s.paddingRight = v;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return s;
|
|
95
|
+
});
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<style scoped>
|
|
99
|
+
.sv-out{opacity:0;transform:translateY(16px)}.sv-in{opacity:1;transform:none;transition:opacity .6s ease,transform .6s ease}
|
|
100
|
+
</style>
|