@codecavepro/brand 1.6.0 → 2.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 CHANGED
@@ -58,7 +58,7 @@ else:
58
58
  @import "@codecavepro/brand/tokens.css";
59
59
  ```
60
60
 
61
- Same 102 properties on `:root`, same values, zero rules. Nothing it declares can change
61
+ Same 103 properties on `:root`, same values, zero rules. Nothing it declares can change
62
62
  how a single existing element renders — a `var()` only takes effect where you write
63
63
  one. Fonts come with it only as a *name*: `--font-sans` says Satoshi, and declaring the
64
64
  faces stays your call (see below).
@@ -149,10 +149,34 @@ Two things to know about it:
149
149
 
150
150
  ## The components
151
151
 
152
- 19 components and 13 icons — the buttons, form controls, menu, footer, and the
153
- article, testimonial and technology cards codecave.pro renders — ship as **source**, byte-for-byte the files the site builds
154
- from. Not a reimplementation of them, and not a bundle compiled from a snapshot:
155
- the same files, kept identical by a check that runs on every push.
152
+ 13 components and 13 icons — the buttons, the form controls and the forms
153
+ themselves, the review card and the typing effect codecave.pro renders — ship as
154
+ **source**, byte-for-byte the files the site builds from. Not a reimplementation
155
+ of them, and not a bundle compiled from a snapshot: the same files, kept
156
+ identical by a check that runs on every push.
157
+
158
+ **The site's own menus are deliberately absent, and `BrandNav` is why they can
159
+ be.** The footer link group, the article and technology cards and the two menus
160
+ all reach codecave.pro's route table and menu data, so shipping them would put
161
+ one site's navigation behind an npm release — changing a menu item would mean
162
+ publishing. `BrandNav` is the same bar with the coupling removed: it takes its
163
+ items, its wordmark and its active state as props and reaches nothing.
164
+
165
+ ```vue
166
+ <BrandNav
167
+ :left="[{ name: &apos;Workflow&apos;, href: &apos;/workflow&apos; }, { name: &apos;Services&apos;, slot: &apos;services&apos; }]"
168
+ :right="[{ name: &apos;Insights&apos;, href: &apos;/insights&apos; }]"
169
+ :logo="wordmarkUrl"
170
+ current="Workflow"
171
+ >
172
+ <template #services><YourServicesPanel /></template>
173
+ </BrandNav>
174
+ ```
175
+
176
+ Render it without a `client:` directive and it ships no JavaScript: the dropdown
177
+ is CSS hover and the responsive wrap is a media query. **The wordmark is a URL
178
+ you pass in** — this package ships no brand marks, so the logo is yours to
179
+ resolve through your own asset pipeline.
156
180
 
157
181
  ```vue
158
182
  <script setup lang="ts">
@@ -297,6 +297,20 @@
297
297
  --control-height: 3rem; /* 48px — every button and pill */
298
298
  --input-height: 4rem; /* 64px — floating-label text input */
299
299
 
