@codecavepro/brand 2.8.1 → 3.0.1
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 +2 -3
- package/dist/colors_and_type.css +2 -2
- package/dist/src/common/HeaderBrows.vue +16 -7
- package/dist/src/{homepage → common}/technology-card.vue +1 -1
- package/dist/src/footer/FooterCta.vue +9 -3
- package/package.json +2 -6
- package/dist/src/common/forms/ResearchForm.vue +0 -56
- package/dist/src/helpers/breakpoints.ts +0 -5
- package/dist/src/helpers/carousel-gesture-locker.ts +0 -72
- package/dist/src/homepage/technologies.vue +0 -136
package/README.md
CHANGED
|
@@ -155,7 +155,7 @@ Two things to know about it:
|
|
|
155
155
|
|
|
156
156
|
## The components
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
20 components and 13 icons — the buttons, the form controls and the forms
|
|
159
159
|
themselves, the nav bar, the footer link group, the article and technology
|
|
160
160
|
cards, the review card and the typing effect codecave.pro renders — ship as
|
|
161
161
|
**source**, byte-for-byte the files the site builds from. Not a reimplementation
|
|
@@ -212,8 +212,7 @@ surprises you.
|
|
|
212
212
|
|
|
213
213
|
`vue` is the only required peer. Four more are optional, each wanted by one or two
|
|
214
214
|
components; leave one out and only those are affected — `gsap` by `GlowButton.vue`,
|
|
215
|
-
`effects/TypingEffect.vue` and `forms/ContactUsForm.vue`; `
|
|
216
|
-
`homepage/technologies.vue`; `marked` and `isomorphic-dompurify` by
|
|
215
|
+
`effects/TypingEffect.vue` and `forms/ContactUsForm.vue`; `marked` and `isomorphic-dompurify` by
|
|
217
216
|
`project/pain-points-item.vue`.
|
|
218
217
|
|
|
219
218
|
### Import the theme, not just the tokens
|
package/dist/colors_and_type.css
CHANGED
|
@@ -619,7 +619,7 @@ a:hover { color: var(--color-hovered); }
|
|
|
619
619
|
|
|
620
620
|
/* ---------------------------------------------------------------------------
|
|
621
621
|
* Cards — sources: homepage/projects.astro, common/ArticlePreview.vue,
|
|
622
|
-
*
|
|
622
|
+
* common/technology-card.vue
|
|
623
623
|
* Depth is never built from surface contrast: the card sits ONE narrow step
|
|
624
624
|
* off the page (#0F0F15 on #0A0A0B). Radius and border carry the separation.
|
|
625
625
|
* ------------------------------------------------------------------------ */
|
|
@@ -636,7 +636,7 @@ a:hover { color: var(--color-hovered); }
|
|
|
636
636
|
.card-article { border-radius: var(--radius-article); }
|
|
637
637
|
|
|
638
638
|
/* Feature card — gradient hairline drawn with mask-composite, blurred
|
|
639
|
-
* backdrop, no fill. Source:
|
|
639
|
+
* backdrop, no fill. Source: common/technology-card.vue.
|
|
640
640
|
* Since the rebuild brand-400 is violet (#5F3ABD), so the ring reads violet
|
|
641
641
|
* end to end; cyan survives only in the wash tokens above. */
|
|
642
642
|
.card-feature {
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Button from "./Button.vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
brows: {
|
|
6
|
+
header: string;
|
|
7
|
+
description: string;
|
|
8
|
+
}[]
|
|
9
|
+
}>()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
1
12
|
<template>
|
|
2
13
|
<div class="flex flex-col md:flex-row gap-4 md:gap-6 text-lg md:text-xl">
|
|
14
|
+
<div v-for="brow in props.brows">
|
|
3
15
|
<div class="space-x-1">
|
|
4
|
-
<span class="font-bold text-body-primary">
|
|
5
|
-
<span class="text-body-secondary">
|
|
16
|
+
<span class="font-bold text-body-primary">{{ brow.header }}</span>
|
|
17
|
+
<span class="text-body-secondary">{{ brow.description }}</span>
|
|
6
18
|
</div>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<span class="text-body-secondary">Average rating</span>
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
12
21
|
</template>
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import Button from "../common/Button.vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
header?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
subDescription?: string;
|
|
8
|
+
}>()
|
|
3
9
|
</script>
|
|
4
10
|
|
|
5
11
|
<template>
|
|
@@ -8,13 +14,13 @@ import Button from "../common/Button.vue";
|
|
|
8
14
|
>
|
|
9
15
|
<div class="flex-1">
|
|
10
16
|
<h3 class="text-heading font-bold xl:text-[32px] text-2xl">
|
|
11
|
-
|
|
17
|
+
{{ props.header }}
|
|
12
18
|
</h3>
|
|
13
19
|
|
|
14
20
|
<p class="md:mt-1 mt-4 text-body-secondary-lighter text-pretty">
|
|
15
|
-
|
|
21
|
+
{{ props.description }}
|
|
16
22
|
<br>
|
|
17
|
-
|
|
23
|
+
{{ props.subDescription }}
|
|
18
24
|
</p>
|
|
19
25
|
</div>
|
|
20
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codecavepro/brand",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "CODECAVE design system — colour, typography and layout tokens as CSS custom properties and as a typed module.",
|
|
5
5
|
"license": "Unlicense",
|
|
6
6
|
"type": "module",
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
"gsap": "^3.13.0",
|
|
42
42
|
"isomorphic-dompurify": "^3.22.0",
|
|
43
43
|
"marked": "^18.0.5",
|
|
44
|
-
"vue": "^3.5.22"
|
|
45
|
-
"vue3-carousel": "^0.17.0"
|
|
44
|
+
"vue": "^3.5.22"
|
|
46
45
|
},
|
|
47
46
|
"peerDependenciesMeta": {
|
|
48
47
|
"gsap": {
|
|
@@ -53,9 +52,6 @@
|
|
|
53
52
|
},
|
|
54
53
|
"marked": {
|
|
55
54
|
"optional": true
|
|
56
|
-
},
|
|
57
|
-
"vue3-carousel": {
|
|
58
|
-
"optional": true
|
|
59
55
|
}
|
|
60
56
|
},
|
|
61
57
|
"scripts": {
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import InputText from '../InputText.vue'
|
|
4
|
-
import Button from '../Button.vue'
|
|
5
|
-
import { isCorrectEmailFormat } from "../../helpers/form-validator.ts";
|
|
6
|
-
|
|
7
|
-
const email = ref('')
|
|
8
|
-
|
|
9
|
-
const hasError = ref(false)
|
|
10
|
-
const emailErrorMessage = ref('')
|
|
11
|
-
|
|
12
|
-
const submitForm = () => {
|
|
13
|
-
hasError.value = false
|
|
14
|
-
emailErrorMessage.value = ''
|
|
15
|
-
|
|
16
|
-
if (!email.value.trim()) {
|
|
17
|
-
hasError.value = true
|
|
18
|
-
emailErrorMessage.value = 'This field is required'
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!isCorrectEmailFormat(email.value)) {
|
|
23
|
-
hasError.value = true
|
|
24
|
-
emailErrorMessage.value = 'Please enter valid email'
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
</script>
|
|
29
|
-
|
|
30
|
-
<template>
|
|
31
|
-
<form
|
|
32
|
-
novalidate
|
|
33
|
-
class="flex flex-col gap-4 md:flex-row md:items-center"
|
|
34
|
-
@submit.prevent="submitForm"
|
|
35
|
-
>
|
|
36
|
-
<div class="md:w-64 xl:w-80">
|
|
37
|
-
<InputText
|
|
38
|
-
id="file-email"
|
|
39
|
-
v-model="email"
|
|
40
|
-
is-required
|
|
41
|
-
:isError="hasError"
|
|
42
|
-
:errorMessage="emailErrorMessage"
|
|
43
|
-
label="E-mail"
|
|
44
|
-
type="email"
|
|
45
|
-
placeholder="Where should we send it?"
|
|
46
|
-
autocomplete="email"
|
|
47
|
-
/>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<Button
|
|
51
|
-
title="Grab research"
|
|
52
|
-
variant="tertiary"
|
|
53
|
-
class="w-40 flex-shrink-0"
|
|
54
|
-
/>
|
|
55
|
-
</form>
|
|
56
|
-
</template>
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { onMounted, onUnmounted, ref } from 'vue';
|
|
2
|
-
|
|
3
|
-
export function carouselGestureLocker() {
|
|
4
|
-
const carouselRoot = ref<HTMLElement | null>(null)
|
|
5
|
-
|
|
6
|
-
let viewport: HTMLElement | null = null
|
|
7
|
-
|
|
8
|
-
let startX = 0
|
|
9
|
-
let startY = 0
|
|
10
|
-
let gestureLocked = false
|
|
11
|
-
let isVerticalGesture = false
|
|
12
|
-
|
|
13
|
-
const onTouchStart = (e: TouchEvent) => {
|
|
14
|
-
startX = e.touches[0].clientX
|
|
15
|
-
startY = e.touches[0].clientY
|
|
16
|
-
|
|
17
|
-
gestureLocked = false
|
|
18
|
-
isVerticalGesture = false
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const onTouchMove = (e: TouchEvent) => {
|
|
22
|
-
const dx = Math.abs(e.touches[0].clientX - startX)
|
|
23
|
-
const dy = Math.abs(e.touches[0].clientY - startY)
|
|
24
|
-
|
|
25
|
-
if (!gestureLocked) {
|
|
26
|
-
if (dx < 10 && dy < 10) return
|
|
27
|
-
|
|
28
|
-
gestureLocked = true
|
|
29
|
-
isVerticalGesture = dy > dx
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (isVerticalGesture) {
|
|
33
|
-
e.stopImmediatePropagation()
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const onTouchEnd = () => {
|
|
38
|
-
gestureLocked = false
|
|
39
|
-
isVerticalGesture = false
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
onMounted(() => {
|
|
43
|
-
|
|
44
|
-
viewport =
|
|
45
|
-
carouselRoot.value?.querySelector('.carousel__viewport') ?? null
|
|
46
|
-
|
|
47
|
-
if (!viewport) return
|
|
48
|
-
|
|
49
|
-
viewport.addEventListener('touchstart', onTouchStart, {
|
|
50
|
-
passive: true,
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
viewport.addEventListener('touchmove', onTouchMove, {
|
|
54
|
-
passive: false,
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
viewport.addEventListener('touchend', onTouchEnd)
|
|
58
|
-
|
|
59
|
-
viewport.addEventListener('touchcancel', onTouchEnd)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
onUnmounted(() => {
|
|
63
|
-
if (!viewport) return
|
|
64
|
-
|
|
65
|
-
viewport.removeEventListener('touchstart', onTouchStart)
|
|
66
|
-
viewport.removeEventListener('touchmove', onTouchMove)
|
|
67
|
-
viewport.removeEventListener('touchend', onTouchEnd)
|
|
68
|
-
viewport.removeEventListener('touchcancel', onTouchEnd)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
return carouselRoot;
|
|
72
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, ref } from "vue";
|
|
3
|
-
import { Carousel, type CarouselConfig, Slide } from "vue3-carousel";
|
|
4
|
-
import "vue3-carousel/carousel.css";
|
|
5
|
-
import { BREAKPOINTS } from "../helpers/breakpoints.ts";
|
|
6
|
-
import GlowButton from "../common/GlowButton.vue";
|
|
7
|
-
import TypingEffect from "../common/effects/TypingEffect.vue";
|
|
8
|
-
import HeaderBrows from "../common/HeaderBrows.vue";
|
|
9
|
-
import TechnologyCard from "./technology-card.vue";
|
|
10
|
-
import { carouselGestureLocker } from "../helpers/carousel-gesture-locker.ts";
|
|
11
|
-
|
|
12
|
-
/* See ArticlePreview.vue: the fields read here, not the generated Strapi
|
|
13
|
-
* `Technology`, which a caller may still pass unchanged. This component
|
|
14
|
-
* renders no images, so it needs no resolver. */
|
|
15
|
-
type TechnologySummary = {
|
|
16
|
-
name: string
|
|
17
|
-
title1: string
|
|
18
|
-
title2: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const props = defineProps<{
|
|
22
|
-
technologies: TechnologySummary[]
|
|
23
|
-
hideSelected?: boolean
|
|
24
|
-
/* Where the consultation button goes. Was `paths.contactUs`, read straight
|
|
25
|
-
* off this site's route table. */
|
|
26
|
-
ctaHref?: string
|
|
27
|
-
/* Technology name to service URL. Plain data rather than a resolver function
|
|
28
|
-
* because every call site mounts this with `client:only`, and Astro can only
|
|
29
|
-
* hand an island props it can serialise. @helpers/service-links.ts holds the
|
|
30
|
-
* site's map; a name that is missing renders an inert button. */
|
|
31
|
-
serviceLinks?: Record<string, string>
|
|
32
|
-
}>()
|
|
33
|
-
|
|
34
|
-
const activeIndex = ref(0)
|
|
35
|
-
const defaultAutoplayValue = 12000
|
|
36
|
-
const autoplay = ref(defaultAutoplayValue)
|
|
37
|
-
const handleMouseOver = (index: number) => {
|
|
38
|
-
activeIndex.value = index
|
|
39
|
-
autoplay.value = 0
|
|
40
|
-
}
|
|
41
|
-
const selectedTechnology = computed(() => {
|
|
42
|
-
return props.technologies[activeIndex.value]
|
|
43
|
-
})
|
|
44
|
-
const handleMouseLeave = () => {
|
|
45
|
-
activeIndex.value = 0
|
|
46
|
-
autoplay.value = defaultAutoplayValue
|
|
47
|
-
}
|
|
48
|
-
const config = computed<Partial<CarouselConfig>>(() => ({
|
|
49
|
-
gap: 8,
|
|
50
|
-
itemsToShow: 'auto',
|
|
51
|
-
autoplay: autoplay.value,
|
|
52
|
-
wrapAround: true,
|
|
53
|
-
height: 220,
|
|
54
|
-
snapAlign: 'center',
|
|
55
|
-
pauseAutoplayOnHover: true,
|
|
56
|
-
transition: 500,
|
|
57
|
-
breakpoints: {
|
|
58
|
-
[BREAKPOINTS.md]: {
|
|
59
|
-
gap: 16,
|
|
60
|
-
},
|
|
61
|
-
[BREAKPOINTS.xl]: {
|
|
62
|
-
enabled: false,
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
}))
|
|
66
|
-
const carouselLocker = carouselGestureLocker()
|
|
67
|
-
</script>
|
|
68
|
-
|
|
69
|
-
<template>
|
|
70
|
-
<section :class="`page-container
|
|
71
|
-
${hideSelected ? 'pt-8' : 'flex flex-col pt-4 md:pt-14 pb-20 xl:pb-0 swipe-over'}`">
|
|
72
|
-
<div v-if="!hideSelected" class="flex flex-col items-center text-center">
|
|
73
|
-
<div class="space-y-3">
|
|
74
|
-
<div class="flex justify-center gap-6 text-xl">
|
|
75
|
-
<div class="space-x-1">
|
|
76
|
-
<span class="font-bold text-body-primary">15+</span>
|
|
77
|
-
<span class="text-body-secondary-lighter">Years of experience</span>
|
|
78
|
-
</div>
|
|
79
|
-
<div class="space-x-1">
|
|
80
|
-
<span class="font-bold text-body-primary">4.7</span>
|
|
81
|
-
<span class="text-body-secondary-lighter">Average rating</span>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
<TypingEffect
|
|
85
|
-
:text1="selectedTechnology.title1"
|
|
86
|
-
:text2="selectedTechnology.title2"
|
|
87
|
-
/>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
<div v-if="!hideSelected" class="self-center pt-10 pb-[4.5rem] md:pt-14 md:pb-16">
|
|
91
|
-
<GlowButton title="Get a free consultation" :href="ctaHref" />
|
|
92
|
-
</div>
|
|
93
|
-
<div v-else class="pt-[7.5rem] pb-20 space-y-7 text-center">
|
|
94
|
-
<h2 class="font-bold text-heading-md text-heading">
|
|
95
|
-
Looking for something specific?
|
|
96
|
-
</h2>
|
|
97
|
-
<p class="text-xl mx-auto max-w-lg text-body-secondary-lighter">
|
|
98
|
-
We offer a variety of services that might be a game-changer for your business.
|
|
99
|
-
</p>
|
|
100
|
-
</div>
|
|
101
|
-
<div class="hidden xl:block relative h-80">
|
|
102
|
-
<div
|
|
103
|
-
v-for="(item, index) in technologies"
|
|
104
|
-
:key="index"
|
|
105
|
-
@mouseover="handleMouseOver(index)"
|
|
106
|
-
@mouseleave="handleMouseLeave"
|
|
107
|
-
>
|
|
108
|
-
<TechnologyCard
|
|
109
|
-
:name="item.name"
|
|
110
|
-
:href="serviceLinks?.[item.name]"
|
|
111
|
-
:active="activeIndex === index"
|
|
112
|
-
className="w-72 h-72"
|
|
113
|
-
:index="index"
|
|
114
|
-
/>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
<div ref="carouselLocker">
|
|
118
|
-
<Carousel
|
|
119
|
-
v-model="activeIndex"
|
|
120
|
-
v-bind="config"
|
|
121
|
-
class="-mx-4 xl:mx-0 xl:hidden"
|
|
122
|
-
>
|
|
123
|
-
<Slide v-for="(item, index) in technologies" :key="index">
|
|
124
|
-
<div class="flex items-center w-[190px] h-[220px]">
|
|
125
|
-
<TechnologyCard
|
|
126
|
-
:name="item.name"
|
|
127
|
-
:href="serviceLinks?.[item.name]"
|
|
128
|
-
:active="activeIndex === index"
|
|
129
|
-
:className="`w-full h-full ${activeIndex === index ? 'max-h-full' : 'max-h-[190px]'}`"
|
|
130
|
-
/>
|
|
131
|
-
</div>
|
|
132
|
-
</Slide>
|
|
133
|
-
</Carousel>
|
|
134
|
-
</div>
|
|
135
|
-
</section>
|
|
136
|
-
</template>
|