@codecavepro/brand 3.2.0 → 3.2.2

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.
@@ -1,19 +1,29 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
3
 
4
- const props = defineProps<{
4
+ const props = withDefaults(defineProps<{
5
5
  isDisabled?: boolean
6
6
  title?: string
7
7
  as?: 'link'
8
8
  href?: string
9
9
  variant?: 'primary' | 'secondary' | 'tertiary' | 'ghost' | 'text' | 'link' | 'icon'
10
- type?: 'submit'
10
+ type?: 'button' | 'submit'
11
11
  class?: string
12
- }>()
12
+ }>(), {
13
+ type: 'button',
14
+ })
15
+
13
16
  const buttonBaseClass = `flex items-center justify-center
14
- ${props.isDisabled ? 'cursor-not-allowed opacity-20' : 'cursor-pointer'}
17
+ ${props.isDisabled
18
+ ? 'cursor-not-allowed opacity-60'
19
+ : 'cursor-pointer'}
15
20
  w-fit max-w-full min-w-12 min-h-12 rounded-full text-body-primary font-bold transition-colors`
16
- const linkBaseClass = 'flex transition-colors'
21
+
22
+ const linkBaseClass = `flex transition-colors
23
+ ${props.isDisabled
24
+ ? 'cursor-not-allowed opacity-60'
25
+ : 'cursor-pointer'}`
26
+
17
27
  const variantClass = computed(() => {
18
28
  switch (props.variant) {
19
29
  case 'secondary':
@@ -32,14 +42,29 @@ const variantClass = computed(() => {
32
42
  return `${buttonBaseClass} px-6 py-1 bg-primary-500 hover:bg-primary-700 active:bg-primary-900`
33
43
  }
34
44
  })
45
+
46
+ const preventDisabledLinkActivation = (event: Event) => {
47
+ if (props.isDisabled) {
48
+ event.preventDefault()
49
+ event.stopPropagation()
50
+ }
51
+ }
35
52
  </script>
36
53
 
37
54
  <template>
38
- <a v-if="props.as === 'link'" :href="props.href" :class="[variantClass, props.class ?? '']">
55
+ <a v-if="props.as === 'link'"
56
+ :href="props.isDisabled ? '' : props.href"
57
+ :class="[variantClass, props.class ?? '']"
58
+ :aria-disabled="props.isDisabled ? 'true' : undefined"
59
+ :tabindex="props.isDisabled ? -1 : undefined"
60
+ @click="preventDisabledLinkActivation"
61
+ @keydown.enter="preventDisabledLinkActivation"
62
+ @keydown.space.prevent="preventDisabledLinkActivation">
39
63
  <span>{{ title }}</span>
40
64
  <slot />
41
65
  </a>
42
- <button v-else :class="[variantClass, props.class ?? '']">
66
+
67
+ <button v-else :type="props.type" :disabled="props.isDisabled" :class="[variantClass, props.class ?? '']">
43
68
  {{ title }}
44
69
  <slot />
45
70
  </button>
@@ -5,49 +5,68 @@ import { gsap } from "gsap";
5
5
  const props = defineProps<{
6
6
  title: string
7
7
  class?: string
8
- href?: string
8
+ href?: string | null
9
9
  }>()
10
+
10
11
  const isTouchDevice = ref(false)
11
- const link = ref(null)
12
- const glow = ref(null)
13
- const edgeLeft = ref(null)
14
- const edgeRight = ref(null)
15
- let leaveTween = null
12
+ const link = ref<HTMLElement | null>(null)
13
+ const glow = ref<HTMLElement | null>(null)
14
+ const edgeLeft = ref<HTMLElement | null>(null)
15
+ const edgeRight = ref<HTMLElement | null>(null)
16
+
17
+ let leaveTween: gsap.core.Tween | null = null
18
+
16
19
  const handleMouseMove = (e: MouseEvent) => {
17
- if (isTouchDevice.value) return
20
+ if (isTouchDevice.value || !link.value || !glow.value) return
18
21
 
19
22
  const rect = link.value.getBoundingClientRect()
20
23
  const x = e.clientX - rect.left
21
24
  const y = e.clientY - rect.top
22
25
  const percentX = x / rect.width
26
+
23
27
  gsap.to(glow.value, {
24
28
  left: x,
25
29
  top: y,
26
30
  duration: 0.5,
27
31
  ease: 'power2.out',
28
32
  })
33
+
29
34
  const edgeZone = 0.1
30
35
  let leftOpacity = 0
31
36
  let rightOpacity = 0
37
+
32
38
  if (percentX < edgeZone) {
33
39
  leftOpacity = 1 - percentX / edgeZone
34
40
  } else if (percentX > 1 - edgeZone) {
35
41
  rightOpacity = (percentX - (1 - edgeZone)) / edgeZone
36
42
  }
37
- edgeLeft.value.style.opacity = leftOpacity
38
- edgeRight.value.style.opacity = rightOpacity
43
+
44
+ if (edgeLeft.value) {
45
+ edgeLeft.value.style.opacity = String(leftOpacity)
46
+ }
47
+
48
+ if (edgeRight.value) {
49
+ edgeRight.value.style.opacity = String(rightOpacity)
50
+ }
39
51
  }
52
+
40
53
  const handleMouseEnter = () => {
41
54
  if (isTouchDevice.value) return
55
+
42
56
  if (leaveTween) {
43
57
  leaveTween.kill()
44
58
  leaveTween = null
45
59
  }
46
60
  }
61
+
47
62
  const handleMouseLeave = () => {
48
- edgeLeft.value.style.opacity = 1
49
- edgeRight.value.style.opacity = 0
63
+ if (edgeLeft.value) edgeLeft.value.style.opacity = '1'
64
+ if (edgeRight.value) edgeRight.value.style.opacity = '0'
65
+
50
66
  if (leaveTween) leaveTween.kill()
67
+
68
+ if (!glow.value) return
69
+
51
70
  leaveTween = gsap.to(glow.value, {
52
71
  left: '20%',
53
72
  top: '50%',
@@ -56,35 +75,49 @@ const handleMouseLeave = () => {
56
75
  ease: 'power2.out',
57
76
  })
58
77
  }
78
+
59
79
  onMounted(() => {
60
- isTouchDevice.value = window.matchMedia("(pointer: coarse)").matches
61
- || "ontouchstart" in window
62
- || navigator.maxTouchPoints > 0
80
+ isTouchDevice.value =
81
+ window.matchMedia('(pointer: coarse)').matches ||
82
+ 'ontouchstart' in window ||
83
+ navigator.maxTouchPoints > 0
63
84
  })
64
85
  </script>
65
86
 
66
87
  <template>
67
88
  <div :class="`relative inline-block ${props.class ?? ''}`">
68
89
  <div ref="edgeLeft" class="edge-glow -left-2"></div>
69
- <a ref="link" :href="href" @mousemove="handleMouseMove" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" :class="`cursor-pointer flex items-center justify-center relative z-[1] overflow-hidden w-full h-12 px-6 py-1 rounded-full bg-glow-25 ${props.class ?? ''}`">
70
- <span class="relative z-[2] text-primary-800 font-bold select-none">
90
+
91
+ <component
92
+ :is="href ? 'a' : 'span'"
93
+ ref="link"
94
+ :href="href || undefined"
95
+ @mousemove="handleMouseMove"
96
+ @mouseenter="handleMouseEnter"
97
+ @mouseleave="handleMouseLeave"
98
+ class="cursor-pointer flex items-center justify-center relative z-[1] overflow-hidden w-full h-12 px-6 py-1 rounded-full bg-glow-25"
99
+ >
100
+ <p class="relative z-[2] text-primary-800 font-bold select-none">
71
101
  {{ title }}
72
- </span>
102
+ </p>
103
+
73
104
  <div ref="glow" class="glow"></div>
74
- </a>
105
+ </component>
106
+
75
107
  <div ref="edgeRight" class="edge-glow -right-2 opacity-0"></div>
76
108
  </div>
77
109
  </template>
78
110
 
79
111
  <style scoped>
80
- a {
112
+ span, a {
81
113
  box-shadow: 0 0 64px 0 #7A58FFA8,
82
114
  0 0 16px 0 #4F22FFA6,
83
115
  0 0 4px 2px #5B34FA;
84
116
  transition: transform 0.5s ease;
85
117
  }
86
118
 
87
- a:active {
119
+
120
+ span, a:active {
88
121
  transform: scale(0.98);
89
122
  }
90
123
 
@@ -2,6 +2,7 @@
2
2
  import LinkedinIcon from "../assets/icons/linkedin-icon.vue";
3
3
  import VerifiedIcon from "../assets/icons/verified-icon.vue";
4
4
  import LazyImage from "./images/LazyImage.vue"
5
+ import { isCorrectLinkedInFormat } from "../helpers/form-validator.ts";
5
6
 
6
7
  /* See ArticlePreview.vue: the fields read here, not the generated Strapi
7
8
  * `Testimonial`, which a caller may still pass unchanged. */
@@ -27,14 +28,14 @@ const imageUrl = (url: string) => props.resolveImage?.(url) ?? url
27
28
  <template>
28
29
  <div :class="`mx-1 lg:mx-2 testimonial space-y-2 lg:space-y-3 py-10 px-6 lg:px-11 ${className}`">
29
30
  <div class="flex flex-col lg:flex-row gap-5 lg:items-center">
30
- <LazyImage v-if="item.photo.name !== 'no-image.svg'"
31
+ <img v-if="item.photo.name !== ''"
31
32
  class="w-12 lg:w-16 h-12 lg:h-16" :src="imageUrl(item.photo.url)"
32
33
  :alt=item.photo.name />
33
34
  <div>
34
35
  <div class="flex items-center gap-2">
35
36
  <p class="text-heading text-lg lg:text-xl">{{ item.name }}</p>
36
37
  <a
37
- v-if="item.linkedinurl.length > 1"
38
+ v-if="isCorrectLinkedInFormat(item.linkedinurl)"
38
39
  target="_blank"
39
40
  rel='noopener noreferrer'
40
41
  class="text-default-transparent hover:text-action transition-colors"
@@ -2,39 +2,53 @@
2
2
  import { ref } from "vue";
3
3
 
4
4
  defineProps<{
5
- id: string
6
- label: string
7
- placeholder: string
8
- isRequired?: boolean
9
- isError?: boolean
10
- errorMessage?: string,
11
- modelValue?: string
12
- }>()
13
-
14
- const textareaRef = ref(null)
15
- const autoResize = () => {
16
- if (textareaRef.value) {
17
- textareaRef.value.style.height = 'auto'
18
- textareaRef.value.style.height = textareaRef.value.scrollHeight + 'px'
19
- }
20
- }
5
+ id: string;
6
+ label: string;
7
+ placeholder: string;
8
+ isRequired?: boolean;
9
+ isError?: boolean;
10
+ errorMessage?: string;
11
+ modelValue?: string;
12
+ }>();
13
+
14
+ const emit = defineEmits(["update:modelValue"]);
15
+
16
+ const textareaRef = ref<HTMLTextAreaElement | null>(null);
17
+
18
+ const MAX_HEIGHT = 300;
21
19
 
22
- defineEmits(['update:modelValue'])
20
+ const autoResize = (event: Event) => {
21
+ const textarea = event.target as HTMLTextAreaElement;
22
+
23
+ textarea.style.height = "auto";
24
+
25
+ const height = Math.min(textarea.scrollHeight, MAX_HEIGHT);
26
+ textarea.style.height = `${height}px`;
27
+ textarea.style.overflowY =
28
+ textarea.scrollHeight > MAX_HEIGHT ? "auto" : "hidden";
29
+
30
+ emit("update:modelValue", textarea.value);
31
+ };
23
32
  </script>
24
33
 
25
34
  <template>
26
35
  <div class="w-full relative">
27
- <div :class="`focus-area flex flex-col min-h-[7.5rem] p-3 rounded-lg bg-surface-secondary border border-surface-tertiary hover:bg-surface-tertiary transition-colors text-heading
28
- ${isError ? 'input-error' : ''}`">
36
+ <div :class="`focus-area flex flex-col min-h-[7.5rem] p-3 rounded-lg bg-surface-secondary border border-surface-tertiary hover:bg-surface-tertiary transition-colors text-heading
37
+ ${isError ? 'input-error' : ''}`"
38
+ >
29
39
  <label :for="id" class="font-bold text-sm select-none">
30
40
  {{ label }}
31
41
  </label>
32
- <textarea
33
- @input="autoResize"
34
- ref="textareaRef"
35
- :id="id" :placeholder="placeholder" :value="modelValue" @change="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
36
- :class="`w-full flex-grow resize-none overflow-hidden placeholder:pt-1.5 placeholder:text-xs placeholder:text-body-secondary outline-none
37
- ${isError ? 'text-error focus:text-error' : 'text-hovered focus:text-hovered'}`" />
42
+
43
+ <textarea
44
+ ref="textareaRef"
45
+ :id="id"
46
+ :placeholder="placeholder"
47
+ :value="modelValue"
48
+ @input="autoResize"
49
+ :class="`w-full flex-grow resize-none placeholder:pt-1.5 placeholder:text-xs placeholder:text-body-secondary outline-none
50
+ ${isError ? 'text-error focus:text-error' : 'text-hovered focus:text-hovered'}`"
51
+ />
38
52
  </div>
39
53
  <span v-if="isError" class="text-error text-xs">
40
54
  {{ errorMessage }}
@@ -44,12 +58,14 @@ defineEmits(['update:modelValue'])
44
58
 
45
59
  <style scoped>
46
60
  .focus-area:focus-within {
47
- box-shadow: 0 0 16px 0 hsl(from var(--color-brand-500) h s l / 0.5),
61
+ box-shadow:
62
+ 0 0 16px 0 hsl(from var(--color-brand-500) h s l / 0.5),
48
63
  0 0 4px 0 hsl(from var(--color-brand-500) h s l / 0.8);
49
64
  }
50
65
 
51
66
  .input-error {
52
- box-shadow: 0 0 16px 0 hsl(from var(--color-error-200) h s l / 0.5),
67
+ box-shadow:
68
+ 0 0 16px 0 hsl(from var(--color-error-200) h s l / 0.5),
53
69
  0 0 4px 0 hsl(from var(--color-error-100) h s l / 0.6);
54
70
  }
55
71
  </style>
@@ -76,7 +76,7 @@ const handleCloseMenu = () => {
76
76
  <template>
77
77
  <div>
78
78
  <div class="fixed z-50 top-0 w-full bg-surface-primary-transparent backdrop-blur-3xl border-b border-surface-quaternary">
79
- <div :class="`px-5 ${isMenuOpen ? 'pb-5' : 'pb-0'} flex flex-col gap-2 transition-all max-h-screen`">
79
+ <div :class="['px-5 flex flex-col gap-2 transition-all', isMenuOpen ? 'pb-5 h-dvh' : 'pb-0']">
80
80
  <div class="flex justify-between items-center py-1.5 shrink-0">
81
81
  <a :href="homeHref" class="px-1.5 hover:opacity-80 transition-opacity">
82
82
  <img :src="logo" alt="CODECAVE" />
@@ -85,12 +85,12 @@ const handleCloseMenu = () => {
85
85
  <span></span>
86
86
  </button>
87
87
  </div>
88
- <nav v-if="isMenuOpen" class="flex-1 min-h-0 overflow-y-auto text-sm">
88
+ <nav v-if="isMenuOpen" class="flex-1 min-h-0 overflow-hidden text-sm">
89
89
  <div v-if="isServicesOpen" class="h-full flex flex-col min-h-0">
90
90
  <h2 class="text-center font-bold text-body-primary py-2.5 shrink-0">
91
91
  {{ servicesItem?.submenuTitle }}
92
92
  </h2>
93
- <div class="flex-1 min-h-0 overflow-y-auto">
93
+ <div class="services-scroll flex-1 min-h-0 overflow-y-auto overscroll-contain">
94
94
  <slot name="services" />
95
95
  </div>
96
96
 
@@ -136,4 +136,13 @@ const handleCloseMenu = () => {
136
136
  background-color: currentColor;
137
137
  border-radius: 3rem;
138
138
  }
139
+
140
+ .services-scroll {
141
+ min-height: 0;
142
+ overflow-y: auto;
143
+ overflow-x: hidden;
144
+ -webkit-overflow-scrolling: touch;
145
+ overscroll-behavior-y: contain;
146
+ touch-action: pan-y;
147
+ }
139
148
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codecavepro/brand",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
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",