300
+ /* The tick and dot transition, read from the scoped styles of Checkbox.vue
301
+ * and Radio.vue and never used as a utility class — so it belongs here and
302
+ * NOT in theme.css: a Tailwind @theme entry would emit a duration-control
303
+ * utility nothing asks for, and this has to exist unconditionally rather than
304
+ * because some unrelated utility happened to pull it in.
305
+ *
306
+ * It was the one name a shipped component reached for that the package did
307
+ * not declare. codecave.pro carried it privately in its own :root, so the
308
+ * site looked right while any other consumer got `transition: var(...)` with
309
+ * nothing behind it — invalid at computed-value time, so the tick appeared
310
+ * instantly instead of scaling in. Same 150ms the site has always used;
311
+ * moving it changes nothing anywhere and makes the package self-contained. */
312
+ --duration-control: 150ms;
313
+
300
314
  /* =========================================================================
301
315
  * ELEVATION — CODECAVE's light comes from BELOW.
302
316
  * Never substitute a conventional downward drop shadow for --shadow-section.
@@ -0,0 +1,4 @@
1
+ <svg viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M8.34124 0.592028C8.49972 0.441153 8.71061 0.357744 8.92941 0.359399C9.14822 0.361055 9.35782 0.447646 9.514 0.600902C9.67017 0.754158 9.7607 0.96209 9.76649 1.18082C9.77227 1.39956 9.69285 1.61198 9.54499 1.77328L5.05624 7.38703C4.97906 7.47016 4.8859 7.53688 4.78234 7.58319C4.67878 7.6295 4.56695 7.65445 4.45353 7.65655C4.3401 7.65865 4.22742 7.63786 4.12222 7.59541C4.01702 7.55297 3.92145 7.48975 3.84124 7.40953L0.864494 4.43278C0.781597 4.35553 0.715107 4.26238 0.668991 4.15888C0.622875 4.05538 0.598077 3.94366 0.596079 3.83037C0.59408 3.71708 0.61492 3.60454 0.657356 3.49948C0.699792 3.39442 0.762955 3.29898 0.843076 3.21886C0.923197 3.13874 1.01864 3.07558 1.1237 3.03314C1.22876 2.9907 1.34129 2.96986 1.45458 2.97186C1.56787 2.97386 1.6796 2.99866 1.7831 3.04477C1.8866 3.09089 1.97975 3.15738 2.05699 3.24028L4.41274 5.5949L8.31987 0.616779C8.3269 0.608117 8.33442 0.599854 8.34237 0.592028H8.34124Z"
3
+ fill="#E8E6F0" />
4
+ </svg>
@@ -0,0 +1,190 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The CODECAVE top bar. Ghost-button links split around a centred wordmark.
4
+ *
5
+ * AUTHORED, not captured. Every other component this package ships is a copy of
6
+ * a file codecave.pro builds from, and docs/source_examples/ holds nothing
7
+ * anyone wrote here. This one is the exception and lives in docs/authored/
8
+ * because it has no upstream: it is the site bar and the docs bar reconciled
9
+ * into one, so neither repo owns it any more.
10
+ *
11
+ * WHY IT CAN SHIP WHEN THE MENUS COULD NOT. desktop-menu.vue and mobile-menu.vue
12
+ * are excluded from this package (CCWEB2-371) because they reach the site paths.ts
13
+ * and menu.ts -- installing one would put codecave.pro navigation behind an npm
14
+ * release. This takes its items, its logo and its active state as PROPS, so it
15
+ * reaches nothing. That is the whole difference, and it is why the same bar can
16
+ * serve a marketing site and a documentation site without either one lending the
17
+ * other its route table.
18
+ *
19
+ * NO CLIENT DIRECTIVE. Rendered from Astro without `client:*` this emits static
20
+ * HTML and ships no JavaScript, which is what the docs bar already does and what
21
+ * the site header does NOT -- it is `client:only`, so it renders nothing at all
22
+ * until Vue boots. The dropdown below is CSS hover for the same reason.
23
+ */
24
+ export interface NavItem {
25
+ /** Link text, and the value `current` is matched against. */
26
+ name: string;
27
+ /** Omitted for an item that only opens a dropdown. */
28
+ href?: string;
29
+ /** Names a slot to render as a hover dropdown under this item. */
30
+ slot?: string;
31
+ }
32
+
33
+ withDefaults(defineProps<{
34
+ /** Items left of the wordmark. */
35
+ left?: NavItem[];
36
+ /** Items right of it. */
37
+ right?: NavItem[];
38
+ /** Resolved URL of the wordmark. A path, not an import: the consumer owns
39
+ * its own asset pipeline, and this package ships no brand marks. */
40
+ logo: string;
41
+ logoHref?: string;
42
+ logoAlt?: string;
43
+ /** `name` of the item to mark aria-current="page". */
44
+ current?: string;
45
+ }>(), {
46
+ left: () => [],
47
+ right: () => [],
48
+ logoHref: "/",
49
+ logoAlt: "CODECAVE",
50
+ });
51
+ </script>
52
+
53
+ <template>
54
+ <nav class="brand-nav" aria-label="Main">
55
+ <div class="brand-nav-inner">
56
+ <ul>
57
+ <li v-for="item in left" :key="item.name" :class="{ dropdown: item.slot }">
58
+ <a
59
+ v-if="item.href"
60
+ class="btn btn-ghost"
61
+ :href="item.href"
62
+ :aria-current="item.name === current ? &apos;page&apos; : undefined"
63
+ >{{ item.name }}</a>
64
+ <span v-else class="btn btn-ghost dropbtn">{{ item.name }}<slot name="chevron" /></span>
65
+ <div v-if="item.slot" class="dropdown-content">
66
+ <slot :name="item.slot" />
67
+ </div>
68
+ </li>
69
+ </ul>
70
+
71
+ <a class="brand-nav-logo" :href="logoHref" :aria-label="logoAlt">
72
+ <img :src="logo" :alt="logoAlt" />
73
+ </a>
74
+
75
+ <ul>
76
+ <li v-for="item in right" :key="item.name" :class="{ dropdown: item.slot }">
77
+ <a
78
+ v-if="item.href"
79
+ class="btn btn-ghost"
80
+ :href="item.href"
81
+ :aria-current="item.name === current ? &apos;page&apos; : undefined"
82
+ >{{ item.name }}</a>
83
+ <span v-else class="btn btn-ghost dropbtn">{{ item.name }}<slot name="chevron" /></span>
84
+ <div v-if="item.slot" class="dropdown-content">
85
+ <slot :name="item.slot" />
86
+ </div>
87
+ </li>
88
+ </ul>
89
+ </div>
90
+ </nav>
91
+ </template>
92
+
93
+ <style scoped>
94
+ /* Layout only. Every colour comes from colors_and_type.css, which ships beside
95
+ * this file -- .btn-ghost included, so the links are the same ghost buttons the
96
+ * rest of the system uses and a palette change moves the bar with it.
97
+ *
98
+ * The three values that used to differ between the site bar and the docs bar are
99
+ * settled here on the docs side: 16px links on the 48px --control-height grid,
100
+ * a 28px wordmark, and the 1px rule underneath. The site had shrunk all three
101
+ * with a text-sm wrapper, which quietly took its ghost buttons off the control
102
+ * grid every other control sits on. */
103
+ .brand-nav {
104
+ position: sticky;
105
+ top: 0;
106
+ z-index: 100;
107
+ border-bottom: 1px solid var(--color-surface-quaternary);
108
+ background: var(--color-surface-primary-transparent);
109
+ -webkit-backdrop-filter: blur(24px);
110
+ backdrop-filter: blur(24px);
111
+ }
112
+
113
+ .brand-nav-inner {
114
+ box-sizing: border-box;
115
+ position: relative;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: space-between;
119
+ max-width: var(--max-width-desktop);
120
+ margin: 0 auto;
121
+ padding: 0.75rem 1.25rem;
122
+ }
123
+
124
+ .brand-nav ul {
125
+ display: flex;
126
+ align-items: center;
127
+ margin: 0;
128
+ padding: 0;
129
+ list-style: none;
130
+ }
131
+
132
+ .brand-nav-logo {
133
+ position: absolute;
134
+ left: 50%;
135
+ transform: translateX(-50%);
136
+ display: flex;
137
+ transition: opacity var(--transition-base);
138
+ }
139
+ .brand-nav-logo:hover { opacity: 0.8; }
140
+ .brand-nav-logo img { height: 1.75rem; width: auto; }
141
+
142
+ /* The page you are on. The hovered violet, not a new colour and not a
143
+ background: the bar stays quiet. */
144
+ .brand-nav a[aria-current="page"] { color: var(--color-hovered); }
145
+
146
+ /* Hover-only, so the bar needs no JavaScript. A dropdown that opened on click
147
+ would, and that is the whole reason this renders without a client directive. */
148
+ .dropdown { position: relative; display: inline-block; }
149
+ .dropdown-content {
150
+ position: absolute;
151
+ top: 100%;
152
+ left: 0;
153
+ margin-top: 12px;
154
+ width: max-content;
155
+ opacity: 0;
156
+ visibility: hidden;
157
+ border-radius: 1rem;
158
+ padding: 1.25rem;
159
+ background: var(--color-surface-primary-transparent);
160
+ -webkit-backdrop-filter: blur(24px);
161
+ backdrop-filter: blur(24px);
162
+ transition: opacity 0.2s ease, visibility 0.2s;
163
+ }
164
+ /* Bridges the 12px gap so the pointer can travel into the panel. */
165
+ .dropdown-content::before {
166
+ content: "";
167
+ position: absolute;
168
+ top: -20px;
169
+ left: 0;
170
+ width: 100px;
171
+ height: 20px;
172
+ }
173
+ .dropdown:hover .dropdown-content { opacity: 1; visibility: visible; }
174
+ .dropdown:hover .dropbtn { color: var(--color-hovered); }
175
+
176
+ /* Under 768 the centred wordmark would sit on top of the links, so it takes its
177
+ own row. Same breakpoint and same wrap the docs bar already used. */
178
+ @media (max-width: 767px) {
179
+ .brand-nav-inner { flex-wrap: wrap; justify-content: center; }
180
+ .brand-nav ul { flex: 1; justify-content: center; }
181
+ .brand-nav-logo {
182
+ position: static;
183
+ transform: none;
184
+ order: -1;
185
+ flex-basis: 100%;
186
+ justify-content: center;
187
+ padding: 0.5rem 0 0.75rem;
188
+ }
189
+ }
190
+ </style>
package/dist/theme.css CHANGED
@@ -1,7 +1,7 @@
1
1
  /* ==========================================================================
2
2
  * CODECAVE Tailwind theme — @codecavepro/brand/theme.css
3
3
  * --------------------------------------------------------------------------
4
- * GENERATED from docs/source_examples/styles/global.css — do not edit.
4
+ * GENERATED from docs/theme.css — do not edit.
5
5
  *
6
6
  * Declares the names Tailwind needs in order to emit CODECAVE utility
7
7
  * classes: bg-surface-primary, text-heading-lg, rounded-card and the rest.
package/dist/tokens.css CHANGED
@@ -193,6 +193,20 @@
193
193
  --control-height: 3rem; /* 48px — every button and pill */
