@hyvor/design 2.1.9 → 2.1.10
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/components/ColorPicker/ColorPicker.svelte +68 -9
- package/dist/components/ColorPicker/ColorPicker.svelte.d.ts +3 -0
- package/dist/index.css +4 -0
- package/dist/marketing/AllFeaturesAccordion/AllFeaturesAccordion.svelte +251 -0
- package/dist/marketing/AllFeaturesAccordion/AllFeaturesAccordion.svelte.d.ts +23 -0
- package/dist/marketing/AllFeaturesAccordion/AllFeaturesAccordionFeature.svelte +70 -0
- package/dist/marketing/AllFeaturesAccordion/AllFeaturesAccordionFeature.svelte.d.ts +10 -0
- package/dist/marketing/FAQ/FAQ.svelte +227 -0
- package/dist/marketing/FAQ/FAQ.svelte.d.ts +13 -0
- package/dist/marketing/FeatureSplit/FeatureSplit.svelte +325 -0
- package/dist/marketing/FeatureSplit/FeatureSplit.svelte.d.ts +24 -0
- package/dist/marketing/Footer/Footer.svelte +395 -98
- package/dist/marketing/Footer/Footer.svelte.d.ts +18 -2
- package/dist/marketing/Footer/FooterLinkList.svelte +10 -3
- package/dist/marketing/FullTrialSignup/FullTrialSignup.svelte +164 -0
- package/dist/marketing/FullTrialSignup/FullTrialSignup.svelte.d.ts +18 -0
- package/dist/marketing/Header/Header.svelte +72 -28
- package/dist/marketing/Header/Header.svelte.d.ts +5 -2
- package/dist/marketing/Header/HeaderLanguageToggle.svelte +119 -0
- package/dist/marketing/Header/HeaderLanguageToggle.svelte.d.ts +14 -0
- package/dist/marketing/Header/HeaderNavLink.svelte +149 -0
- package/dist/marketing/Header/HeaderNavLink.svelte.d.ts +18 -0
- package/dist/marketing/Header/language.d.ts +16 -0
- package/dist/marketing/Header/language.js +28 -0
- package/dist/marketing/Hero/Hero.svelte +349 -0
- package/dist/marketing/Hero/Hero.svelte.d.ts +16 -0
- package/dist/marketing/LogoStrip/LogoStrip.svelte +199 -0
- package/dist/marketing/LogoStrip/LogoStrip.svelte.d.ts +19 -0
- package/dist/marketing/Seal/CcpaSeal.svelte +29 -0
- package/dist/marketing/Seal/CcpaSeal.svelte.d.ts +6 -0
- package/dist/marketing/Seal/GdprSeal.svelte +60 -0
- package/dist/marketing/Seal/GdprSeal.svelte.d.ts +6 -0
- package/dist/marketing/Seal/IsoSeal.svelte +44 -0
- package/dist/marketing/Seal/IsoSeal.svelte.d.ts +6 -0
- package/dist/marketing/Seal/Seal.svelte +62 -0
- package/dist/marketing/Seal/Seal.svelte.d.ts +8 -0
- package/dist/marketing/Seal/SsoSeal.svelte +31 -0
- package/dist/marketing/Seal/SsoSeal.svelte.d.ts +6 -0
- package/dist/marketing/Seal/ccpa.svg +131 -0
- package/dist/marketing/SpotlightSplit/SpotlightSplit.svelte +256 -0
- package/dist/marketing/SpotlightSplit/SpotlightSplit.svelte.d.ts +19 -0
- package/dist/marketing/Testimonials/Testimonials.svelte +441 -0
- package/dist/marketing/Testimonials/Testimonials.svelte.d.ts +24 -0
- package/dist/marketing/index.d.ts +20 -2
- package/dist/marketing/index.js +26 -2
- package/dist/marketing/social.d.ts +1 -1
- package/dist/marketing/social.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../../components/Button/Button.svelte';
|
|
3
|
+
import IconCheckCircleFill from '@hyvor/icons/IconCheckCircleFill';
|
|
4
|
+
|
|
5
|
+
type ButtonConfig = { href: string; label: string; external?: boolean };
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
badge?: string | null;
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
button?: ButtonConfig | ButtonConfig[] | null;
|
|
12
|
+
checks?: string[];
|
|
13
|
+
accentColor?: string;
|
|
14
|
+
accentColorDark?: string;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
badge = null,
|
|
20
|
+
title = 'Start your trial today',
|
|
21
|
+
description = '',
|
|
22
|
+
button = null,
|
|
23
|
+
checks = ['14-day free trial', 'No credit card required', 'Cancel anytime'],
|
|
24
|
+
accentColor,
|
|
25
|
+
accentColorDark,
|
|
26
|
+
...rest
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const buttons = $derived(button ? (Array.isArray(button) ? button : [button]) : []);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<section
|
|
33
|
+
class="hds-full-trial-signup"
|
|
34
|
+
style:--fts-accent={accentColor}
|
|
35
|
+
style:--fts-accent-dark={accentColorDark}
|
|
36
|
+
{...rest}
|
|
37
|
+
>
|
|
38
|
+
<div class="hds-container inner">
|
|
39
|
+
{#if badge}
|
|
40
|
+
<div class="badge">{badge}</div>
|
|
41
|
+
{/if}
|
|
42
|
+
|
|
43
|
+
<h2>{title}</h2>
|
|
44
|
+
|
|
45
|
+
{#if description}
|
|
46
|
+
<p>{description}</p>
|
|
47
|
+
{/if}
|
|
48
|
+
|
|
49
|
+
{#if buttons.length}
|
|
50
|
+
<div class="cta-actions">
|
|
51
|
+
{#each buttons as b}
|
|
52
|
+
<Button
|
|
53
|
+
as="a"
|
|
54
|
+
size="large"
|
|
55
|
+
href={b.href}
|
|
56
|
+
target={b.external ? '_blank' : undefined}
|
|
57
|
+
rel={b.external ? 'noopener' : undefined}
|
|
58
|
+
>
|
|
59
|
+
{b.label}
|
|
60
|
+
</Button>
|
|
61
|
+
{/each}
|
|
62
|
+
</div>
|
|
63
|
+
{/if}
|
|
64
|
+
|
|
65
|
+
{#if checks.length}
|
|
66
|
+
<div class="checks">
|
|
67
|
+
{#each checks as check}
|
|
68
|
+
<div class="check">
|
|
69
|
+
<IconCheckCircleFill size={15} />
|
|
70
|
+
{check}
|
|
71
|
+
</div>
|
|
72
|
+
{/each}
|
|
73
|
+
</div>
|
|
74
|
+
{/if}
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.hds-full-trial-signup {
|
|
80
|
+
--fts-accent-resolved: var(--fts-accent, var(--accent));
|
|
81
|
+
padding: 96px 0 138px;
|
|
82
|
+
background: linear-gradient(
|
|
83
|
+
to bottom,
|
|
84
|
+
var(--background, var(--box-background)),
|
|
85
|
+
color-mix(in srgb, var(--fts-accent-resolved) 40%, var(--background, var(--box-background)))
|
|
86
|
+
);
|
|
87
|
+
text-align: center;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:global(:root.dark) .hds-full-trial-signup {
|
|
91
|
+
--fts-accent-resolved: var(--fts-accent-dark, var(--fts-accent, var(--accent)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.inner {
|
|
95
|
+
width: 600px;
|
|
96
|
+
max-width: 100%;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.badge {
|
|
100
|
+
display: inline-flex;
|
|
101
|
+
padding: 4px 14px;
|
|
102
|
+
border-radius: 100px;
|
|
103
|
+
font-size: 13px;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
background: color-mix(in srgb, var(--fts-accent-resolved) 12%, transparent);
|
|
106
|
+
color: var(--fts-accent-resolved);
|
|
107
|
+
border: 1px solid color-mix(in srgb, var(--fts-accent-resolved) 25%, transparent);
|
|
108
|
+
margin-bottom: 24px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
h2 {
|
|
112
|
+
font-size: clamp(28px, 4vw, 40px);
|
|
113
|
+
font-weight: 800;
|
|
114
|
+
margin: 0 0 12px;
|
|
115
|
+
letter-spacing: -0.02em;
|
|
116
|
+
font-family: var(--font-serif);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
p {
|
|
120
|
+
font-size: 1rem;
|
|
121
|
+
color: var(--text-light);
|
|
122
|
+
margin: 0 0 32px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.cta-actions {
|
|
126
|
+
display: flex;
|
|
127
|
+
justify-content: center;
|
|
128
|
+
flex-wrap: wrap;
|
|
129
|
+
gap: 12px;
|
|
130
|
+
margin: 0 0 24px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.checks {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
gap: 20px;
|
|
138
|
+
flex-wrap: wrap;
|
|
139
|
+
margin-top: 24px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.check {
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 6px;
|
|
146
|
+
font-size: 14px;
|
|
147
|
+
color: var(--text-light);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.check :global(svg) {
|
|
151
|
+
color: var(--fts-accent-resolved);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@media (max-width: 600px) {
|
|
155
|
+
.checks {
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
align-items: flex-start;
|
|
158
|
+
width: fit-content;
|
|
159
|
+
max-width: 100%;
|
|
160
|
+
margin: 24px auto 0;
|
|
161
|
+
gap: 10px;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type ButtonConfig = {
|
|
2
|
+
href: string;
|
|
3
|
+
label: string;
|
|
4
|
+
external?: boolean;
|
|
5
|
+
};
|
|
6
|
+
interface Props {
|
|
7
|
+
badge?: string | null;
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
button?: ButtonConfig | ButtonConfig[] | null;
|
|
11
|
+
checks?: string[];
|
|
12
|
+
accentColor?: string;
|
|
13
|
+
accentColorDark?: string;
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
}
|
|
16
|
+
declare const FullTrialSignup: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type FullTrialSignup = ReturnType<typeof FullTrialSignup>;
|
|
18
|
+
export default FullTrialSignup;
|
|
@@ -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,9 +17,11 @@
|
|
|
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;
|
|
23
|
+
logoAltText?: string;
|
|
24
|
+
menuLabel?: string;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
let {
|
|
@@ -29,20 +34,40 @@
|
|
|
29
34
|
darkToggle = true,
|
|
30
35
|
center,
|
|
31
36
|
end,
|
|
32
|
-
max = false
|
|
37
|
+
max = false,
|
|
38
|
+
logoAltText = 'Logo',
|
|
39
|
+
menuLabel = 'Menu'
|
|
33
40
|
}: Props = $props();
|
|
41
|
+
|
|
42
|
+
let scrolled = $state(false);
|
|
43
|
+
let mobileOpen = $state(false);
|
|
44
|
+
|
|
45
|
+
onMount(() => {
|
|
46
|
+
const onScroll = () => (scrolled = window.scrollY > 8);
|
|
47
|
+
onScroll();
|
|
48
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
49
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// close the mobile menu once a link inside it is clicked
|
|
53
|
+
function closeOnLinkClick(e: MouseEvent) {
|
|
54
|
+
const target = e.target as HTMLElement;
|
|
55
|
+
if (target.tagName === 'A' || target.closest('a')) {
|
|
56
|
+
mobileOpen = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
34
59
|
</script>
|
|
35
60
|
|
|
36
|
-
<header>
|
|
61
|
+
<header class:scrolled>
|
|
37
62
|
<HeaderNotification {instance} {product} />
|
|
38
63
|
<Container as="nav" {max}>
|
|
39
64
|
<div class="nav-start">
|
|
40
65
|
<a class="nav-brand" {href}>
|
|
41
66
|
<img
|
|
42
67
|
src={logo || `${instance}/api/public/logo/${product}.svg`}
|
|
43
|
-
alt="{name + (subName ? ' ' + subName : '')}
|
|
44
|
-
width="
|
|
45
|
-
height="
|
|
68
|
+
alt="{name + (subName ? ' ' + subName : '')} {logoAltText}"
|
|
69
|
+
width="26"
|
|
70
|
+
height="26"
|
|
46
71
|
/>
|
|
47
72
|
<span class="brand-product">
|
|
48
73
|
<span class="brand">{name}</span>
|
|
@@ -70,20 +95,30 @@
|
|
|
70
95
|
</div>
|
|
71
96
|
|
|
72
97
|
<span class="mobile-nav-wrap">
|
|
73
|
-
<Dropdown align="end" width={300}>
|
|
98
|
+
<Dropdown bind:show={mobileOpen} align="end" width={300} contentPadding={8}>
|
|
74
99
|
{#snippet trigger()}
|
|
75
|
-
<IconButton variant="invisible">
|
|
76
|
-
|
|
100
|
+
<IconButton variant="invisible" aria-label={menuLabel} aria-expanded={mobileOpen}>
|
|
101
|
+
{#if mobileOpen}
|
|
102
|
+
<IconX size={18} />
|
|
103
|
+
{:else}
|
|
104
|
+
<IconList size={18} />
|
|
105
|
+
{/if}
|
|
77
106
|
</IconButton>
|
|
78
107
|
{/snippet}
|
|
79
108
|
{#snippet content()}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
110
|
+
<div class="mobile-content" onclick={closeOnLinkClick}>
|
|
111
|
+
{#if center}
|
|
112
|
+
<div class="mobile-inner">
|
|
113
|
+
{@render center()}
|
|
114
|
+
</div>
|
|
115
|
+
{/if}
|
|
116
|
+
{#if end}
|
|
117
|
+
<div class="mobile-divider"></div>
|
|
118
|
+
<div class="mobile-inner end">
|
|
119
|
+
{@render end()}
|
|
120
|
+
</div>
|
|
121
|
+
{/if}
|
|
87
122
|
</div>
|
|
88
123
|
{/snippet}
|
|
89
124
|
</Dropdown>
|
|
@@ -105,10 +140,15 @@
|
|
|
105
140
|
width: 100%;
|
|
106
141
|
z-index: 100;
|
|
107
142
|
background-color: var(--background, var(--accent-lightest));
|
|
108
|
-
border-bottom: 1px solid
|
|
143
|
+
border-bottom: 1px solid transparent;
|
|
109
144
|
height: var(--header-height);
|
|
110
145
|
display: flex;
|
|
111
146
|
flex-direction: column;
|
|
147
|
+
transition: border-color 0.2s ease;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
header.scrolled {
|
|
151
|
+
border-bottom-color: var(--border);
|
|
112
152
|
}
|
|
113
153
|
|
|
114
154
|
header :global(> nav) {
|
|
@@ -117,7 +157,9 @@
|
|
|
117
157
|
flex: 1;
|
|
118
158
|
}
|
|
119
159
|
.nav-brand {
|
|
120
|
-
display: inline-
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 8px;
|
|
121
163
|
line-height: inherit;
|
|
122
164
|
white-space: nowrap;
|
|
123
165
|
color: inherit;
|
|
@@ -125,11 +167,10 @@
|
|
|
125
167
|
font-weight: 600;
|
|
126
168
|
}
|
|
127
169
|
.nav-brand img {
|
|
128
|
-
|
|
170
|
+
display: block;
|
|
129
171
|
}
|
|
130
172
|
|
|
131
173
|
.brand-product {
|
|
132
|
-
vertical-align: middle;
|
|
133
174
|
display: inline-flex;
|
|
134
175
|
flex-direction: column;
|
|
135
176
|
justify-content: center;
|
|
@@ -148,7 +189,7 @@
|
|
|
148
189
|
flex: 1;
|
|
149
190
|
display: flex;
|
|
150
191
|
align-items: center;
|
|
151
|
-
gap:
|
|
192
|
+
gap: 2px;
|
|
152
193
|
justify-content: center;
|
|
153
194
|
}
|
|
154
195
|
|
|
@@ -192,18 +233,21 @@
|
|
|
192
233
|
.mobile-inner {
|
|
193
234
|
display: flex;
|
|
194
235
|
flex-direction: column;
|
|
195
|
-
|
|
196
|
-
.mobile-content {
|
|
197
|
-
gap: 10px;
|
|
236
|
+
gap: 2px;
|
|
198
237
|
}
|
|
199
238
|
|
|
200
239
|
.mobile-content :global(.button) {
|
|
201
240
|
display: flex;
|
|
202
241
|
}
|
|
203
242
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
243
|
+
.mobile-divider {
|
|
244
|
+
height: 1px;
|
|
245
|
+
background: var(--border);
|
|
246
|
+
margin: 6px 4px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/*
|
|
250
|
+
used to prevent the content from being hidden behind the header
|
|
207
251
|
*/
|
|
208
252
|
:global(html) {
|
|
209
253
|
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,9 +7,11 @@ 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;
|
|
13
|
+
logoAltText?: string;
|
|
14
|
+
menuLabel?: string;
|
|
12
15
|
}
|
|
13
16
|
declare const Header: import("svelte").Component<Props, {}, "">;
|
|
14
17
|
type Header = ReturnType<typeof Header>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Dropdown from '../../components/Dropdown/Dropdown.svelte';
|
|
3
|
+
import HeaderNavLink from './HeaderNavLink.svelte';
|
|
4
|
+
import type {
|
|
5
|
+
DropdownAlign,
|
|
6
|
+
DropdownPosition
|
|
7
|
+
} from '../../components/Dropdown/dropdown.types.js';
|
|
8
|
+
import type { LanguageOption } from './language.js';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
languages: LanguageOption[];
|
|
12
|
+
current: string;
|
|
13
|
+
href: (code: string) => string;
|
|
14
|
+
showName?: boolean;
|
|
15
|
+
align?: DropdownAlign;
|
|
16
|
+
position?: DropdownPosition;
|
|
17
|
+
label?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
languages,
|
|
22
|
+
current,
|
|
23
|
+
href,
|
|
24
|
+
showName = false,
|
|
25
|
+
align = 'center',
|
|
26
|
+
position = 'bottom',
|
|
27
|
+
label = 'Change language'
|
|
28
|
+
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
const currentLanguage = $derived(languages.find((l) => l.code === current) ?? languages[0]);
|
|
31
|
+
|
|
32
|
+
let show = $state(false);
|
|
33
|
+
|
|
34
|
+
// close the dropdown once a language link inside it is clicked
|
|
35
|
+
function closeOnLinkClick(e: MouseEvent) {
|
|
36
|
+
const target = e.target as HTMLElement;
|
|
37
|
+
if (target.tagName === 'A' || target.closest('a')) {
|
|
38
|
+
show = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
{#if currentLanguage}
|
|
44
|
+
<div class="header-language-toggle">
|
|
45
|
+
<Dropdown bind:show {align} {position} contentPadding={8}>
|
|
46
|
+
{#snippet trigger()}
|
|
47
|
+
<HeaderNavLink aria-label={label} aria-expanded={show}>
|
|
48
|
+
<span class="flag">{currentLanguage.flag}</span>
|
|
49
|
+
{#if showName}{currentLanguage.name}{/if}
|
|
50
|
+
</HeaderNavLink>
|
|
51
|
+
{/snippet}
|
|
52
|
+
{#snippet content()}
|
|
53
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
54
|
+
<div class="menu" onclick={closeOnLinkClick}>
|
|
55
|
+
{#each languages as language (language.code)}
|
|
56
|
+
<HeaderNavLink
|
|
57
|
+
href={href(language.code)}
|
|
58
|
+
active={language.code === current}
|
|
59
|
+
data-lang={language.code}
|
|
60
|
+
menu
|
|
61
|
+
>
|
|
62
|
+
<span class="lang-row">
|
|
63
|
+
<span class="flag">{language.flag}</span>
|
|
64
|
+
<span class="name">{language.name}</span>
|
|
65
|
+
<span class="code">{language.code.toUpperCase()}</span>
|
|
66
|
+
</span>
|
|
67
|
+
</HeaderNavLink>
|
|
68
|
+
{/each}
|
|
69
|
+
</div>
|
|
70
|
+
{/snippet}
|
|
71
|
+
</Dropdown>
|
|
72
|
+
</div>
|
|
73
|
+
{/if}
|
|
74
|
+
|
|
75
|
+
<style>
|
|
76
|
+
.header-language-toggle {
|
|
77
|
+
position: relative;
|
|
78
|
+
display: inline-flex;
|
|
79
|
+
margin-left: 12px;
|
|
80
|
+
padding-left: 12px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.header-language-toggle::before {
|
|
84
|
+
content: '';
|
|
85
|
+
position: absolute;
|
|
86
|
+
left: 0;
|
|
87
|
+
top: 50%;
|
|
88
|
+
transform: translateY(-50%);
|
|
89
|
+
width: 1px;
|
|
90
|
+
height: 16px;
|
|
91
|
+
background: var(--border);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.flag {
|
|
95
|
+
font-size: 15px;
|
|
96
|
+
line-height: 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.menu {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
gap: 2px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.lang-row {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 8px;
|
|
109
|
+
width: 100%;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.code {
|
|
113
|
+
margin-left: auto;
|
|
114
|
+
padding-left: 8px;
|
|
115
|
+
font-size: 11px;
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
color: var(--text-light);
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DropdownAlign, DropdownPosition } from '../../components/Dropdown/dropdown.types.js';
|
|
2
|
+
import type { LanguageOption } from './language.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
languages: LanguageOption[];
|
|
5
|
+
current: string;
|
|
6
|
+
href: (code: string) => string;
|
|
7
|
+
showName?: boolean;
|
|
8
|
+
align?: DropdownAlign;
|
|
9
|
+
position?: DropdownPosition;
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const HeaderLanguageToggle: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type HeaderLanguageToggle = ReturnType<typeof HeaderLanguageToggle>;
|
|
14
|
+
export default HeaderLanguageToggle;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
interface ComponentProps {
|
|
6
|
+
href?: string;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
menu?: boolean;
|
|
9
|
+
target?: string;
|
|
10
|
+
rel?: string;
|
|
11
|
+
start?: Snippet;
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
end?: Snippet;
|
|
14
|
+
description?: Snippet;
|
|
15
|
+
onclick?: (event: MouseEvent) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type Props = ComponentProps & Omit<HTMLAttributes<HTMLElement>, keyof ComponentProps>;
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
href,
|
|
22
|
+
active = false,
|
|
23
|
+
menu = false,
|
|
24
|
+
target,
|
|
25
|
+
rel,
|
|
26
|
+
start,
|
|
27
|
+
children,
|
|
28
|
+
end,
|
|
29
|
+
description,
|
|
30
|
+
onclick,
|
|
31
|
+
...rest
|
|
32
|
+
}: Props = $props();
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<svelte:element
|
|
36
|
+
this={href ? 'a' : 'span'}
|
|
37
|
+
{href}
|
|
38
|
+
{target}
|
|
39
|
+
{rel}
|
|
40
|
+
{onclick}
|
|
41
|
+
role={href ? undefined : 'button'}
|
|
42
|
+
tabindex={href ? undefined : 0}
|
|
43
|
+
class="header-nav-link"
|
|
44
|
+
class:active
|
|
45
|
+
class:menu={menu || !!description}
|
|
46
|
+
class:rich={!!description}
|
|
47
|
+
{...rest}
|
|
48
|
+
>
|
|
49
|
+
{#if description}
|
|
50
|
+
{#if start}
|
|
51
|
+
<span class="icon-box">{@render start()}</span>
|
|
52
|
+
{/if}
|
|
53
|
+
<span class="text-stack">
|
|
54
|
+
<span class="title">{@render children?.()}</span>
|
|
55
|
+
<span class="desc">{@render description()}</span>
|
|
56
|
+
</span>
|
|
57
|
+
{:else}
|
|
58
|
+
{#if start}
|
|
59
|
+
<span class="start">{@render start()}</span>
|
|
60
|
+
{/if}
|
|
61
|
+
{@render children?.()}
|
|
62
|
+
{#if end}
|
|
63
|
+
<span class="end">{@render end()}</span>
|
|
64
|
+
{/if}
|
|
65
|
+
{/if}
|
|
66
|
+
</svelte:element>
|
|
67
|
+
|
|
68
|
+
<style>
|
|
69
|
+
.header-nav-link {
|
|
70
|
+
display: inline-flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 6px;
|
|
73
|
+
padding: 6px 16px;
|
|
74
|
+
border-radius: 20px;
|
|
75
|
+
font-size: 13px;
|
|
76
|
+
font-weight: 500;
|
|
77
|
+
color: var(--text-light);
|
|
78
|
+
text-decoration: none;
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
user-select: none;
|
|
82
|
+
transition:
|
|
83
|
+
0.15s background-color,
|
|
84
|
+
0.15s color;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.header-nav-link:hover {
|
|
88
|
+
background: var(--hover, var(--accent-lightest));
|
|
89
|
+
color: var(--text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.header-nav-link.active {
|
|
93
|
+
background: var(--accent-light);
|
|
94
|
+
color: var(--text);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.header-nav-link.menu {
|
|
98
|
+
width: 100%;
|
|
99
|
+
padding: 8px 10px;
|
|
100
|
+
border-radius: 12px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.start,
|
|
104
|
+
.end {
|
|
105
|
+
display: inline-flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.header-nav-link.rich {
|
|
110
|
+
align-items: flex-start;
|
|
111
|
+
gap: 10px;
|
|
112
|
+
white-space: normal;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.icon-box {
|
|
116
|
+
flex: none;
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
width: 30px;
|
|
121
|
+
height: 30px;
|
|
122
|
+
border-radius: 8px;
|
|
123
|
+
background: var(--accent-light);
|
|
124
|
+
color: var(--accent);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.header-nav-link.rich.active .icon-box {
|
|
128
|
+
background: var(--background);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.text-stack {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
gap: 1px;
|
|
135
|
+
padding-top: 1px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.title {
|
|
139
|
+
font-size: 13px;
|
|
140
|
+
font-weight: 600;
|
|
141
|
+
color: var(--text);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.desc {
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
font-weight: 400;
|
|
147
|
+
color: var(--text-light);
|
|
148
|
+
}
|
|
149
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
interface ComponentProps {
|
|
4
|
+
href?: string;
|
|
5
|
+
active?: boolean;
|
|
6
|
+
menu?: boolean;
|
|
7
|
+
target?: string;
|
|
8
|
+
rel?: string;
|
|
9
|
+
start?: Snippet;
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
end?: Snippet;
|
|
12
|
+
description?: Snippet;
|
|
13
|
+
onclick?: (event: MouseEvent) => void;
|
|
14
|
+
}
|
|
15
|
+
type Props = ComponentProps & Omit<HTMLAttributes<HTMLElement>, keyof ComponentProps>;
|
|
16
|
+
declare const HeaderNavLink: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type HeaderNavLink = ReturnType<typeof HeaderNavLink>;
|
|
18
|
+
export default HeaderNavLink;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface LanguageOption {
|
|
2
|
+
code: string;
|
|
3
|
+
flag: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Rewrites `path` (the current pathname) from `currentLang` to `targetLang`,
|
|
8
|
+
* for the common convention where the default language is served without a
|
|
9
|
+
* prefix and every other language is served under `/{code}`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* buildLocalizedUrl('/pricing', 'en', 'fr', 'en') // '/fr/pricing'
|
|
13
|
+
* buildLocalizedUrl('/fr/pricing', 'fr', 'en', 'en') // '/pricing'
|
|
14
|
+
* buildLocalizedUrl('/fr', 'fr', 'en', 'en') // '/'
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildLocalizedUrl(path: string, currentLang: string, targetLang: string, defaultLang: string): string;
|