@hyvor/design 2.1.6 → 2.1.8-beta.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/dist/cloud/HyvorBar/BarSupport.svelte +1 -1
- package/dist/marketing/Footer/Footer.svelte +396 -97
- package/dist/marketing/Footer/Footer.svelte.d.ts +13 -2
- package/dist/marketing/Footer/FooterLinkList.svelte +10 -3
- package/dist/marketing/Header/Header.svelte +66 -26
- package/dist/marketing/Header/Header.svelte.d.ts +3 -2
- package/dist/marketing/Header/HeaderNavLink.svelte +148 -0
- package/dist/marketing/Header/HeaderNavLink.svelte.d.ts +17 -0
- package/dist/marketing/index.d.ts +1 -0
- package/dist/marketing/index.js +1 -0
- package/dist/marketing/social.d.ts +1 -1
- package/dist/marketing/social.js +1 -1
- package/package.json +1 -1
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
import Container from '../Container/Container.svelte';
|
|
4
|
+
import IconEnvelope from '@hyvor/icons/IconEnvelope';
|
|
3
5
|
import IconCopy from '@hyvor/icons/IconCopy';
|
|
4
6
|
import IconDiscord from '@hyvor/icons/IconDiscord';
|
|
5
|
-
import IconEnvelope from '@hyvor/icons/IconEnvelope';
|
|
6
7
|
import IconGithub from '@hyvor/icons/IconGithub';
|
|
7
8
|
import IconLinkedin from '@hyvor/icons/IconLinkedin';
|
|
8
9
|
import IconTwitterX from '@hyvor/icons/IconTwitterX';
|
|
9
10
|
import IconYoutube from '@hyvor/icons/IconYoutube';
|
|
10
|
-
import
|
|
11
|
+
import IconBluesky from '@hyvor/icons/IconBluesky';
|
|
12
|
+
import IconLockFill from '@hyvor/icons/IconLockFill';
|
|
11
13
|
import IconButton from '../../components/IconButton/IconButton.svelte';
|
|
12
14
|
import Tooltip from '../../components/Tooltip/Tooltip.svelte';
|
|
13
15
|
import LanguageToggle from '../../components/Internationalization/LanguageToggle.svelte';
|
|
14
|
-
import IconBluesky from '@hyvor/icons/IconBluesky';
|
|
15
16
|
import { SOCIAL_LINKS, type Socials } from '../social.js';
|
|
16
17
|
import Affiliate from '../Affiliate/Affiliate.svelte';
|
|
17
18
|
import RecordVisit from './RecordVisit.svelte';
|
|
@@ -19,104 +20,207 @@
|
|
|
19
20
|
const year = new Date().getFullYear();
|
|
20
21
|
|
|
21
22
|
interface Props {
|
|
23
|
+
name?: string;
|
|
24
|
+
subname?: string;
|
|
25
|
+
product?: string;
|
|
26
|
+
instance?: string;
|
|
27
|
+
logo?: string;
|
|
28
|
+
background?: string;
|
|
29
|
+
backgroundDark?: string;
|
|
30
|
+
card?: boolean;
|
|
22
31
|
email?: string | null;
|
|
23
32
|
social?: Partial<Socials>;
|
|
24
|
-
|
|
33
|
+
languageToggle?: boolean;
|
|
34
|
+
gdpr?: boolean;
|
|
25
35
|
affiliate?: boolean;
|
|
26
36
|
recordVisit?: boolean;
|
|
37
|
+
center?: Snippet;
|
|
27
38
|
max?: boolean;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
let {
|
|
42
|
+
name = 'HYVOR',
|
|
43
|
+
subname,
|
|
44
|
+
product,
|
|
45
|
+
instance = 'https://hyvor.com',
|
|
46
|
+
logo,
|
|
47
|
+
background,
|
|
48
|
+
backgroundDark,
|
|
49
|
+
card = false,
|
|
31
50
|
email = null,
|
|
32
|
-
social =
|
|
33
|
-
|
|
51
|
+
social = {},
|
|
52
|
+
languageToggle = true,
|
|
53
|
+
gdpr = true,
|
|
34
54
|
affiliate = true,
|
|
35
55
|
recordVisit = true,
|
|
56
|
+
center,
|
|
36
57
|
max = false
|
|
37
58
|
}: Props = $props();
|
|
38
59
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
// same "explicit logo, else derive from product" pattern as Header — pass
|
|
61
|
+
// `product` (e.g. "blogs") for the mascot to follow that product's logo
|
|
62
|
+
// automatically, or `logo` to override it directly. Neither given means
|
|
63
|
+
// no mascot.
|
|
64
|
+
const mascotLogo = $derived(
|
|
65
|
+
logo || (product ? `${instance}/api/public/logo/${product}.svg` : undefined)
|
|
66
|
+
);
|
|
43
67
|
|
|
44
|
-
|
|
68
|
+
const socialLinks = $derived({ ...SOCIAL_LINKS, ...social });
|
|
45
69
|
|
|
46
|
-
|
|
47
|
-
|
|
70
|
+
const SOCIAL_PLATFORMS = [
|
|
71
|
+
{ key: 'x', icon: IconTwitterX, label: 'X (Twitter)' },
|
|
72
|
+
{ key: 'github', icon: IconGithub, label: 'GitHub' },
|
|
73
|
+
{ key: 'discord', icon: IconDiscord, label: 'Discord' },
|
|
74
|
+
{ key: 'linkedin', icon: IconLinkedin, label: 'LinkedIn' },
|
|
75
|
+
{ key: 'youtube', icon: IconYoutube, label: 'YouTube' },
|
|
76
|
+
{ key: 'bluesky', icon: IconBluesky, label: 'Bluesky' }
|
|
77
|
+
] as const satisfies { key: keyof Socials; icon: unknown; label: string }[];
|
|
78
|
+
|
|
79
|
+
let emailCopied = $state(false);
|
|
80
|
+
function handleCopyEmail() {
|
|
81
|
+
if (!email) return;
|
|
82
|
+
navigator.clipboard.writeText(email);
|
|
48
83
|
emailCopied = true;
|
|
49
|
-
setTimeout(() =>
|
|
50
|
-
|
|
51
|
-
|
|
84
|
+
setTimeout(() => (emailCopied = false), 1000);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// the floating mascot (only shown when a `logo` is given) tilts/fades in
|
|
88
|
+
// once it's scrolled into view, instead of just appearing on page load
|
|
89
|
+
let mascotInView = $state(false);
|
|
90
|
+
function onView(node: HTMLElement, callback: () => void) {
|
|
91
|
+
const observer = new IntersectionObserver(
|
|
92
|
+
(entries) => {
|
|
93
|
+
if (entries[0]?.isIntersecting) {
|
|
94
|
+
setTimeout(callback, 400);
|
|
95
|
+
observer.disconnect();
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{ threshold: 0.3 }
|
|
99
|
+
);
|
|
100
|
+
observer.observe(node);
|
|
101
|
+
return {
|
|
102
|
+
destroy() {
|
|
103
|
+
observer.disconnect();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
52
106
|
}
|
|
107
|
+
|
|
108
|
+
// a single 5-pointed star (outer/inner vertices alternating), centered on
|
|
109
|
+
// its own origin — reused via `transform="translate(...)"` per position
|
|
110
|
+
// below, rather than a plain dot, for the EU ring on the GDPR badge
|
|
111
|
+
function starPath(outerR: number, innerR: number) {
|
|
112
|
+
const points: string[] = [];
|
|
113
|
+
for (let i = 0; i < 10; i++) {
|
|
114
|
+
const angle = -Math.PI / 2 + (i * Math.PI) / 5;
|
|
115
|
+
const r = i % 2 === 0 ? outerR : innerR;
|
|
116
|
+
points.push(`${(r * Math.cos(angle)).toFixed(2)},${(r * Math.sin(angle)).toFixed(2)}`);
|
|
117
|
+
}
|
|
118
|
+
return `M${points.join('L')}Z`;
|
|
119
|
+
}
|
|
120
|
+
const gdprStarPath = starPath(1.7, 0.68);
|
|
121
|
+
|
|
122
|
+
// positions for the 12-star EU ring on the small GDPR badge
|
|
123
|
+
const gdprStars = Array.from({ length: 12 }, (_, i) => {
|
|
124
|
+
const angle = (i * 30 * Math.PI) / 180;
|
|
125
|
+
return {
|
|
126
|
+
x: 16 + 12 * Math.cos(angle),
|
|
127
|
+
y: 16 + 12 * Math.sin(angle)
|
|
128
|
+
};
|
|
129
|
+
});
|
|
53
130
|
</script>
|
|
54
131
|
|
|
55
|
-
<footer>
|
|
56
|
-
|
|
57
|
-
<div class="
|
|
58
|
-
<
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<Tooltip text={emailCopied ? 'Copied!' : 'Copy email'} position="top">
|
|
68
|
-
<IconButton
|
|
69
|
-
size="small"
|
|
70
|
-
variant="invisible"
|
|
71
|
-
on:click={handleCopy}
|
|
72
|
-
on:mouseleave={() => (emailCopied = false)}
|
|
73
|
-
>
|
|
74
|
-
<IconCopy size={12} />
|
|
75
|
-
</IconButton>
|
|
76
|
-
</Tooltip>
|
|
77
|
-
</div>
|
|
78
|
-
{/if}
|
|
132
|
+
<div class="footer-outer">
|
|
133
|
+
{#if mascotLogo}
|
|
134
|
+
<div class="mascot-wrap" use:onView={() => (mascotInView = true)}>
|
|
135
|
+
<img
|
|
136
|
+
src={mascotLogo}
|
|
137
|
+
alt="{name} Logo"
|
|
138
|
+
width="100"
|
|
139
|
+
height="100"
|
|
140
|
+
class:in-view={mascotInView}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
{/if}
|
|
79
144
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{#if social.blueksy}
|
|
88
|
-
<a href={social.blueksy} target="_blank" rel="nofollow"><IconBluesky /></a>
|
|
89
|
-
{/if}
|
|
90
|
-
{#if social.x}
|
|
91
|
-
<a href={social.x} target="_blank" rel="nofollow"><IconTwitterX /></a>
|
|
145
|
+
<footer class:card style:--footer-bg={background} style:--footer-bg-dark={backgroundDark}>
|
|
146
|
+
<Container {max}>
|
|
147
|
+
<div class="top-row">
|
|
148
|
+
<div class="brand">
|
|
149
|
+
<span>{name}</span>
|
|
150
|
+
{#if subname}
|
|
151
|
+
<div class="subname">{subname}</div>
|
|
92
152
|
{/if}
|
|
93
|
-
|
|
94
|
-
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div class="top-row-right">
|
|
156
|
+
{#if email}
|
|
157
|
+
<div class="email-wrap">
|
|
158
|
+
<a class="email" href="mailto:{email}">
|
|
159
|
+
<IconEnvelope size={14} />
|
|
160
|
+
{email}
|
|
161
|
+
</a>
|
|
162
|
+
<Tooltip text={emailCopied ? 'Copied!' : 'Copy email'} position="top">
|
|
163
|
+
<IconButton
|
|
164
|
+
size="small"
|
|
165
|
+
variant="invisible"
|
|
166
|
+
onclick={handleCopyEmail}
|
|
167
|
+
onmouseleave={() => (emailCopied = false)}
|
|
168
|
+
>
|
|
169
|
+
<IconCopy size={12} />
|
|
170
|
+
</IconButton>
|
|
171
|
+
</Tooltip>
|
|
172
|
+
</div>
|
|
95
173
|
{/if}
|
|
96
|
-
|
|
97
|
-
|
|
174
|
+
|
|
175
|
+
<div class="socials">
|
|
176
|
+
{#each SOCIAL_PLATFORMS as platform (platform.key)}
|
|
177
|
+
{@const href = socialLinks[platform.key]}
|
|
178
|
+
{#if href}
|
|
179
|
+
<a {href} target="_blank" rel="nofollow" aria-label={platform.label}>
|
|
180
|
+
<platform.icon size={16} />
|
|
181
|
+
</a>
|
|
182
|
+
{/if}
|
|
183
|
+
{/each}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{#if languageToggle}
|
|
187
|
+
<span class="language-toggle-wrap">
|
|
188
|
+
<LanguageToggle align="end" position="top" staticPage />
|
|
189
|
+
</span>
|
|
98
190
|
{/if}
|
|
99
191
|
</div>
|
|
100
192
|
</div>
|
|
101
193
|
|
|
102
|
-
<div class="footer-
|
|
103
|
-
|
|
104
|
-
<LanguageToggle align="end" position="top" staticPage />
|
|
105
|
-
</span>
|
|
194
|
+
<div class="footer-center">
|
|
195
|
+
{@render center?.()}
|
|
106
196
|
</div>
|
|
107
|
-
</div>
|
|
108
197
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
198
|
+
<div class="bottom-bar">
|
|
199
|
+
<div>HYVOR © {year}</div>
|
|
200
|
+
|
|
201
|
+
{#if gdpr}
|
|
202
|
+
<div class="bottom-center">
|
|
203
|
+
<a class="gdpr-chip" href="https://hyvor.com/compliance" target="_blank">
|
|
204
|
+
<span class="gdpr-chip-icon">
|
|
205
|
+
<svg class="ring" viewBox="0 0 32 32" aria-hidden="true">
|
|
206
|
+
<circle cx="16" cy="16" r="16" fill="#173a8a" />
|
|
207
|
+
{#each gdprStars as s}
|
|
208
|
+
<path d={gdprStarPath} fill="#ffcd3c" transform="translate({s.x}, {s.y})" />
|
|
209
|
+
{/each}
|
|
210
|
+
</svg>
|
|
211
|
+
<span class="lock"><IconLockFill size={10} /></span>
|
|
212
|
+
</span>
|
|
213
|
+
<span class="gdpr-chip-text">GDPR Compliant</span>
|
|
214
|
+
</a>
|
|
215
|
+
</div>
|
|
216
|
+
{/if}
|
|
112
217
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
218
|
+
<div class="bottom-right">
|
|
219
|
+
<div class="france">From France <span class="flag">🇫🇷</span></div>
|
|
220
|
+
</div>
|
|
116
221
|
</div>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
</Container>
|
|
222
|
+
</Container>
|
|
223
|
+
</footer>
|
|
120
224
|
|
|
121
225
|
{#if affiliate}
|
|
122
226
|
<Affiliate />
|
|
@@ -125,57 +229,252 @@
|
|
|
125
229
|
{#if recordVisit}
|
|
126
230
|
<RecordVisit />
|
|
127
231
|
{/if}
|
|
128
|
-
</
|
|
232
|
+
</div>
|
|
129
233
|
|
|
130
234
|
<style>
|
|
131
|
-
footer {
|
|
132
|
-
|
|
235
|
+
.footer-outer {
|
|
236
|
+
position: relative;
|
|
237
|
+
margin-top: 100px;
|
|
133
238
|
}
|
|
134
239
|
|
|
135
|
-
.
|
|
136
|
-
|
|
240
|
+
.mascot-wrap {
|
|
241
|
+
position: absolute;
|
|
242
|
+
z-index: 10;
|
|
137
243
|
display: flex;
|
|
138
|
-
|
|
244
|
+
justify-content: center;
|
|
245
|
+
pointer-events: none;
|
|
246
|
+
left: 0;
|
|
247
|
+
bottom: 100%;
|
|
248
|
+
transform: translate(-35%, 45%) rotate(20deg);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.mascot-wrap img {
|
|
252
|
+
opacity: 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.mascot-wrap img.in-view {
|
|
256
|
+
animation: mascot-tilt-in 0.8s ease forwards;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@keyframes mascot-tilt-in {
|
|
260
|
+
0% {
|
|
261
|
+
opacity: 0;
|
|
262
|
+
transform: rotate(-14deg) scale(0.9) translateY(16px);
|
|
263
|
+
}
|
|
264
|
+
55% {
|
|
265
|
+
opacity: 1;
|
|
266
|
+
transform: rotate(10deg) scale(1.06) translateY(-4px);
|
|
267
|
+
}
|
|
268
|
+
100% {
|
|
269
|
+
opacity: 1;
|
|
270
|
+
transform: rotate(0deg) scale(1) translateY(0);
|
|
271
|
+
}
|
|
139
272
|
}
|
|
140
|
-
|
|
273
|
+
|
|
274
|
+
@media (prefers-reduced-motion: reduce) {
|
|
275
|
+
.mascot-wrap img {
|
|
276
|
+
opacity: 1;
|
|
277
|
+
}
|
|
278
|
+
.mascot-wrap img.in-view {
|
|
279
|
+
animation: none;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
footer {
|
|
284
|
+
--footer-text: var(--text);
|
|
285
|
+
--footer-text-strong: var(--text);
|
|
286
|
+
--footer-muted: var(--text-light);
|
|
287
|
+
--footer-border: var(--border);
|
|
288
|
+
--footer-hover: var(--accent);
|
|
289
|
+
|
|
290
|
+
position: relative;
|
|
291
|
+
z-index: 1;
|
|
292
|
+
background: var(--footer-bg, transparent);
|
|
293
|
+
color: var(--footer-text);
|
|
141
294
|
padding-top: 50px;
|
|
142
295
|
}
|
|
143
|
-
|
|
144
|
-
|
|
296
|
+
|
|
297
|
+
/*
|
|
298
|
+
`backgroundDark` only when needed for dark mode - not complusory
|
|
299
|
+
*/
|
|
300
|
+
:global(:root.dark) footer {
|
|
301
|
+
background: var(--footer-bg-dark, var(--footer-bg, transparent));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
footer.card {
|
|
305
|
+
--footer-text: rgba(255, 255, 255, 0.75);
|
|
306
|
+
--footer-text-strong: #fff;
|
|
307
|
+
--footer-muted: rgba(255, 255, 255, 0.45);
|
|
308
|
+
--footer-border: rgba(255, 255, 255, 0.08);
|
|
309
|
+
--footer-hover: #fff;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.top-row {
|
|
313
|
+
display: flex;
|
|
314
|
+
align-items: center;
|
|
315
|
+
justify-content: space-between;
|
|
316
|
+
flex-wrap: wrap;
|
|
317
|
+
gap: 20px;
|
|
318
|
+
padding-bottom: 40px;
|
|
319
|
+
border-bottom: 1px solid var(--footer-border);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.brand {
|
|
323
|
+
font-size: 16px;
|
|
324
|
+
font-weight: 700;
|
|
325
|
+
color: var(--footer-text-strong);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.subname {
|
|
329
|
+
font-size: 13px;
|
|
330
|
+
font-weight: 400;
|
|
331
|
+
color: var(--footer-muted);
|
|
145
332
|
}
|
|
146
|
-
|
|
333
|
+
|
|
334
|
+
.top-row-right {
|
|
147
335
|
display: flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
gap: 24px;
|
|
338
|
+
flex-wrap: wrap;
|
|
148
339
|
}
|
|
340
|
+
|
|
149
341
|
.email-wrap {
|
|
150
342
|
display: inline-flex;
|
|
151
343
|
align-items: center;
|
|
152
|
-
gap:
|
|
153
|
-
margin-bottom: 15px;
|
|
344
|
+
gap: 2px;
|
|
154
345
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
346
|
+
|
|
347
|
+
.email {
|
|
348
|
+
display: inline-flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
gap: 6px;
|
|
351
|
+
font-size: 13px;
|
|
352
|
+
color: var(--footer-text);
|
|
160
353
|
}
|
|
161
|
-
|
|
162
|
-
|
|
354
|
+
|
|
355
|
+
.email:hover {
|
|
356
|
+
color: var(--footer-hover);
|
|
163
357
|
}
|
|
164
|
-
|
|
165
|
-
|
|
358
|
+
|
|
359
|
+
.socials {
|
|
360
|
+
display: flex;
|
|
361
|
+
align-items: center;
|
|
362
|
+
gap: 14px;
|
|
166
363
|
}
|
|
167
364
|
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
padding-bottom: 30px;
|
|
365
|
+
.socials a {
|
|
366
|
+
color: var(--footer-text);
|
|
171
367
|
display: flex;
|
|
172
368
|
}
|
|
173
|
-
|
|
174
|
-
|
|
369
|
+
|
|
370
|
+
.socials a:hover {
|
|
371
|
+
color: var(--footer-hover);
|
|
175
372
|
}
|
|
176
373
|
|
|
177
374
|
.language-toggle-wrap {
|
|
178
|
-
margin-left: 8px;
|
|
179
375
|
font-size: 18px;
|
|
180
376
|
}
|
|
377
|
+
|
|
378
|
+
.footer-center:not(:empty) {
|
|
379
|
+
padding: 48px 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* only if there is no center element - to prevent double border apperance */
|
|
383
|
+
.footer-center:not(:empty) + .bottom-bar {
|
|
384
|
+
border-top: 1px solid var(--footer-border);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.bottom-bar {
|
|
388
|
+
display: flex;
|
|
389
|
+
align-items: center;
|
|
390
|
+
justify-content: space-between;
|
|
391
|
+
flex-wrap: wrap;
|
|
392
|
+
gap: 10px;
|
|
393
|
+
padding: 24px 0 32px;
|
|
394
|
+
font-size: 13px;
|
|
395
|
+
color: var(--footer-muted);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.bottom-right {
|
|
399
|
+
display: flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
gap: 16px;
|
|
402
|
+
flex-wrap: wrap;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.france {
|
|
406
|
+
display: inline-flex;
|
|
407
|
+
align-items: center;
|
|
408
|
+
gap: 6px;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.gdpr-chip {
|
|
412
|
+
display: inline-flex;
|
|
413
|
+
align-items: center;
|
|
414
|
+
gap: 8px;
|
|
415
|
+
padding: 5px 12px 5px 5px;
|
|
416
|
+
border-radius: 100px;
|
|
417
|
+
background: #1f2f57;
|
|
418
|
+
color: #fff;
|
|
419
|
+
opacity: 0.9;
|
|
420
|
+
transition: opacity 0.15s ease;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.gdpr-chip:hover {
|
|
424
|
+
opacity: 1;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.gdpr-chip-icon {
|
|
428
|
+
position: relative;
|
|
429
|
+
width: 22px;
|
|
430
|
+
height: 22px;
|
|
431
|
+
flex-shrink: 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.gdpr-chip-icon .ring {
|
|
435
|
+
position: absolute;
|
|
436
|
+
inset: 0;
|
|
437
|
+
width: 100%;
|
|
438
|
+
height: 100%;
|
|
439
|
+
display: block;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.gdpr-chip-icon .lock {
|
|
443
|
+
position: absolute;
|
|
444
|
+
inset: 0;
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
justify-content: center;
|
|
448
|
+
color: #fff;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.gdpr-chip-text {
|
|
452
|
+
font-size: 11px;
|
|
453
|
+
font-weight: 800;
|
|
454
|
+
letter-spacing: 0.02em;
|
|
455
|
+
color: #fff;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* emoji glyphs are pre-colored — never let the surrounding muted text
|
|
459
|
+
color dim or filter them */
|
|
460
|
+
.flag {
|
|
461
|
+
color: initial;
|
|
462
|
+
filter: none;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
@media (max-width: 560px) {
|
|
466
|
+
.mascot-wrap {
|
|
467
|
+
/* keep it clear of the brand wordmark that sits right below it */
|
|
468
|
+
margin-bottom: -40px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
footer {
|
|
472
|
+
padding-top: 130px;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.top-row {
|
|
476
|
+
flex-direction: column;
|
|
477
|
+
align-items: flex-start;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
181
480
|
</style>
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
import { type Socials } from '../social.js';
|
|
2
3
|
interface Props {
|
|
4
|
+
name?: string;
|
|
5
|
+
subname?: string;
|
|
6
|
+
product?: string;
|
|
7
|
+
instance?: string;
|
|
8
|
+
logo?: string;
|
|
9
|
+
background?: string;
|
|
10
|
+
backgroundDark?: string;
|
|
11
|
+
card?: boolean;
|
|
3
12
|
email?: string | null;
|
|
4
13
|
social?: Partial<Socials>;
|
|
5
|
-
|
|
14
|
+
languageToggle?: boolean;
|
|
15
|
+
gdpr?: boolean;
|
|
6
16
|
affiliate?: boolean;
|
|
7
17
|
recordVisit?: boolean;
|
|
18
|
+
center?: Snippet;
|
|
8
19
|
max?: boolean;
|
|
9
20
|
}
|
|
10
|
-
declare const Footer: import("svelte").Component<Props, {}, "
|
|
21
|
+
declare const Footer: import("svelte").Component<Props, {}, "">;
|
|
11
22
|
type Footer = ReturnType<typeof Footer>;
|
|
12
23
|
export default Footer;
|
|
@@ -17,12 +17,16 @@
|
|
|
17
17
|
<style>
|
|
18
18
|
.link-list {
|
|
19
19
|
flex: 1;
|
|
20
|
+
min-width: 150px;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
.title {
|
|
23
|
-
font-
|
|
24
|
+
font-size: 12px;
|
|
25
|
+
font-weight: 700;
|
|
24
26
|
text-transform: uppercase;
|
|
25
|
-
|
|
27
|
+
letter-spacing: 0.05em;
|
|
28
|
+
color: var(--footer-muted, var(--text-light));
|
|
29
|
+
margin-bottom: 14px;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
.links {
|
|
@@ -32,9 +36,12 @@
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
.links :global(a) {
|
|
35
|
-
|
|
39
|
+
font-size: 14px;
|
|
40
|
+
color: var(--footer-text, var(--text-light));
|
|
41
|
+
margin-top: 10px;
|
|
36
42
|
}
|
|
37
43
|
.links :global(a:hover) {
|
|
44
|
+
color: var(--footer-text-strong, var(--text));
|
|
38
45
|
text-decoration: underline;
|
|
39
46
|
}
|
|
40
47
|
</style>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
2
4
|
import Container from './../Container/Container.svelte';
|
|
3
5
|
import DarkToggle from '../../components/Dark/DarkToggle.svelte';
|
|
4
6
|
import IconButton from '../../components/IconButton/IconButton.svelte';
|
|
5
7
|
import Dropdown from '../../components/Dropdown/Dropdown.svelte';
|
|
6
8
|
import IconList from '@hyvor/icons/IconList';
|
|
9
|
+
import IconX from '@hyvor/icons/IconX';
|
|
7
10
|
import HeaderNotification from './HeaderNotification.svelte';
|
|
8
11
|
|
|
9
12
|
interface Props {
|
|
@@ -14,8 +17,8 @@
|
|
|
14
17
|
href?: string;
|
|
15
18
|
subName?: undefined | string;
|
|
16
19
|
darkToggle?: boolean;
|
|
17
|
-
center?:
|
|
18
|
-
end?:
|
|
20
|
+
center?: Snippet;
|
|
21
|
+
end?: Snippet;
|
|
19
22
|
max?: boolean;
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -31,9 +34,27 @@
|
|
|
31
34
|
end,
|
|
32
35
|
max = false
|
|
33
36
|
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
let scrolled = $state(false);
|
|
39
|
+
let mobileOpen = $state(false);
|
|
40
|
+
|
|
41
|
+
onMount(() => {
|
|
42
|
+
const onScroll = () => (scrolled = window.scrollY > 8);
|
|
43
|
+
onScroll();
|
|
44
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
45
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// close the mobile menu once a link inside it is clicked
|
|
49
|
+
function closeOnLinkClick(e: MouseEvent) {
|
|
50
|
+
const target = e.target as HTMLElement;
|
|
51
|
+
if (target.tagName === 'A' || target.closest('a')) {
|
|
52
|
+
mobileOpen = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
34
55
|
</script>
|
|
35
56
|
|
|
36
|
-
<header>
|
|
57
|
+
<header class:scrolled>
|
|
37
58
|
<HeaderNotification {instance} {product} />
|
|
38
59
|
<Container as="nav" {max}>
|
|
39
60
|
<div class="nav-start">
|
|
@@ -41,8 +62,8 @@
|
|
|
41
62
|
<img
|
|
42
63
|
src={logo || `${instance}/api/public/logo/${product}.svg`}
|
|
43
64
|
alt="{name + (subName ? ' ' + subName : '')} Logo"
|
|
44
|
-
width="
|
|
45
|
-
height="
|
|
65
|
+
width="26"
|
|
66
|
+
height="26"
|
|
46
67
|
/>
|
|
47
68
|
<span class="brand-product">
|
|
48
69
|
<span class="brand">{name}</span>
|
|
@@ -70,20 +91,30 @@
|
|
|
70
91
|
</div>
|
|
71
92
|
|
|
72
93
|
<span class="mobile-nav-wrap">
|
|
73
|
-
<Dropdown align="end" width={300}>
|
|
94
|
+
<Dropdown bind:show={mobileOpen} align="end" width={300} contentPadding={8}>
|
|
74
95
|
{#snippet trigger()}
|
|
75
|
-
<IconButton variant="invisible">
|
|
76
|
-
|
|
96
|
+
<IconButton variant="invisible" aria-label="Menu">
|
|
97
|
+
{#if mobileOpen}
|
|
98
|
+
<IconX size={18} />
|
|
99
|
+
{:else}
|
|
100
|
+
<IconList size={18} />
|
|
101
|
+
{/if}
|
|
77
102
|
</IconButton>
|
|
78
103
|
{/snippet}
|
|
79
104
|
{#snippet content()}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
106
|
+
<div class="mobile-content" onclick={closeOnLinkClick}>
|
|
107
|
+
{#if center}
|
|
108
|
+
<div class="mobile-inner">
|
|
109
|
+
{@render center()}
|
|
110
|
+
</div>
|
|
111
|
+
{/if}
|
|
112
|
+
{#if end}
|
|
113
|
+
<div class="mobile-divider"></div>
|
|
114
|
+
<div class="mobile-inner end">
|
|
115
|
+
{@render end()}
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
87
118
|
</div>
|
|
88
119
|
{/snippet}
|
|
89
120
|
</Dropdown>
|
|
@@ -105,10 +136,15 @@
|
|
|
105
136
|
width: 100%;
|
|
106
137
|
z-index: 100;
|
|
107
138
|
background-color: var(--background, var(--accent-lightest));
|
|
108
|
-
border-bottom: 1px solid
|
|
139
|
+
border-bottom: 1px solid transparent;
|
|
109
140
|
height: var(--header-height);
|
|
110
141
|
display: flex;
|
|
111
142
|
flex-direction: column;
|
|
143
|
+
transition: border-color 0.2s ease;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
header.scrolled {
|
|
147
|
+
border-bottom-color: var(--border);
|
|
112
148
|
}
|
|
113
149
|
|
|
114
150
|
header :global(> nav) {
|
|
@@ -117,7 +153,9 @@
|
|
|
117
153
|
flex: 1;
|
|
118
154
|
}
|
|
119
155
|
.nav-brand {
|
|
120
|
-
display: inline-
|
|
156
|
+
display: inline-flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
gap: 8px;
|
|
121
159
|
line-height: inherit;
|
|
122
160
|
white-space: nowrap;
|
|
123
161
|
color: inherit;
|
|
@@ -125,11 +163,10 @@
|
|
|
125
163
|
font-weight: 600;
|
|
126
164
|
}
|
|
127
165
|
.nav-brand img {
|
|
128
|
-
|
|
166
|
+
display: block;
|
|
129
167
|
}
|
|
130
168
|
|
|
131
169
|
.brand-product {
|
|
132
|
-
vertical-align: middle;
|
|
133
170
|
display: inline-flex;
|
|
134
171
|
flex-direction: column;
|
|
135
172
|
justify-content: center;
|
|
@@ -148,7 +185,7 @@
|
|
|
148
185
|
flex: 1;
|
|
149
186
|
display: flex;
|
|
150
187
|
align-items: center;
|
|
151
|
-
gap:
|
|
188
|
+
gap: 2px;
|
|
152
189
|
justify-content: center;
|
|
153
190
|
}
|
|
154
191
|
|
|
@@ -192,18 +229,21 @@
|
|
|
192
229
|
.mobile-inner {
|
|
193
230
|
display: flex;
|
|
194
231
|
flex-direction: column;
|
|
195
|
-
|
|
196
|
-
.mobile-content {
|
|
197
|
-
gap: 10px;
|
|
232
|
+
gap: 2px;
|
|
198
233
|
}
|
|
199
234
|
|
|
200
235
|
.mobile-content :global(.button) {
|
|
201
236
|
display: flex;
|
|
202
237
|
}
|
|
203
238
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
239
|
+
.mobile-divider {
|
|
240
|
+
height: 1px;
|
|
241
|
+
background: var(--border);
|
|
242
|
+
margin: 6px 4px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/*
|
|
246
|
+
used to prevent the content from being hidden behind the header
|
|
207
247
|
*/
|
|
208
248
|
:global(html) {
|
|
209
249
|
scroll-padding-top: calc(var(--header-height) + 20px);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
1
2
|
interface Props {
|
|
2
3
|
product: string;
|
|
3
4
|
instance?: string;
|
|
@@ -6,8 +7,8 @@ interface Props {
|
|
|
6
7
|
href?: string;
|
|
7
8
|
subName?: undefined | string;
|
|
8
9
|
darkToggle?: boolean;
|
|
9
|
-
center?:
|
|
10
|
-
end?:
|
|
10
|
+
center?: Snippet;
|
|
11
|
+
end?: Snippet;
|
|
11
12
|
max?: boolean;
|
|
12
13
|
}
|
|
13
14
|
declare const Header: import("svelte").Component<Props, {}, "">;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
href?: string;
|
|
6
|
+
active?: boolean;
|
|
7
|
+
menu?: boolean;
|
|
8
|
+
target?: string;
|
|
9
|
+
rel?: string;
|
|
10
|
+
start?: Snippet;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
end?: Snippet;
|
|
13
|
+
description?: Snippet;
|
|
14
|
+
onclick?: (event: MouseEvent) => void;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
href,
|
|
20
|
+
active = false,
|
|
21
|
+
menu = false,
|
|
22
|
+
target,
|
|
23
|
+
rel,
|
|
24
|
+
start,
|
|
25
|
+
children,
|
|
26
|
+
end,
|
|
27
|
+
description,
|
|
28
|
+
onclick,
|
|
29
|
+
...rest
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<svelte:element
|
|
34
|
+
this={href ? 'a' : 'span'}
|
|
35
|
+
{href}
|
|
36
|
+
{target}
|
|
37
|
+
{rel}
|
|
38
|
+
{onclick}
|
|
39
|
+
role={href ? undefined : 'button'}
|
|
40
|
+
tabindex={href ? undefined : 0}
|
|
41
|
+
class="header-nav-link"
|
|
42
|
+
class:active
|
|
43
|
+
class:menu={menu || !!description}
|
|
44
|
+
class:rich={!!description}
|
|
45
|
+
{...rest}
|
|
46
|
+
>
|
|
47
|
+
{#if description}
|
|
48
|
+
{#if start}
|
|
49
|
+
<span class="icon-box">{@render start()}</span>
|
|
50
|
+
{/if}
|
|
51
|
+
<span class="text-stack">
|
|
52
|
+
<span class="title">{@render children?.()}</span>
|
|
53
|
+
<span class="desc">{@render description()}</span>
|
|
54
|
+
</span>
|
|
55
|
+
{:else}
|
|
56
|
+
{#if start}
|
|
57
|
+
<span class="start">{@render start()}</span>
|
|
58
|
+
{/if}
|
|
59
|
+
{@render children?.()}
|
|
60
|
+
{#if end}
|
|
61
|
+
<span class="end">{@render end()}</span>
|
|
62
|
+
{/if}
|
|
63
|
+
{/if}
|
|
64
|
+
</svelte:element>
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
.header-nav-link {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 6px;
|
|
71
|
+
padding: 6px 16px;
|
|
72
|
+
border-radius: 20px;
|
|
73
|
+
font-size: 13px;
|
|
74
|
+
font-weight: 500;
|
|
75
|
+
color: var(--text-light);
|
|
76
|
+
text-decoration: none;
|
|
77
|
+
white-space: nowrap;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
user-select: none;
|
|
80
|
+
transition:
|
|
81
|
+
0.15s background-color,
|
|
82
|
+
0.15s color;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.header-nav-link:hover {
|
|
86
|
+
background: var(--hover, var(--accent-lightest));
|
|
87
|
+
color: var(--text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.header-nav-link.active {
|
|
91
|
+
background: var(--accent-light);
|
|
92
|
+
color: var(--text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.header-nav-link.menu {
|
|
96
|
+
width: 100%;
|
|
97
|
+
padding: 8px 10px;
|
|
98
|
+
border-radius: 12px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.start,
|
|
102
|
+
.end {
|
|
103
|
+
display: inline-flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* icon + title + description layout, see `description` above */
|
|
108
|
+
.header-nav-link.rich {
|
|
109
|
+
align-items: flex-start;
|
|
110
|
+
gap: 10px;
|
|
111
|
+
white-space: normal;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.icon-box {
|
|
115
|
+
flex: none;
|
|
116
|
+
display: inline-flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
width: 30px;
|
|
120
|
+
height: 30px;
|
|
121
|
+
border-radius: 8px;
|
|
122
|
+
background: var(--accent-light);
|
|
123
|
+
color: var(--accent);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.header-nav-link.rich.active .icon-box {
|
|
127
|
+
background: var(--background);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.text-stack {
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
gap: 1px;
|
|
134
|
+
padding-top: 1px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.title {
|
|
138
|
+
font-size: 13px;
|
|
139
|
+
font-weight: 600;
|
|
140
|
+
color: var(--text);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.desc {
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
font-weight: 400;
|
|
146
|
+
color: var(--text-light);
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
href?: string;
|
|
4
|
+
active?: boolean;
|
|
5
|
+
menu?: boolean;
|
|
6
|
+
target?: string;
|
|
7
|
+
rel?: string;
|
|
8
|
+
start?: Snippet;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
end?: Snippet;
|
|
11
|
+
description?: Snippet;
|
|
12
|
+
onclick?: (event: MouseEvent) => void;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
declare const HeaderNavLink: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type HeaderNavLink = ReturnType<typeof HeaderNavLink>;
|
|
17
|
+
export default HeaderNavLink;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as Accordion } from './DetailsAccordion/DetailsAccordion.svelte';
|
|
2
2
|
export { default as Header } from './Header/Header.svelte';
|
|
3
|
+
export { default as HeaderNavLink } from './Header/HeaderNavLink.svelte';
|
|
3
4
|
export { default as Container } from './Container/Container.svelte';
|
|
4
5
|
export { default as Docs } from './Docs/Docs.svelte';
|
|
5
6
|
export { default as DocsImage } from './Docs/DocsImage.svelte';
|
package/dist/marketing/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as Accordion } from './DetailsAccordion/DetailsAccordion.svelte';
|
|
2
2
|
export { default as Header } from './Header/Header.svelte';
|
|
3
|
+
export { default as HeaderNavLink } from './Header/HeaderNavLink.svelte';
|
|
3
4
|
export { default as Container } from './Container/Container.svelte';
|
|
4
5
|
export { default as Docs } from './Docs/Docs.svelte';
|
|
5
6
|
export { default as DocsImage } from './Docs/DocsImage.svelte';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const SOCIAL_LINKS: {
|
|
2
2
|
readonly x: "https://x.com/HyvorHQ";
|
|
3
|
-
readonly
|
|
3
|
+
readonly bluesky: "https://bsky.app/profile/hyvor.com";
|
|
4
4
|
readonly discord: "https://discord.com/invite/2WRJxQB";
|
|
5
5
|
readonly github: "https://github.com/hyvor";
|
|
6
6
|
readonly youtube: "https://www.youtube.com/@HYVOR";
|
package/dist/marketing/social.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const SOCIAL_LINKS = {
|
|
2
2
|
x: 'https://x.com/HyvorHQ',
|
|
3
|
-
|
|
3
|
+
bluesky: 'https://bsky.app/profile/hyvor.com',
|
|
4
4
|
discord: 'https://discord.com/invite/2WRJxQB',
|
|
5
5
|
github: 'https://github.com/hyvor',
|
|
6
6
|
youtube: 'https://www.youtube.com/@HYVOR',
|