194
194
  --input-height: 4rem; /* 64px — floating-label text input */
195
195
 
196
+ /* The tick and dot transition, read from the scoped styles of Checkbox.vue
197
+ * and Radio.vue and never used as a utility class — so it belongs here and
198
+ * NOT in theme.css: a Tailwind @theme entry would emit a duration-control
199
+ * utility nothing asks for, and this has to exist unconditionally rather than
200
+ * because some unrelated utility happened to pull it in.
201
+ *
202
+ * It was the one name a shipped component reached for that the package did
203
+ * not declare. codecave.pro carried it privately in its own :root, so the
204
+ * site looked right while any other consumer got `transition: var(...)` with
205
+ * nothing behind it — invalid at computed-value time, so the tick appeared
206
+ * instantly instead of scaling in. Same 150ms the site has always used;
207
+ * moving it changes nothing anywhere and makes the package self-contained. */
208
+ --duration-control: 150ms;
209
+
196
210
  /* =========================================================================
197
211
  * ELEVATION — CODECAVE's light comes from BELOW.
198
212
  * Never substitute a conventional downward drop shadow for --shadow-section.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codecavepro/brand",
3
- "version": "1.6.0",
3
+ "version": "2.0.0",
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,13 +0,0 @@
1
- <svg width="71" height="20" viewBox="0 0 71 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <path d="M22.1002 19.9983H27.6803V15.0798C27.6803 15.0798 20.4048 5.28256 16.3243 0.000355625C13.9775 -0.000451478 10.7646 0.000367635 10.7646 0.000367635L10.7394 5.51094L22.1002 19.9983Z" fill="#5F20FE"/>
3
- <path d="M4.8979 19.9995H0V15.4323L6.72261 7.76791H11.6205V12.3351L4.8979 19.9995Z" fill="#5F20FE"/>
4
- <path d="M18.7376 17.4507L16.7006 20H10.8021L8.55237 17.2913L10.8021 14.7422H16.6578L18.7376 17.4507Z" fill="#5F20FE"/>
5
- <path fill-rule="evenodd" clip-rule="evenodd" d="M45.7984 19.9858C45.0048 19.9858 44.279 19.788 43.6209 19.3923C42.9628 18.9872 42.4354 18.4502 42.0386 17.7813C41.6515 17.103 41.4579 16.3587 41.4579 15.5485C41.4579 14.7383 41.6515 13.9987 42.0386 13.3298C42.4354 12.6515 42.9628 12.1145 43.6209 11.7188C44.279 11.3137 45.0048 11.1111 45.7984 11.1111C46.3694 11.1111 46.8968 11.21 47.3807 11.4079C47.8646 11.6057 48.2759 11.8836 48.6147 12.2416V11.3014H50.8067V19.9858L48.6147 20V18.8553C48.2759 19.2039 47.8646 19.4818 47.3807 19.6891C46.8968 19.8869 46.3694 19.9858 45.7984 19.9858ZM47.3227 17.6824C46.9549 17.9085 46.5388 18.0215 46.0742 18.0215C45.6194 18.0215 45.2032 17.9085 44.8258 17.6824C44.458 17.4563 44.1629 17.1595 43.9403 16.7921C43.7177 16.4152 43.6064 16.0007 43.6064 15.5485C43.6064 15.0963 43.7177 14.6864 43.9403 14.319C44.1629 13.9422 44.458 13.6407 44.8258 13.4146C45.2032 13.1885 45.6242 13.0754 46.0887 13.0754C46.5436 13.0754 46.9549 13.1885 47.3227 13.4146C47.7001 13.6407 48.0001 13.9422 48.2227 14.319C48.4453 14.6864 48.5566 15.0963 48.5566 15.5485C48.5566 16.0007 48.4453 16.4152 48.2227 16.7921C48.0001 17.1595 47.7001 17.4563 47.3227 17.6824Z" fill="white"/>
6
- <path d="M56.1725 20H57.4073L61.4643 11.3117L59.2206 11.3118C59.2206 11.3118 56.9641 16.2216 56.848 16.5076C56.7318 16.2216 54.5462 11.3117 54.5462 11.3117H52.1155L56.1725 20Z" fill="white"/>
7
- <path fill-rule="evenodd" clip-rule="evenodd" d="M66.9034 20C65.9481 20 65.0988 19.8043 64.3557 19.413C63.6223 19.0216 63.0481 18.4859 62.6332 17.8057C62.2279 17.1162 62.0252 16.3382 62.0252 15.4717C62.0252 14.6424 62.223 13.9017 62.6187 13.2495C63.024 12.5879 63.5692 12.0661 64.2544 11.6841C64.9396 11.3021 65.6971 11.1111 66.5271 11.1111C67.3763 11.1111 68.1339 11.3021 68.7997 11.6841C69.4752 12.0568 70.0108 12.5646 70.4065 13.2075C70.8022 13.8504 71 14.5819 71 15.4018V16.0892H65.6006H64.8232H64.1915C64.3073 16.6948 64.6163 17.1628 65.1085 17.5541C65.6006 17.9455 66.2183 18.1411 66.9613 18.1411C67.5114 18.1411 68.0084 18.0293 68.4523 17.8057C68.8962 17.5821 69.2629 17.3026 69.5525 16.9671L70.0736 19.0216C69.6393 19.3478 69.152 19.59 68.6115 19.7484C68.0711 19.9161 67.5018 20 66.9034 20ZM65.5772 14.703H64.1404C64.2948 14.1626 64.5825 13.7247 65.0071 13.3892C65.4318 13.0538 65.9336 12.8861 66.5126 12.8861C67.1013 12.8861 67.6031 13.0491 68.018 13.3752C68.4427 13.7014 68.7324 14.1626 68.8771 14.703H68.2254H67.4445H65.5772Z" fill="white"/>
8
- <path fill-rule="evenodd" clip-rule="evenodd" d="M66.9034 8.88888C65.9481 8.88888 65.0988 8.69321 64.3557 8.30188C63.6223 7.91054 63.0481 7.37479 62.6332 6.69461C62.2279 6.00512 62.0252 5.22711 62.0252 4.36058C62.0252 3.53133 62.223 2.79059 62.6187 2.13836C63.024 1.47682 63.5692 0.955042 64.2544 0.573025C64.9396 0.191008 65.6971 0 66.5271 0C67.3763 0 68.1339 0.191008 68.7997 0.573025C69.4752 0.945725 70.0108 1.45353 70.4065 2.09643C70.8022 2.73934 71 3.47076 71 4.2907V4.97808H65.6006H64.8232H64.1915C64.3073 5.58371 64.6163 6.05171 65.1085 6.44304C65.6006 6.83438 66.2183 7.03004 66.9613 7.03004C67.5114 7.03004 68.0084 6.91823 68.4523 6.69461C68.8962 6.47099 69.2629 6.19147 69.5525 5.85604L70.0736 7.91054C69.6393 8.23666 69.152 8.47891 68.6115 8.63731C68.0711 8.80502 67.5018 8.88888 66.9034 8.88888ZM65.5772 3.59189H64.1404C64.2948 3.05148 64.5825 2.61355 65.0071 2.27812C65.4318 1.9427 65.9336 1.77498 66.5126 1.77498C67.1013 1.77498 67.6031 1.93804 68.018 2.26415C68.4427 2.59026 68.7324 3.05148 68.8771 3.59189H68.2254H67.4445H65.5772Z" fill="white"/>
9
- <path fill-rule="evenodd" clip-rule="evenodd" d="M52.3025 8.88888V0H55.4891C56.2389 0 56.9193 0.11068 57.5302 0.332041C58.1481 0.553402 58.6792 0.860145 59.1235 1.25227C59.5748 1.63807 59.9219 2.09028 60.1649 2.6089C60.4079 3.12751 60.5294 3.91326 60.5294 4.50145C60.5294 5.08963 60.4079 5.64304 60.1649 6.16165C59.9219 6.68027 59.5748 7.23127 59.1235 7.6234C58.6792 8.0092 58.1481 8.33548 57.5302 8.55684C56.9193 8.7782 56.2389 8.88888 55.4891 8.88888H52.3025ZM56.778 7.01353C56.3753 7.15267 54.7743 7.12887 54.2466 7.12887V1.62167C54.7743 1.62167 56.3753 1.69047 56.778 1.82962C57.1807 1.96876 57.4573 2.08079 57.7281 2.33378C58.0058 2.58043 58.2141 2.87137 58.3529 3.20657C58.4987 3.53545 58.5682 4.06496 58.5682 4.44444C58.5682 4.82392 58.4987 5.2351 58.3529 5.56398C58.2141 5.89286 58.0058 6.18379 57.7281 6.43677C57.4573 6.68343 57.1807 6.86806 56.778 7.01353Z" fill="white"/>
10
- <path fill-rule="evenodd" clip-rule="evenodd" d="M46.0388 8.88888C45.1947 8.88888 44.4217 8.68855 43.7199 8.2879C43.0276 7.88725 42.4775 7.3515 42.0697 6.68064C41.6618 6.00978 41.4579 5.26438 41.4579 4.44444C41.4579 3.6245 41.6618 2.8791 42.0697 2.20824C42.4775 1.53738 43.0276 1.00163 43.7199 0.600978C44.4217 0.200326 45.19 0 46.0246 0C46.8782 0 47.6511 0.200326 48.3435 0.600978C49.0358 1.00163 49.5859 1.53738 49.9937 2.20824C50.411 2.86979 50.6197 3.61518 50.6197 4.44444C50.6197 5.25506 50.411 6.00046 49.9937 6.68064C49.5859 7.3515 49.0358 7.88725 48.3435 8.2879C47.6511 8.68855 46.8829 8.88888 46.0388 8.88888ZM47.2765 6.56883C46.9066 6.79245 46.4893 6.90426 46.0246 6.90426C45.5693 6.90426 45.1568 6.79245 44.7869 6.56883C44.417 6.34521 44.123 6.04705 43.9049 5.67435C43.6867 5.30165 43.5776 4.89168 43.5776 4.44444C43.5776 3.98788 43.6867 3.57791 43.9049 3.21453C44.123 2.84183 44.417 2.54367 44.7869 2.32005C45.1568 2.09643 45.5693 1.98462 46.0246 1.98462C46.4893 1.98462 46.9066 2.09643 47.2765 2.32005C47.6464 2.54367 47.9404 2.84183 48.1585 3.21453C48.3767 3.57791 48.4857 3.98788 48.4857 4.44444C48.4857 4.89168 48.3767 5.30165 48.1585 5.67435C47.9404 6.04705 47.6464 6.34521 47.2765 6.56883Z" fill="white"/>
11
- <path d="M36.7918 20C35.929 20 35.1389 19.7997 34.4214 19.399C33.7137 18.9984 33.1514 18.4626 32.7345 17.7917C32.3176 17.1116 32.1092 16.3662 32.1092 15.5555C32.1092 14.7356 32.3176 13.9902 32.7345 13.3193C33.1514 12.6485 33.7137 12.1127 34.4214 11.7121C35.1389 11.3114 35.929 11.1111 36.7918 11.1111C37.4026 11.1111 37.9795 11.2089 38.5224 11.4046C39.0653 11.6003 39.5452 11.8845 39.9621 12.2572L39.3513 14.5772C39.109 14.1766 38.7745 13.8318 38.3479 13.543C37.9213 13.2448 37.4269 13.0957 36.8646 13.0957C36.3798 13.0957 35.9435 13.2075 35.5557 13.4312C35.1776 13.6548 34.8771 13.9529 34.6541 14.3256C34.4311 14.689 34.3196 15.099 34.3196 15.5555C34.3196 16.0028 34.4311 16.4128 34.6541 16.7854C34.8771 17.1581 35.1776 17.4563 35.5557 17.6799C35.9435 17.9035 36.3798 18.0154 36.8646 18.0154C37.4269 18.0154 37.9213 17.8709 38.3479 17.5821C38.7745 17.2839 39.109 16.9345 39.3513 16.5339L39.9621 18.8539C39.5452 19.2266 39.0653 19.5108 38.5224 19.7065C37.9795 19.9021 37.4026 20 36.7918 20Z" fill="white"/>
12
- <path d="M36.7918 8.88888C35.929 8.88888 35.1389 8.68856 34.4214 8.2879C33.7137 7.88725 33.1514 7.3515 32.7345 6.68064C32.3176 6.00046 32.1092 5.25507 32.1092 4.44445C32.1092 3.62451 32.3176 2.8791 32.7345 2.20824C33.1514 1.53738 33.7137 1.00163 34.4214 0.600978C35.1389 0.200325 35.929 0 36.7918 0C37.4026 0 37.9795 0.0978308 38.5224 0.293498C39.0653 0.489166 39.5452 0.773352 39.9621 1.14605L39.3513 3.4661C39.109 3.06545 38.7745 2.72071 38.3479 2.43186C37.9213 2.1337 37.4269 1.98462 36.8646 1.98462C36.3798 1.98462 35.9435 2.09643 35.5557 2.32005C35.1776 2.54367 34.8771 2.84184 34.6541 3.21453C34.4311 3.57792 34.3196 3.98789 34.3196 4.44445C34.3196 4.89169 34.4311 5.30165 34.6541 5.67435C34.8771 6.04705 35.1776 6.34521 35.5557 6.56883C35.9435 6.79245 36.3798 6.90426 36.8646 6.90426C37.4269 6.90426 37.9213 6.75984 38.3479 6.471C38.7745 6.17284 39.109 5.82343 39.3513 5.42278L39.9621 7.74283C39.5452 8.11553 39.0653 8.39971 38.5224 8.59538C37.9795 8.79105 37.4026 8.88888 36.7918 8.88888Z" fill="white"/>
13
- </svg>
@@ -1,58 +0,0 @@
1
- <script setup lang="ts">
2
- import { formattedDate } from "../../helpers/date-formatter.ts";
3
- import { paths } from "../../helpers/paths.ts";
4
-
5
- /* The fields this component actually reads, instead of the generated Strapi
6
- * `Article`. TypeScript is structural, so a real Article still satisfies this
7
- * -- callers pass exactly what they passed before -- but the component no
8
- * longer carries the CMS schema with it. */
9
- type ArticleSummary = {
10
- slug: string | null
11
- title: string
12
- excerpt: string
13
- date: Date | null
14
- locale: string | null
15
- readingtime: unknown
16
- cover: { url: string; name: string }
17
- }
18
-
19
- const props = defineProps<{
20
- article: ArticleSummary
21
- className?: string
22
- /* CMS media URLs arrive relative ("uploads/x.png"). The site injects its own
23
- * resolver; a caller whose URLs are already absolute passes nothing. */
24
- resolveImage?: (url: string) => string
25
- }>()
26
-
27
- const imageUrl = (url: string) => props.resolveImage?.(url) ?? url
28
- </script>
29
-
30
- <template>
31
- <a :href="`${paths.insights}${article.slug}/`" :class="`mx-1 lg:mx-2 w-full h-full self-start sm:self-auto p-6 flex flex-col gap-5 sm:gap-8
32
- rounded-[2.25rem] bg-surface-secondary hover:bg-surface-secondary transition-colors cursor-pointer border-surface-tertiary border
33
- ${className ?? ''}`">
34
- <div class="flex flex-col sm:flex-row gap-5 sm:gap-8 h-fit">
35
- <img loading="lazy" class="sm:w-[132px] sm:h-[132px] rounded-xl object-cover"
36
- :src="imageUrl(article.cover.url)"
37
- :alt="article.cover.name"
38
- :width="100"
39
- :height="100"/>
40
- <div class="space-y-2 sm:space-y-3">
41
- <time class="text-body-secondary text-xs sm:text-sm" :datetime="article.date?.toString()">
42
- {{ formattedDate(article.locale, article.date) }}
43
- </time>
44
- <h3 class="font-bold text-lg md:text-xl text-heading">
45
- {{ article.title }}
46
- </h3>
47
- </div>
48
- </div>
49
- <div class="text-sm flex flex-col justify-between h-full">
50
- <p class="text-body-secondary-lighter">
51
- {{ article.excerpt }}
52
- </p>
53
- <p class="text-xs sm:text-sm text-body-secondary mt-6 sm:mt-7">
54
- Reading time: {{ article.readingtime }} m.
55
- </p>
56
- </div>
57
- </a>
58
- </template>
@@ -1,27 +0,0 @@
1
- <script setup lang="ts">
2
- import Button from "../common/Button.vue";
3
- import type { Link } from "./links.ts";
4
-
5
- defineProps<{
6
- groupName: string
7
- items: Link[]
8
- }>()
9
- </script>
10
-
11
- <template>
12
- <div class="space-y-4">
13
- <p class="text-body-secondary uppercase font-bold text-xs">
14
- {{ groupName }}
15
- </p>
16
- <div class="space-y-3 xl:space-y-2 text-sm">
17
- <div :key="index" v-for="(item, index) in items">
18
- <Button
19
- :title="item.name"
20
- as="link"
21
- :href="item.href"
22
- variant="text"
23
- />
24
- </div>
25
- </div>
26
- </div>
27
- </template>
@@ -1,52 +0,0 @@
1
- import { paths } from "../../helpers/paths.ts";
2
-
3
- export interface Link {
4
- name: string
5
- href: string
6
- }
7
-
8
- export const services: Link[] = [
9
- {name: 'AR & VR', href: paths.arVr},
10
- {name: 'Cloud services', href: paths.devops},
11
- {name: 'DevOps', href: paths.devops},
12
- {name: 'E-commerce', href: paths.ecommerce},
13
- {name: 'Web development', href: paths.ecommerce},
14
- {name: 'UX\\UI design', href: paths.ecommerce},
15
- {name: 'QA & testing', href: paths.ecommerce},
16
- {name: 'Automation & AI', href: paths.automation},
17
- {name: 'Autodesk plugins', href: paths.autodesk},
18
- {name: 'Legacy modernization', href: paths.ecommerce},
19
- {name: 'Data migration', href: paths.hubspot},
20
- {name: 'HubSpot', href: paths.hubspot},
21
- ]
22
-
23
- export const industries: Link[] = [
24
- {name: 'eCommerce', href: '/'},
25
- {name: 'Logistics', href: '/'},
26
- {name: 'Healthcare', href: '/'},
27
- {name: 'Manufacturing', href: '/'},
28
- {name: 'Education', href: '/'},
29
- {name: 'Finance', href: '/'},
30
- ]
31
-
32
- export const other: Link[] = [
33
- {name: 'Our projects', href: paths.projects},
34
- {name: 'Feedback', href: paths.testimonials},
35
- {name: 'How we work', href: paths.workflow},
36
- {name: 'Our strong points', href: paths.strongPoints},
37
- {name: 'Our tech stack', href: paths.toolsAndTechnologies},
38
- {name: 'All Insights', href: paths.insights},
39
- {name: 'F.A.Q.', href: '/#questions'},
40
- ]
41
-
42
- export const reviews: Link[] = [
43
- {name: 'Trustpilot', href: 'https://www.trustpilot.com/review/codecave.pro'},
44
- {name: 'Glassdoor', href: 'https://www.glassdoor.com/Reviews/Employee-Review-CodeCave-E8324784-RVW95188942.htm'},
45
- {name: 'Clutch', href: 'https://clutch.co/profile/codecave-0-1'},
46
- {name: 'G2', href: 'https://www.g2.com/products/codecave/expertises'},
47
- ]
48
-
49
- export const legal: Link[] = [
50
- {name: 'Cookie policy', href: paths.cookiePolicy},
51
- {name: 'Privacy policy', href: paths.privacyPolicy},
52
- ]
@@ -1,95 +0,0 @@
1
- <script setup lang="ts">
2
- import { menu } from "./menu.ts";
3
- import Button from "../common/Button.vue";
4
- import Shevron from "../../assets/icons/shevron.vue";
5
- import { paths } from "../../helpers/paths.ts";
6
- import Logo from "../../assets/images/logo.svg";
7
- import ServicesList from "./services-list.vue";
8
- </script>
9
-
10
- <template>
11
- <div class="page-container py-3">
12
- <nav class="flex justify-between items-center relative text-sm">
13
- <ul class="flex">
14
- <li v-for="(item, index) in menu.slice(0, 3)" :key="index">
15
- <div v-if="item.name === 'Services'" class="dropdown">
16
- <Button variant="ghost" class="cursor-pointer dropbtn" :title="item.name">
17
- <Shevron class="ml-1" />
18
- </Button>
19
- <div class="dropdown-content bg-surface-primary-transparent backdrop-blur-3xl rounded-2xl p-5">
20
- <ServicesList />
21
- </div>
22
- </div>
23
- <Button v-else as="link" variant="ghost" :title="item.name" :href="item.link" />
24
- </li>
25
- </ul>
26
- <a :href="paths.home" class="absolute left-1/2 -translate-x-1/2 hover:opacity-80 transition-opacity">
27
- <img :src="Logo.src" alt="CODECAVE" />
28
- </a>
29
- <ul class="flex">
30
- <li v-for="(item, index) in menu.slice(3, 5)" :key="index">
31
- <Button as="link" variant="ghost" :title="item.name" :href="item.link" />
32
- </li>
33
- </ul>
34
- </nav>
35
- </div>
36
- </template>
37
-
38
- <style scoped>
39
- ul:last-of-type li:last-child {
40
- position: relative;
41
- }
42
-
43
- ul:last-of-type li:last-child::after {
44
- position: absolute;
45
- content: '';
46
- width: 10px;
47
- height: 10px;
48
- right: 8px;
49
- top: 11px;
50
- background: red;
51
- border-radius: 50%;
52
- box-shadow: 0 0 12px 0 var(--color-error-400),
53
- 0 0 4px 0 hsl(from var(--color-error-400) h s l / 0.5);
54
- }
55
-
56
- .dropdown {
57
- position: relative;
58
- display: inline-block;
59
- }
60
-
61
- .dropdown-content {
62
- position: absolute;
63
- top: 100%;
64
- left: 0;
65
- margin-top: 12px;
66
- width: max-content;
67
-
68
- opacity: 0;
69
- visibility: hidden;
70
-
71
- transition:
72
- opacity 0.2s ease,
73
- transform 0.2s ease,
74
- visibility 0.2s;
75
- }
76
-
77
- .dropdown-content::before {
78
- content: '';
79
- position: absolute;
80
- top: -20px;
81
- left: 0;
82
- width: 100px;
83
- height: 20px;
84
- background-color: transparent;
85
- }
86
-
87
- .dropdown:hover .dropdown-content {
88
- opacity: 1;
89
- visibility: visible;
90
- }
91
-
92
- .dropdown:hover .dropbtn {
93
- color: var(--color-hovered);
94
- }
95
- </style>
@@ -1,41 +0,0 @@
1
- import { paths } from "../../helpers/paths.ts";
2
- import type { Component } from "vue";
3
- import CloudIcon from "../../assets/icons/cloud-icon.vue";
4
- import CartIcon from "../../assets/icons/cart-icon.vue";
5
- import WidgetIcon from "../../assets/icons/widget-icon.vue";
6
- import LightningIcon from "../../assets/icons/lightning-icon.vue";
7
- import DatabaseIcon from "../../assets/icons/database-icon.vue";
8
- import PanoramaIcon from "../../assets/icons/panorama-icon.vue";
9
-
10
- interface Submenu {
11
- icon: Component
12
- name: string
13
- description: string
14
- link: string
15
- }
16
-
17
- interface Menu {
18
- name: string
19
- link?: string
20
- submenuTitle?: string
21
- submenu?: Submenu[]
22
- }
23
-
24
- export const menu: Menu[] = [
25
- {
26
- name: 'Services',
27
- submenuTitle: 'Services',
28
- submenu: [
29
- {icon: CloudIcon, name: 'Cloud & DevOps', description: 'Optimize costs. Protect your Data', link: paths.devops},
30
- {icon: CartIcon, name: 'E-Commerce', description: 'Maximize revenue, dominate Markets', link: paths.ecommerce},
31
- {icon: WidgetIcon, name: 'Autodesk plugins', description: '10x efficiency with custom Plugins', link: paths.autodesk},
32
- {icon: LightningIcon, name: 'Automation & AI', description: 'Leverage virtual Workforce', link: paths.automation},
33
- {icon: DatabaseIcon, name: 'HubSpot', description: 'Aggregate data from all your Tools', link: paths.hubspot},
34
- {icon: PanoramaIcon, name: 'AR & VR', description: 'Stunning visualisations for your business', link: paths.arVr},
35
- ]
36
- },
37
- {name: 'Workflow', link: paths.workflow},
38
- {name: 'Projects', link: paths.projects},
39
- {name: 'Insights', link: paths.insights},
40
- {name: 'Contact us', link: paths.contactUs},
41
- ]
@@ -1,117 +0,0 @@
1
- <script setup lang="ts">
2
- import { menu } from "./menu.ts";
3
- import Button from "../common/Button.vue";
4
- import Shevron from "../../assets/icons/shevron.vue";
5
- import { onUnmounted, ref, watch } from "vue";
6
- import { paths } from "../../helpers/paths.ts";
7
- import Logo from "../../assets/images/logo.svg";
8
- import BackIcon from "../../assets/icons/back-icon.vue";
9
- import ServicesList from "./services-list.vue";
10
-
11
- const isMenuOpen = ref(false)
12
- const isServicesOpen = ref(false)
13
- let scrollPosition = 0
14
-
15
- const lockScroll = () => {
16
- scrollPosition = window.scrollY || window.pageYOffset
17
-
18
- document.body.style.overflow = 'hidden'
19
- document.body.style.position = 'fixed'
20
- document.body.style.top = `-${scrollPosition}px`
21
- document.body.style.width = '100%'
22
-
23
- const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
24
- if (scrollbarWidth > 0) {
25
- document.body.style.paddingRight = `${scrollbarWidth}px`
26
- }
27
- }
28
-
29
- const unlockScroll = () => {
30
- document.body.style.position = ''
31
- document.body.style.top = ''
32
- document.body.style.width = ''
33
- document.body.style.paddingRight = ''
34
- document.body.style.overflow = ''
35
- window.scrollTo(0, scrollPosition)
36
- }
37
-
38
- watch(isMenuOpen, (newVal) => {
39
- if (newVal) {
40
- lockScroll()
41
- } else {
42
- unlockScroll()
43
- }
44
- })
45
-
46
- onUnmounted(() => {
47
- if (isMenuOpen.value) {
48
- unlockScroll()
49
- }
50
- })
51
- const handleCloseMenu = () => {
52
- isMenuOpen.value = false
53
- }
54
- </script>
55
-
56
- <template>
57
- <div>
58
- <div :class="`fixed z-50 top-0 w-full bg-surface-primary-transparent backdrop-blur-3xl ${isMenuOpen ? 'rounded-b-3xl' : 'rounded-none'}`">
59
- <div :class="`h-full px-5 ${isMenuOpen ? 'pb-5' : 'pb-0'} flex flex-col gap-2 transition-all`">
60
- <div class="flex justify-between items-center py-1.5">
61
- <a :href="paths.home" class="px-1.5 hover:opacity-80 transition-opacity">
62
- <img :src="Logo.src" alt="CODECAVE" />
63
- </a>
64
- <button @click="isMenuOpen = !isMenuOpen" :class="`burger-menu ${isMenuOpen ? 'text-action' : 'text-heading'} transition-colors`">
65
- <span></span>
66
- </button>
67
- </div>
68
- <nav v-if="isMenuOpen" class="h-full text-sm">
69
- <div v-if="isServicesOpen">
70
- <h2 class="text-center font-bold text-body-primary py-2.5">
71
- {{ menu[0].submenuTitle }}
72
- </h2>
73
- <ServicesList />
74
- <button class="pt-4" @click="isServicesOpen = false">
75
- <component class="w-11 h-11 text-action" :is="BackIcon" />
76
- </button>
77
- </div>
78
- <ul v-else class="h-full flex flex-col items-center gap-3 justify-around px-4 pt-2">
79
- <li v-for="(item, index) in menu" :key="index">
80
- <div v-if="item.name === 'Services'">
81
- <Button :title="item.name" variant="ghost" @click="isServicesOpen = true" class="transition-transform duration-150 pr-1">
82
- <Shevron class="rotate-270 ml-1" />
83
- </Button>
84
- </div>
85
- <Button v-else-if="item.name === 'Workflow'" as="link" variant="ghost" title="About us | Workflow" :href="item.link" />
86
- <Button v-else-if="item.name === 'Contact us'" as="link" :href="item.link" title="Get a free consultation" variant="tertiary" @click="handleCloseMenu" />
87
- <Button v-else as="link" variant="ghost" :title="item.name" :href="item.link" />
88
- </li>
89
- </ul>
90
- </nav>
91
- </div>
92
- </div>
93
- <div v-if="isMenuOpen" class="fixed z-0 inset-0" @click="handleCloseMenu">
94
- </div>
95
- </div>
96
- </template>
97
-
98
- <style scoped>
99
- .burger-menu {
100
- display: flex;
101
- justify-content: space-around;
102
- flex-direction: column;
103
- padding: 0.625rem;
104
- width: 3rem;
105
- height: 3rem;
106
- }
107
-
108
- .burger-menu::before,
109
- .burger-menu::after,
110
- .burger-menu span {
111
- content: '';
112
- width: 100%;
113
- height: 0.1rem;
114
- background-color: currentColor;
115
- border-radius: 3rem;
116
- }
117
- </style>
@@ -1,26 +0,0 @@
1
- <script setup lang="ts">
2
- import {menu} from "./menu.ts";
3
- </script>
4
-
5
- <template>
6
- <ul class="grid xl:grid-cols-2 gap-3.5 xl:gap-x-8 xl:gap-y-4">
7
- <li v-for="item in menu[0].submenu" :key="item.name">
8
- <a :href="item.link" class="flex items-center gap-3 group">
9
- <div class="w-fit p-3 rounded-xl bg-surface-tertiary">
10
- <component
11
- class="w-5 h-5 text-default-transparent group-active:text-hovered group-hover:text-hovered transition-colors"
12
- :is="item.icon"
13
- />
14
- </div>
15
- <div class="text-sm">
16
- <h3 class="font-bold text-heading group-active:text-hovered group-hover:text-hovered transition-colors">
17
- {{ item.name }}
18
- </h3>
19
- <p class="text-body-secondary-lighter group-active:text-hovered group-hover:text-hovered transition-colors">
20
- {{ item.description }}
21
- </p>
22
- </div>
23
- </a>
24
- </li>
25
- </ul>
26
- </template>
@@ -1,122 +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 TechnologyCard from "./technology-card.vue";
9
- import { paths } from "../../helpers/paths.ts";
10
-
11
- /* See ArticlePreview.vue: the fields read here, not the generated Strapi
12
- * `Technology`, which a caller may still pass unchanged. This component
13
- * renders no images, so it needs no resolver. */
14
- type TechnologySummary = {
15
- name: string
16
- title1: string
17
- title2: string
18
- }
19
-
20
- const props = defineProps<{
21
- technologies: TechnologySummary[]
22
- hideSelected?: boolean
23
- }>()
24
-
25
- const activeIndex = ref(0)
26
- const defaultAutoplayValue = 12000
27
- const autoplay = ref(defaultAutoplayValue)
28
- const handleMouseOver = (index: number) => {
29
- activeIndex.value = index
30
- autoplay.value = 0
31
- }
32
- const selectedTechnology = computed(() => {
33
- return props.technologies[activeIndex.value]
34
- })
35
- const handleMouseLeave = () => {
36
- activeIndex.value = 0
37
- autoplay.value = defaultAutoplayValue
38
- }
39
- const config = computed<Partial<CarouselConfig>>(() => ({
40
- gap: 8,
41
- itemsToShow: 'auto',
42
- height: 220,
43
- autoplay: autoplay.value,
44
- snapAlign: 'center',
45
- pauseAutoplayOnHover: true,
46
- wrapAround: true,
47
- transition: 500,
48
- breakpoints: {
49
- [BREAKPOINTS.md]: {
50
- gap: 16,
51
- },
52
- [BREAKPOINTS.xl]: {
53
- enabled: false,
54
- },
55
- }
56
- }))
57
- </script>
58
-
59
- <template>
60
- <section :class="`page-container
61
- ${hideSelected ? 'pt-8' : 'flex flex-col pt-4 md:pt-14 pb-20 xl:pb-0 swipe-over'}`">
62
- <div v-if="!hideSelected" class="flex flex-col items-center text-center">
63
- <div class="space-y-3">
64
- <div class="flex justify-center gap-6 font-bold text-xl">
65
- <div class="flex flex-col xl:flex-row xl:gap-2">
66
- <span class="text-body-primary">15+</span>
67
- <span class="text-body-secondary-lighter">Years of experience</span>
68
- </div>
69
- <div class="flex flex-col xl:flex-row xl:gap-2">
70
- <span class="text-body-primary">4.8</span>
71
- <span class="text-body-secondary-lighter">Average rating</span>
72
- </div>
73
- </div>
74
- <TypingEffect
75
- :text1="selectedTechnology.title1"
76
- :text2="selectedTechnology.title2"
77
- />
78
- </div>
79
- </div>
80
- <div v-if="!hideSelected" class="self-center pt-10 pb-[4.5rem] md:pt-14 md:pb-16">
81
- <GlowButton title="Get a free consultation" :href="paths.contactUs" />
82
- </div>
83
- <div v-else class="pt-[7.5rem] pb-20 space-y-7 text-center">
84
- <h2 class="font-bold text-heading-md text-heading">
85
- Looking for something specific?
86
- </h2>
87
- <p class="text-xl mx-auto max-w-lg text-body-secondary-lighter">
88
- We offer a variety of services that might be a game-changer for your business.
89
- </p>
90
- </div>
91
- <div class="hidden xl:block relative h-80">
92
- <div
93
- v-for="(item, index) in technologies"
94
- :key="index"
95
- @mouseover="handleMouseOver(index)"
96
- @mouseleave="handleMouseLeave"
97
- >
98
- <TechnologyCard
99
- :name="item.name"
100
- :active="activeIndex === index"
101
- className="w-72 h-72"
102
- :index="index"
103
- />
104
- </div>
105
- </div>
106
- <Carousel
107
- v-model="activeIndex"
108
- v-bind="config"
109
- class="-mx-4 xl:mx-0 xl:hidden"
110
- >
111
- <Slide v-for="(item, index) in technologies" :key="index">
112
- <div class="flex items-center w-[190px] h-[220px]">
113
- <TechnologyCard
114
- :name="item.name"
115
- :active="activeIndex === index"
116
- :className="`w-full h-full ${activeIndex === index ? 'max-h-full' : 'max-h-[190px]'}`"
117
- />
118
- </div>
119
- </Slide>
120
- </Carousel>
121
- </section>
122
- </template>
@@ -1,95 +0,0 @@
1
- <script setup lang="ts">
2
- import { paths } from "../../helpers/paths.ts";
3
- import Button from "../common/Button.vue";
4
-
5
- defineProps<{
6
- active: boolean
7
- name: string
8
- className?: string
9
- index?: number
10
- }>()
11
-
12
- const rotate = [
13
- '-rotate-4 left-2 top-10',
14
- '-rotate-1 left-1/4 top-20',
15
- 'rotate-1 left-1/2 top-20',
16
- 'rotate-4 right-2 top-10',
17
- '-rotate-2 right-2/3 top-3/5',
18
- 'rotate-2 left-2/3 top-3/5',
19
- ]
20
-
21
- const translate = [
22
- 'xl:-translate-y-1/3',
23
- 'xl:-translate-y-1/3',
24
- 'xl:-translate-y-1/3',
25
- 'xl:-translate-y-1/3',
26
- 'xl:-translate-y-1/2',
27
- 'xl:-translate-y-1/2',
28
- ]
29
- const getTechnologyUrl = (name: string) => {
30
- switch (name) {
31
- case 'AR & VR':
32
- return paths.arVr
33
- case 'Autodesk plugins':
34
- return paths.autodesk
35
- case 'Automation & AI':
36
- return paths.automation
37
- case 'Cloud & DevOps':
38
- return paths.devops
39
- case 'E-Commerce':
40
- return paths.ecommerce
41
- case 'HubSpot':
42
- return paths.hubspot
43
- default:
44
- return ''
45
- }
46
- }
47
- </script>
48
-
49
- <template>
50
- <div :class="`rounded-3xl card-wrapper cursor-pointer select-none absolute transform transition-transform duration-500
51
- ${rotate[index]} ${active ? `${translate[index]}` : ''} ${className || ''}`">
52
- <div class="card flex flex-col items-center justify-around">
53
- <h3 class="max-w-[8rem] text-center text-xl font-bold text-heading text-balance">
54
- {{ name }}
55
- </h3>
56
- <Button as="link" :href="getTechnologyUrl(name)" title="Explore service" variant="tertiary" :class="`${active ? 'block' : 'hidden xl:block xl:opacity-0'}`" />
57
- </div>
58
- </div>
59
- </template>
60
-
61
- <style scoped>
62
- .card-wrapper {
63
- background:
64
- linear-gradient(hsl(from var(--color-technology-gradient-25) h s l / 0.1),
65
- hsl(from var(--color-technology-gradient-0) h s l / 0.1),
66
- hsl(from var(--color-technology-gradient-50) h s l / 0.1)) border-box;
67
- }
68
-
69
- .card {
70
- width: 100%;
71
- height: 100%;
72
- border-radius: inherit;
73
- position: relative;
74
- background: transparent;
75
- backdrop-filter: blur(14px);
76
- }
77
-
78
- .card::before {
79
- content: '';
80
- position: absolute;
81
- z-index: -1;
82
- inset: 0;
83
- border-radius: inherit;
84
- border: 1px solid transparent;
85
- outline: 1px solid transparent;
86
- -webkit-backface-visibility: hidden;
87
- transform: translate3d(0, 0, 0);
88
- background: linear-gradient(hsl(from var(--color-brand-500) h s l / 0.35),
89
- hsl(from var(--color-brand-400) h s l / 0.675),
90
- var(--color-brand-500)) border-box;
91
- mask: linear-gradient(black, black) border-box,
92
- linear-gradient(black, black) padding-box;
93
- mask-composite: subtract;
94
- }
95
- </style>
@@ -1,5 +0,0 @@
1
- export const BREAKPOINTS = {
2
- md: 768,
3
- lg: 1024,
4
- xl: 1280,
5
- }
@@ -1,10 +0,0 @@
1
- export const formattedDate = (locale: string | null, date: Date | null) => {
2
- if (!locale || !date) return '';
3
-
4
- const newDate = new Date(date)
5
- return new Intl.DateTimeFormat(locale, {
6
- year: 'numeric',
7
- month: 'long',
8
- day: 'numeric'
9
- }).format(newDate)
10
- }
@@ -1,64 +0,0 @@
1
- /* Every value here is a COMPLETE url, trailing slash included, because
2
- * build.format is Astro's default 'directory' — /workflow is emitted as
3
- * workflow/index.html and only /workflow/ is the address of that file.
4
- * Written without the slash, all 716 internal links on the site were a 308
5
- * to the same page with one added; nothing complained because Astro's
6
- * trailingSlash default is 'ignore'. astro.config.mjs now sets 'always', so
7
- * dev 404s a slash-less route instead of quietly serving it.
8
- *
9
- * Consequence for callers: a path is a PREFIX you append to, never a
10
- * segment you join with a slash. Write `${paths.insights}${slug}/`, not
11
- * `${paths.insights}/${slug}` — the latter now yields //. scripts/check-links.mjs
12
- * fails the build on either mistake. */
13
- const services = '/services'
14
-
15
- export const paths = {
16
- home: '/',
17
-
18
- devops: `${services}/devops/`,
19
- autodesk: `${services}/autodesk/`,
20
- hubspot: `${services}/hubspot/`,
21
- ecommerce: `${services}/ecommerce/`,
22
- automation: `${services}/automation/`,
23
- arVr: `${services}/ar-vr/`,
24
-
25
- workflow: '/workflow/',
26
- projects: '/projects/',
27
- insights: '/insights/',
28
- contactUs: '/#contact-us',
29
- cookiePolicy: '/cookie-policy/',
30
- privacyPolicy: '/privacy-policy/',
31
-
32
- testimonials: '/#testimonials',
33
- strongPoints: '/workflow/#our-strong-point',
34
- toolsAndTechnologies: '/workflow/#tools-and-technologies'
35
- }
36
-
37
- export const websiteUrl = 'https://www.codecave.it'
38
-
39
- /* Give an internal href the trailing slash the route table already carries.
40
- *
41
- * Links in CMS prose do not come from the table above — an editor writes
42
- * them, and the privacy policy links the cookie policy as a bare
43
- * https://www.codecave.it/cookie-policy, one 308 on a page nobody rebuilds.
44
- * Normalising at render time is the only fix that survives the next edit.
45
- *
46
- * Deliberately narrow: internal only, and only where the last segment has no
47
- * dot, so a linked .pdf keeps its own address. A #fragment or ?query is put
48
- * back after the slash, since /workflow#x and /workflow/#x are different
49
- * requests for the same reason. */
50
- export const internalHref = (href: string): string => {
51
- if (!href) return href
52
-
53
- const rest = href.startsWith(websiteUrl)
54
- ? href.slice(websiteUrl.length)
55
- : href.startsWith('/') && !href.startsWith('//')
56
- ? href
57
- : null
58
- if (rest === null) return href
59
-
60
- const [path, tail = ''] = [rest.split(/[#?]/)[0], rest.slice(rest.split(/[#?]/)[0].length)]
61
- if (!path || path.endsWith('/') || path.split('/').pop()!.includes('.')) return href
62
-
63
- return href.slice(0, href.length - rest.length) + path + '/' + tail
64
- }