@hyvor/design 2.1.13 → 2.1.15
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/components/ButtonGroup/ButtonGroup.svelte +5 -4
- package/dist/components/ButtonGroup/ButtonGroup.svelte.d.ts +1 -0
- package/dist/components/Dropdown/DropdownContent.svelte +22 -6
- package/dist/components/FileUploader/Media/Media.svelte +1 -1
- package/dist/components/FileUploader/file-uploader.d.ts +1 -1
- package/dist/marketing/AllFeaturesAccordion/AllFeaturesAccordion.svelte +30 -0
- package/dist/marketing/AllFeaturesAccordion/AllFeaturesAccordionFeature.svelte +15 -2
- package/dist/marketing/Footer/Footer.svelte +28 -1
- package/dist/marketing/Footer/FooterLinkList.svelte +5 -1
- package/dist/marketing/Header/Header.svelte +21 -4
- package/dist/marketing/Header/HeaderLanguageToggle.svelte +50 -29
- package/dist/marketing/Header/HeaderMobileSection.svelte +108 -0
- package/dist/marketing/Header/HeaderMobileSection.svelte.d.ts +12 -0
- package/dist/marketing/Header/HeaderNavMenu.svelte +54 -0
- package/dist/marketing/Header/HeaderNavMenu.svelte.d.ts +12 -0
- package/dist/marketing/Header/breakpoint.d.ts +1 -0
- package/dist/marketing/Header/breakpoint.js +2 -0
- package/dist/marketing/LogoStrip/LogoStrip.svelte +48 -24
- package/dist/marketing/Testimonials/Testimonials.svelte +134 -17
- package/dist/marketing/Testimonials/Testimonials.svelte.d.ts +7 -0
- package/dist/marketing/index.d.ts +1 -0
- package/dist/marketing/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
interface Props {
|
|
3
3
|
column?: boolean;
|
|
4
|
+
divider?: boolean;
|
|
4
5
|
children?: import('svelte').Snippet;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
|
-
let { column = false, children }: Props = $props();
|
|
8
|
+
let { column = false, divider = true, children }: Props = $props();
|
|
8
9
|
</script>
|
|
9
10
|
|
|
10
|
-
<div class="button-group" class:column>
|
|
11
|
+
<div class="button-group" class:column class:no-divider={!divider}>
|
|
11
12
|
{@render children?.()}
|
|
12
13
|
</div>
|
|
13
14
|
|
|
@@ -57,7 +58,7 @@
|
|
|
57
58
|
margin-top: -1px;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
.button-group:not(.column) :global(> .button:not(.outline):not(.outline-fill):not(:first-child))::before {
|
|
61
|
+
.button-group:not(.no-divider):not(.column) :global(> .button:not(.outline):not(.outline-fill):not(:first-child))::before {
|
|
61
62
|
content: "";
|
|
62
63
|
position: absolute;
|
|
63
64
|
top: 22%;
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
pointer-events: none;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
.button-group.column :global(> .button:not(.outline):not(.outline-fill):not(:first-child))::before {
|
|
73
|
+
.button-group.column:not(.no-divider) :global(> .button:not(.outline):not(.outline-fill):not(:first-child))::before {
|
|
73
74
|
content: "";
|
|
74
75
|
position: absolute;
|
|
75
76
|
left: 22%;
|
|
@@ -46,13 +46,10 @@
|
|
|
46
46
|
|
|
47
47
|
if (position === 'bottom') {
|
|
48
48
|
contentWrap.style.top = triggerRect.bottom + GAP + 'px';
|
|
49
|
-
|
|
50
|
-
contentWrap.style.bottom = SPACE_AROUND + 'px';
|
|
51
|
-
} else {
|
|
52
|
-
contentWrap.style.bottom = 'auto';
|
|
53
|
-
}
|
|
49
|
+
contentWrap.style.bottom = 'auto';
|
|
54
50
|
} else if (position === 'top') {
|
|
55
|
-
contentWrap.style.top =
|
|
51
|
+
contentWrap.style.top =
|
|
52
|
+
Math.max(triggerRect.top - contentRect.height - GAP, SPACE_AROUND) + 'px';
|
|
56
53
|
} else if (position === 'left') {
|
|
57
54
|
contentWrap.style.left = triggerRect.left - width - GAP + 'px';
|
|
58
55
|
} else if (position === 'right') {
|
|
@@ -88,6 +85,17 @@
|
|
|
88
85
|
} else {
|
|
89
86
|
contentWrap.style.width = width + 'px';
|
|
90
87
|
}
|
|
88
|
+
|
|
89
|
+
// cap to viewport height; the panel scrolls internally past that
|
|
90
|
+
let available: number;
|
|
91
|
+
if (position === 'bottom') {
|
|
92
|
+
available = window.innerHeight - (triggerRect.bottom + GAP) - SPACE_AROUND;
|
|
93
|
+
} else if (position === 'top') {
|
|
94
|
+
available = triggerRect.top - GAP - SPACE_AROUND;
|
|
95
|
+
} else {
|
|
96
|
+
available = window.innerHeight - SPACE_AROUND * 2;
|
|
97
|
+
}
|
|
98
|
+
contentWrap.style.maxHeight = Math.max(available, 120) + 'px';
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
$effect(() => {
|
|
@@ -137,4 +145,12 @@
|
|
|
137
145
|
<style>.content-wrap {
|
|
138
146
|
position: fixed;
|
|
139
147
|
z-index: 1000000;
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.content {
|
|
153
|
+
overflow-y: auto;
|
|
154
|
+
min-height: 0;
|
|
155
|
+
overscroll-behavior: contain;
|
|
140
156
|
}</style>
|
|
@@ -30,7 +30,7 @@ export interface FileUploaderConfig {
|
|
|
30
30
|
}>;
|
|
31
31
|
allowedMimeTypes?: string[];
|
|
32
32
|
maxFileSizeInMB?: number;
|
|
33
|
-
mediaLoad?: (page: number) => Promise<MediaItem[]>;
|
|
33
|
+
mediaLoad?: (page: number, type: UploadType) => Promise<MediaItem[]>;
|
|
34
34
|
unsplashSearch?: (search: string, page: number) => Promise<UnsplashImage[]>;
|
|
35
35
|
excalidraw?: boolean;
|
|
36
36
|
}
|
|
@@ -244,8 +244,38 @@
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
@media (max-width: 768px) {
|
|
247
|
+
.hds-all-features-accordion {
|
|
248
|
+
padding: 56px 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.section-header {
|
|
252
|
+
margin-bottom: 32px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.section-header h2 {
|
|
256
|
+
font-size: 26px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.section-header p {
|
|
260
|
+
font-size: 0.9rem;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.accordion-trigger {
|
|
264
|
+
gap: 12px;
|
|
265
|
+
padding: 16px 14px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.cat-label {
|
|
269
|
+
font-size: 15px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.accordion-content {
|
|
273
|
+
padding: 4px 14px 20px;
|
|
274
|
+
}
|
|
275
|
+
|
|
247
276
|
.features-grid {
|
|
248
277
|
grid-template-columns: 1fr;
|
|
278
|
+
gap: 18px;
|
|
249
279
|
}
|
|
250
280
|
}
|
|
251
281
|
</style>
|
|
@@ -60,11 +60,24 @@
|
|
|
60
60
|
@media (max-width: 992px) {
|
|
61
61
|
.feature {
|
|
62
62
|
width: 100%;
|
|
63
|
-
text-align: center;
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
.icon-bg {
|
|
67
|
-
|
|
66
|
+
opacity: 0.1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.icon-bg :global(svg) {
|
|
70
|
+
width: 22px;
|
|
71
|
+
height: 22px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.title {
|
|
75
|
+
font-size: 15px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.description {
|
|
79
|
+
font-size: 13px;
|
|
80
|
+
margin-top: 4px;
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
</style>
|
|
@@ -374,7 +374,9 @@
|
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
.footer-center {
|
|
377
|
-
display:
|
|
377
|
+
display: grid;
|
|
378
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
379
|
+
gap: 40px;
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
.footer-center:not(:empty) {
|
|
@@ -474,5 +476,30 @@
|
|
|
474
476
|
flex-direction: column;
|
|
475
477
|
align-items: flex-start;
|
|
476
478
|
}
|
|
479
|
+
|
|
480
|
+
.top-row-right {
|
|
481
|
+
gap: 16px;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.footer-center {
|
|
485
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
486
|
+
gap: 32px 20px;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.footer-center:not(:empty) {
|
|
490
|
+
padding: 36px 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.bottom-bar {
|
|
494
|
+
flex-direction: column;
|
|
495
|
+
align-items: flex-start;
|
|
496
|
+
gap: 16px;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
@media (max-width: 360px) {
|
|
501
|
+
.footer-center {
|
|
502
|
+
grid-template-columns: minmax(0, 1fr);
|
|
503
|
+
}
|
|
477
504
|
}
|
|
478
505
|
</style>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<style>
|
|
18
18
|
.link-list {
|
|
19
19
|
flex: 1;
|
|
20
|
-
min-width:
|
|
20
|
+
min-width: 0;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
.title {
|
|
@@ -27,18 +27,22 @@
|
|
|
27
27
|
letter-spacing: 0.05em;
|
|
28
28
|
color: var(--footer-muted, var(--text-light));
|
|
29
29
|
margin-bottom: 14px;
|
|
30
|
+
overflow-wrap: break-word;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
.links {
|
|
33
34
|
display: flex;
|
|
34
35
|
flex-direction: column;
|
|
35
36
|
align-items: flex-start;
|
|
37
|
+
min-width: 0;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
.links :global(a) {
|
|
39
41
|
font-size: 14px;
|
|
40
42
|
color: var(--footer-text, var(--text-light));
|
|
41
43
|
margin-top: 10px;
|
|
44
|
+
max-width: 100%;
|
|
45
|
+
overflow-wrap: break-word;
|
|
42
46
|
}
|
|
43
47
|
.links :global(a:hover) {
|
|
44
48
|
color: var(--footer-text-strong, var(--text));
|
|
@@ -114,7 +114,9 @@
|
|
|
114
114
|
</div>
|
|
115
115
|
{/if}
|
|
116
116
|
{#if end}
|
|
117
|
-
|
|
117
|
+
{#if center}
|
|
118
|
+
<div class="mobile-divider"></div>
|
|
119
|
+
{/if}
|
|
118
120
|
<div class="mobile-inner end">
|
|
119
121
|
{@render end()}
|
|
120
122
|
</div>
|
|
@@ -240,15 +242,30 @@
|
|
|
240
242
|
display: flex;
|
|
241
243
|
}
|
|
242
244
|
|
|
245
|
+
/* make every nav link a full-width list row inside the mobile menu */
|
|
246
|
+
.mobile-content :global(.header-nav-link) {
|
|
247
|
+
width: 100%;
|
|
248
|
+
justify-content: flex-start;
|
|
249
|
+
padding: 8px 10px;
|
|
250
|
+
border-radius: 12px;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* hide submenu link icon in the mobile menu */
|
|
254
|
+
.mobile-content :global(.header-nav-link .icon-box),
|
|
255
|
+
.mobile-content :global(.header-nav-link .start) {
|
|
256
|
+
display: none;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.mobile-content :global(.header-nav-link.rich) {
|
|
260
|
+
gap: 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
243
263
|
.mobile-divider {
|
|
244
264
|
height: 1px;
|
|
245
265
|
background: var(--border);
|
|
246
266
|
margin: 6px 4px;
|
|
247
267
|
}
|
|
248
268
|
|
|
249
|
-
/*
|
|
250
|
-
used to prevent the content from being hidden behind the header
|
|
251
|
-
*/
|
|
252
269
|
:global(html) {
|
|
253
270
|
scroll-padding-top: calc(var(--header-height) + 20px);
|
|
254
271
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { MediaQuery } from 'svelte/reactivity';
|
|
2
3
|
import Dropdown from '../../components/Dropdown/Dropdown.svelte';
|
|
3
4
|
import HeaderNavLink from './HeaderNavLink.svelte';
|
|
5
|
+
import HeaderMobileSection from './HeaderMobileSection.svelte';
|
|
4
6
|
import type {
|
|
5
7
|
DropdownAlign,
|
|
6
8
|
DropdownPosition
|
|
7
9
|
} from '../../components/Dropdown/dropdown.types.js';
|
|
8
10
|
import type { LanguageOption } from './language.js';
|
|
11
|
+
import { HEADER_MOBILE_BREAKPOINT } from './breakpoint.js';
|
|
9
12
|
|
|
10
13
|
interface Props {
|
|
11
14
|
languages: LanguageOption[];
|
|
@@ -29,9 +32,12 @@
|
|
|
29
32
|
|
|
30
33
|
const currentLanguage = $derived(languages.find((l) => l.code === current) ?? languages[0]);
|
|
31
34
|
|
|
35
|
+
// below the breakpoint: render as a collapsible inline section, not a dropdown
|
|
36
|
+
const mobile = new MediaQuery(`(max-width: ${HEADER_MOBILE_BREAKPOINT}px)`);
|
|
37
|
+
|
|
32
38
|
let show = $state(false);
|
|
33
39
|
|
|
34
|
-
// close the dropdown
|
|
40
|
+
// close the dropdown when a language link inside it is clicked
|
|
35
41
|
function closeOnLinkClick(e: MouseEvent) {
|
|
36
42
|
const target = e.target as HTMLElement;
|
|
37
43
|
if (target.tagName === 'A' || target.closest('a')) {
|
|
@@ -40,36 +46,51 @@
|
|
|
40
46
|
}
|
|
41
47
|
</script>
|
|
42
48
|
|
|
49
|
+
{#snippet languageLink(language: LanguageOption)}
|
|
50
|
+
<HeaderNavLink
|
|
51
|
+
href={href(language.code)}
|
|
52
|
+
active={language.code === current}
|
|
53
|
+
data-lang={language.code}
|
|
54
|
+
menu
|
|
55
|
+
>
|
|
56
|
+
<span class="lang-row">
|
|
57
|
+
<span class="flag">{language.flag}</span>
|
|
58
|
+
<span class="name">{language.name}</span>
|
|
59
|
+
<span class="code">{language.code.toUpperCase()}</span>
|
|
60
|
+
</span>
|
|
61
|
+
</HeaderNavLink>
|
|
62
|
+
{/snippet}
|
|
63
|
+
|
|
43
64
|
{#if currentLanguage}
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
{#snippet
|
|
47
|
-
<
|
|
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>
|
|
65
|
+
{#if mobile.current}
|
|
66
|
+
<HeaderMobileSection {label}>
|
|
67
|
+
{#snippet trailing()}
|
|
68
|
+
<span class="flag">{currentLanguage.flag}</span>
|
|
70
69
|
{/snippet}
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
{#each languages as language (language.code)}
|
|
71
|
+
{@render languageLink(language)}
|
|
72
|
+
{/each}
|
|
73
|
+
</HeaderMobileSection>
|
|
74
|
+
{:else}
|
|
75
|
+
<div class="header-language-toggle">
|
|
76
|
+
<Dropdown bind:show {align} {position} contentPadding={8}>
|
|
77
|
+
{#snippet trigger()}
|
|
78
|
+
<HeaderNavLink aria-label={label} aria-expanded={show}>
|
|
79
|
+
<span class="flag">{currentLanguage.flag}</span>
|
|
80
|
+
{#if showName}{currentLanguage.name}{/if}
|
|
81
|
+
</HeaderNavLink>
|
|
82
|
+
{/snippet}
|
|
83
|
+
{#snippet content()}
|
|
84
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
85
|
+
<div class="menu" onclick={closeOnLinkClick}>
|
|
86
|
+
{#each languages as language (language.code)}
|
|
87
|
+
{@render languageLink(language)}
|
|
88
|
+
{/each}
|
|
89
|
+
</div>
|
|
90
|
+
{/snippet}
|
|
91
|
+
</Dropdown>
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
73
94
|
{/if}
|
|
74
95
|
|
|
75
96
|
<style>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { slide } from 'svelte/transition';
|
|
4
|
+
import IconChevronRight from '@hyvor/icons/IconChevronRight';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Section heading, shown on the toggle row. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Optional content shown on the toggle row, just before the chevron. */
|
|
10
|
+
trailing?: Snippet;
|
|
11
|
+
/** The section content, revealed when expanded. */
|
|
12
|
+
children: Snippet;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { label, trailing, children }: Props = $props();
|
|
16
|
+
|
|
17
|
+
let expanded = $state(false);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div class="section">
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
class="toggle"
|
|
24
|
+
class:expanded
|
|
25
|
+
aria-expanded={expanded}
|
|
26
|
+
onclick={() => (expanded = !expanded)}
|
|
27
|
+
>
|
|
28
|
+
<span class="label">{label}</span>
|
|
29
|
+
<span class="right">
|
|
30
|
+
{#if trailing}
|
|
31
|
+
<span class="trailing">{@render trailing()}</span>
|
|
32
|
+
{/if}
|
|
33
|
+
<span class="chevron"><IconChevronRight size={12} /></span>
|
|
34
|
+
</span>
|
|
35
|
+
</button>
|
|
36
|
+
{#if expanded}
|
|
37
|
+
<div class="items" transition:slide={{ duration: 150 }}>
|
|
38
|
+
{@render children()}
|
|
39
|
+
</div>
|
|
40
|
+
{/if}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.section {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: 2px;
|
|
48
|
+
width: 100%;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.toggle {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: space-between;
|
|
55
|
+
gap: 8px;
|
|
56
|
+
width: 100%;
|
|
57
|
+
padding: 8px 10px;
|
|
58
|
+
border: none;
|
|
59
|
+
background: none;
|
|
60
|
+
border-radius: 12px;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
font: inherit;
|
|
63
|
+
text-align: left;
|
|
64
|
+
color: var(--text-light);
|
|
65
|
+
transition: background-color 0.15s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.toggle:hover {
|
|
69
|
+
background: var(--hover, var(--accent-lightest));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.label {
|
|
73
|
+
font-size: 11px;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
text-transform: uppercase;
|
|
76
|
+
letter-spacing: 0.04em;
|
|
77
|
+
color: var(--text-light);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.right {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: 8px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.trailing {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.chevron {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
color: var(--text-light);
|
|
94
|
+
transition: transform 0.15s ease;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.toggle.expanded .chevron {
|
|
98
|
+
transform: rotate(90deg);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.items {
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
gap: 2px;
|
|
105
|
+
/* nudge sub-items in so they read as children of the section label */
|
|
106
|
+
padding-left: 8px;
|
|
107
|
+
}
|
|
108
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Section heading, shown on the toggle row. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Optional content shown on the toggle row, just before the chevron. */
|
|
6
|
+
trailing?: Snippet;
|
|
7
|
+
/** The section content, revealed when expanded. */
|
|
8
|
+
children: Snippet;
|
|
9
|
+
}
|
|
10
|
+
declare const HeaderMobileSection: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type HeaderMobileSection = ReturnType<typeof HeaderMobileSection>;
|
|
12
|
+
export default HeaderMobileSection;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { MediaQuery } from 'svelte/reactivity';
|
|
4
|
+
import Dropdown from '../../components/Dropdown/Dropdown.svelte';
|
|
5
|
+
import HeaderNavLink from './HeaderNavLink.svelte';
|
|
6
|
+
import HeaderMobileSection from './HeaderMobileSection.svelte';
|
|
7
|
+
import IconCaretDown from '@hyvor/icons/IconCaretDown';
|
|
8
|
+
import type { DropdownAlign } from '../../components/Dropdown/dropdown.types.js';
|
|
9
|
+
import { HEADER_MOBILE_BREAKPOINT } from './breakpoint.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
label: string;
|
|
13
|
+
active?: boolean;
|
|
14
|
+
children: Snippet;
|
|
15
|
+
width?: number;
|
|
16
|
+
align?: DropdownAlign;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { label, active = false, children, width = 300, align = 'center' }: Props = $props();
|
|
20
|
+
|
|
21
|
+
// below the breakpoint: render as a collapsible inline section, not a dropdown
|
|
22
|
+
const mobile = new MediaQuery(`(max-width: ${HEADER_MOBILE_BREAKPOINT}px)`);
|
|
23
|
+
|
|
24
|
+
let open = $state(false);
|
|
25
|
+
|
|
26
|
+
// close the dropdown when a link inside it is clicked
|
|
27
|
+
function closeOnLinkClick(e: MouseEvent) {
|
|
28
|
+
const target = e.target as HTMLElement;
|
|
29
|
+
if (target.tagName === 'A' || target.closest('a')) {
|
|
30
|
+
open = false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
{#if mobile.current}
|
|
36
|
+
<HeaderMobileSection {label}>
|
|
37
|
+
{@render children()}
|
|
38
|
+
</HeaderMobileSection>
|
|
39
|
+
{:else}
|
|
40
|
+
<Dropdown bind:show={open} {width} {align} contentPadding={8}>
|
|
41
|
+
{#snippet trigger()}
|
|
42
|
+
<HeaderNavLink active={active || open}>
|
|
43
|
+
{label}
|
|
44
|
+
{#snippet end()}<IconCaretDown size={11} />{/snippet}
|
|
45
|
+
</HeaderNavLink>
|
|
46
|
+
{/snippet}
|
|
47
|
+
{#snippet content()}
|
|
48
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
49
|
+
<div onclick={closeOnLinkClick}>
|
|
50
|
+
{@render children()}
|
|
51
|
+
</div>
|
|
52
|
+
{/snippet}
|
|
53
|
+
</Dropdown>
|
|
54
|
+
{/if}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { DropdownAlign } from '../../components/Dropdown/dropdown.types.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
label: string;
|
|
5
|
+
active?: boolean;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
width?: number;
|
|
8
|
+
align?: DropdownAlign;
|
|
9
|
+
}
|
|
10
|
+
declare const HeaderNavMenu: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type HeaderNavMenu = ReturnType<typeof HeaderNavMenu>;
|
|
12
|
+
export default HeaderNavMenu;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HEADER_MOBILE_BREAKPOINT = 992;
|
|
@@ -26,8 +26,15 @@
|
|
|
26
26
|
speed = 34
|
|
27
27
|
}: Props = $props();
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
let containerWidth = $state(0);
|
|
30
|
+
let setWidths = $state<number[]>([]);
|
|
31
|
+
const setWidth = $derived(setWidths[0] ?? 0);
|
|
32
|
+
|
|
33
|
+
const copies = $derived.by(() => {
|
|
34
|
+
if (!setWidth || !containerWidth) return 2;
|
|
35
|
+
const needed = Math.ceil((containerWidth * 2) / setWidth);
|
|
36
|
+
return Math.min(20, Math.max(2, needed % 2 === 0 ? needed : needed + 1));
|
|
37
|
+
});
|
|
31
38
|
</script>
|
|
32
39
|
|
|
33
40
|
{#snippet logoImg(logo: Logo, hidden = false)}
|
|
@@ -65,23 +72,27 @@
|
|
|
65
72
|
</div>
|
|
66
73
|
|
|
67
74
|
{#if marquee}
|
|
68
|
-
<div class="marquee hds-container-max">
|
|
69
|
-
<div class="marquee-track">
|
|
70
|
-
{#each
|
|
71
|
-
{@const hidden =
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
75
|
+
<div class="marquee hds-container-max" bind:clientWidth={containerWidth}>
|
|
76
|
+
<div class="marquee-track" style:--ls-copies={copies}>
|
|
77
|
+
{#each Array.from({ length: copies }) as _, setIndex (setIndex)}
|
|
78
|
+
{@const hidden = setIndex > 0}
|
|
79
|
+
<div class="marquee-set" bind:clientWidth={setWidths[setIndex]} aria-hidden={hidden}>
|
|
80
|
+
{#each logos as logo}
|
|
81
|
+
{#if logo.href}
|
|
82
|
+
<a
|
|
83
|
+
href={logo.href}
|
|
84
|
+
target="_blank"
|
|
85
|
+
rel="noopener"
|
|
86
|
+
class="logo-link"
|
|
87
|
+
tabindex={hidden ? -1 : undefined}
|
|
88
|
+
>
|
|
89
|
+
{@render logoImg(logo, hidden)}
|
|
90
|
+
</a>
|
|
91
|
+
{:else}
|
|
92
|
+
{@render logoImg(logo, hidden)}
|
|
93
|
+
{/if}
|
|
94
|
+
{/each}
|
|
95
|
+
</div>
|
|
85
96
|
{/each}
|
|
86
97
|
</div>
|
|
87
98
|
</div>
|
|
@@ -134,9 +145,18 @@
|
|
|
134
145
|
.marquee-track {
|
|
135
146
|
display: flex;
|
|
136
147
|
align-items: center;
|
|
137
|
-
gap: 52px;
|
|
138
148
|
width: max-content;
|
|
139
|
-
|
|
149
|
+
/* keep a constant per-set speed regardless of how many sets we repeat */
|
|
150
|
+
animation: marquee-scroll calc(var(--ls-speed) * var(--ls-copies, 2) / 2) linear infinite;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.marquee-set {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
flex: none;
|
|
157
|
+
gap: 52px;
|
|
158
|
+
/* trailing gap so every set is an identical width and -50% lands cleanly */
|
|
159
|
+
padding-right: 52px;
|
|
140
160
|
}
|
|
141
161
|
|
|
142
162
|
.marquee:hover .marquee-track {
|
|
@@ -148,7 +168,6 @@
|
|
|
148
168
|
transform: translateX(0);
|
|
149
169
|
}
|
|
150
170
|
to {
|
|
151
|
-
/* one full set-width, since the track is two identical sets back to back */
|
|
152
171
|
transform: translateX(-50%);
|
|
153
172
|
}
|
|
154
173
|
}
|
|
@@ -186,13 +205,18 @@
|
|
|
186
205
|
|
|
187
206
|
.marquee-track {
|
|
188
207
|
animation: none;
|
|
208
|
+
width: 100%;
|
|
209
|
+
padding: 0 15px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.marquee-set {
|
|
189
213
|
flex-wrap: wrap;
|
|
190
214
|
justify-content: center;
|
|
191
215
|
width: 100%;
|
|
192
|
-
padding: 0
|
|
216
|
+
padding-right: 0;
|
|
193
217
|
}
|
|
194
218
|
|
|
195
|
-
.marquee-
|
|
219
|
+
.marquee-set:not(:first-child) {
|
|
196
220
|
display: none;
|
|
197
221
|
}
|
|
198
222
|
}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
type: 'text';
|
|
4
4
|
name: string;
|
|
5
5
|
role: string;
|
|
6
|
+
company?: string;
|
|
7
|
+
companyUrl?: string;
|
|
8
|
+
imageUrl?: string;
|
|
9
|
+
summary?: string;
|
|
6
10
|
quote: string;
|
|
7
11
|
}
|
|
8
12
|
|
|
@@ -10,6 +14,9 @@
|
|
|
10
14
|
type: 'video';
|
|
11
15
|
name: string;
|
|
12
16
|
role: string;
|
|
17
|
+
company: string;
|
|
18
|
+
companyUrl?: string;
|
|
19
|
+
imageUrl?: string;
|
|
13
20
|
videoUrl?: string;
|
|
14
21
|
posterUrl?: string;
|
|
15
22
|
summary?: string;
|
|
@@ -102,6 +109,24 @@
|
|
|
102
109
|
{/if}
|
|
103
110
|
</svelte:head>
|
|
104
111
|
|
|
112
|
+
{#snippet meta(review: Review)}
|
|
113
|
+
<div class="role">
|
|
114
|
+
{review.role}, <br />
|
|
115
|
+
{#if review.companyUrl}
|
|
116
|
+
<a
|
|
117
|
+
class="company"
|
|
118
|
+
href={review.companyUrl}
|
|
119
|
+
target="_blank"
|
|
120
|
+
rel="nofollow noopener noreferrer"
|
|
121
|
+
>
|
|
122
|
+
{review.company}
|
|
123
|
+
</a>
|
|
124
|
+
{:else}
|
|
125
|
+
{review.company}
|
|
126
|
+
{/if}
|
|
127
|
+
</div>
|
|
128
|
+
{/snippet}
|
|
129
|
+
|
|
105
130
|
<section class="hds-testimonials" class:handwritten={handwrittenNames}>
|
|
106
131
|
<div class="hds-container head">
|
|
107
132
|
<p class="label">{label}</p>
|
|
@@ -124,15 +149,34 @@
|
|
|
124
149
|
{#if review.type === 'text'}
|
|
125
150
|
{@const av = identicon(review.name)}
|
|
126
151
|
<figure class="card text-card hds-box">
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
{#if review.imageUrl}
|
|
153
|
+
<img class="avatar photo card-avatar" src={review.imageUrl} alt={review.name} />
|
|
154
|
+
{:else}
|
|
155
|
+
<svg
|
|
156
|
+
class="avatar card-avatar"
|
|
157
|
+
viewBox="0 0 5 5"
|
|
158
|
+
style="background: {av.bg}"
|
|
159
|
+
aria-hidden="true"
|
|
160
|
+
>
|
|
161
|
+
{#each av.cells as cell}
|
|
162
|
+
<rect x={cell.x} y={cell.y} width="1" height="1" fill={av.fg} />
|
|
163
|
+
{/each}
|
|
164
|
+
</svg>
|
|
165
|
+
{/if}
|
|
166
|
+
<svg class="quote-mark" viewBox="0 0 24 24" aria-hidden="true">
|
|
167
|
+
<path
|
|
168
|
+
d="M10 7c-3.3 0-6 2.7-6 6v4h6v-6H7c0-1.7 1.3-3 3-3V7zm10 0c-3.3 0-6 2.7-6 6v4h6v-6h-3c0-1.7 1.3-3 3-3V7z"
|
|
169
|
+
/>
|
|
131
170
|
</svg>
|
|
171
|
+
{#if review.summary}
|
|
172
|
+
<p class="text-summary">{review.summary}</p>
|
|
173
|
+
{/if}
|
|
132
174
|
<blockquote>“{review.quote}”</blockquote>
|
|
133
175
|
<figcaption>
|
|
134
|
-
<span class="
|
|
135
|
-
|
|
176
|
+
<span class="caption-text">
|
|
177
|
+
<span class="name">{review.name}</span>
|
|
178
|
+
{@render meta(review)}
|
|
179
|
+
</span>
|
|
136
180
|
</figcaption>
|
|
137
181
|
</figure>
|
|
138
182
|
{:else}
|
|
@@ -160,8 +204,8 @@
|
|
|
160
204
|
{#if !playing[i]}
|
|
161
205
|
<div class="poster-scrim"></div>
|
|
162
206
|
|
|
163
|
-
{#if
|
|
164
|
-
<p class="video-summary"
|
|
207
|
+
{#if review.summary}
|
|
208
|
+
<p class="video-summary">“{review.summary}”</p>
|
|
165
209
|
{/if}
|
|
166
210
|
|
|
167
211
|
<button
|
|
@@ -181,8 +225,13 @@
|
|
|
181
225
|
</button>
|
|
182
226
|
|
|
183
227
|
<figcaption>
|
|
184
|
-
|
|
185
|
-
|
|
228
|
+
{#if review.imageUrl}
|
|
229
|
+
<img class="avatar photo" src={review.imageUrl} alt={review.name} />
|
|
230
|
+
{/if}
|
|
231
|
+
<span class="caption-text">
|
|
232
|
+
<span class="name">{review.name}</span>
|
|
233
|
+
{@render meta(review)}
|
|
234
|
+
</span>
|
|
186
235
|
</figcaption>
|
|
187
236
|
{/if}
|
|
188
237
|
</figure>
|
|
@@ -266,6 +315,22 @@
|
|
|
266
315
|
flex-direction: column;
|
|
267
316
|
}
|
|
268
317
|
|
|
318
|
+
.quote-mark {
|
|
319
|
+
width: 40px;
|
|
320
|
+
height: 40px;
|
|
321
|
+
margin-bottom: 20px;
|
|
322
|
+
fill: var(--accent);
|
|
323
|
+
opacity: 0.14;
|
|
324
|
+
flex-shrink: 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.card-avatar {
|
|
328
|
+
position: absolute;
|
|
329
|
+
top: 28px;
|
|
330
|
+
right: 28px;
|
|
331
|
+
z-index: 1;
|
|
332
|
+
}
|
|
333
|
+
|
|
269
334
|
.avatar {
|
|
270
335
|
width: 46px;
|
|
271
336
|
height: 46px;
|
|
@@ -275,36 +340,80 @@
|
|
|
275
340
|
shape-rendering: crispEdges;
|
|
276
341
|
}
|
|
277
342
|
|
|
343
|
+
.avatar.photo {
|
|
344
|
+
object-fit: cover;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.text-summary {
|
|
348
|
+
margin: 0 0 14px;
|
|
349
|
+
font-family: var(--font-serif);
|
|
350
|
+
font-size: 18px;
|
|
351
|
+
font-weight: 800;
|
|
352
|
+
line-height: 1.25;
|
|
353
|
+
letter-spacing: -0.01em;
|
|
354
|
+
color: var(--text);
|
|
355
|
+
}
|
|
356
|
+
|
|
278
357
|
blockquote {
|
|
279
358
|
font-size: 16px;
|
|
280
359
|
line-height: 1.7;
|
|
281
360
|
color: var(--text);
|
|
282
|
-
margin:
|
|
361
|
+
margin: 0 0 20px;
|
|
283
362
|
flex: 1;
|
|
363
|
+
font-family: var(--font-serif);
|
|
284
364
|
}
|
|
285
365
|
|
|
286
366
|
.text-card figcaption {
|
|
367
|
+
display: flex;
|
|
368
|
+
align-items: center;
|
|
369
|
+
gap: 12px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.caption-text {
|
|
287
373
|
display: flex;
|
|
288
374
|
flex-direction: column;
|
|
289
|
-
|
|
375
|
+
justify-content: center;
|
|
376
|
+
gap: 2px;
|
|
377
|
+
min-width: 0;
|
|
290
378
|
}
|
|
291
379
|
|
|
292
380
|
.name {
|
|
293
381
|
font-size: 16px;
|
|
294
382
|
font-weight: 600;
|
|
295
383
|
color: var(--text);
|
|
296
|
-
line-height: 1.
|
|
384
|
+
line-height: 1.3;
|
|
297
385
|
}
|
|
298
386
|
|
|
299
387
|
.handwritten .name {
|
|
300
388
|
font-family: 'Caveat', cursive;
|
|
301
389
|
font-size: 28px;
|
|
302
390
|
font-weight: 600;
|
|
391
|
+
line-height: 1.15;
|
|
303
392
|
}
|
|
304
393
|
|
|
305
394
|
.role {
|
|
306
|
-
font-size:
|
|
395
|
+
font-size: 14px;
|
|
396
|
+
line-height: 1.35;
|
|
307
397
|
color: var(--text-light);
|
|
398
|
+
margin-top: 4px;
|
|
399
|
+
font-family: var(--font-serif);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.company {
|
|
403
|
+
align-self: flex-start;
|
|
404
|
+
font-size: 13px;
|
|
405
|
+
line-height: 1.35;
|
|
406
|
+
font-weight: 500;
|
|
407
|
+
color: var(--text);
|
|
408
|
+
text-decoration: none;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
a.company:hover {
|
|
412
|
+
text-decoration: underline;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.handwritten .company {
|
|
416
|
+
font-weight: 600;
|
|
308
417
|
}
|
|
309
418
|
|
|
310
419
|
.video-card {
|
|
@@ -344,13 +453,12 @@
|
|
|
344
453
|
right: 20px;
|
|
345
454
|
margin: 0;
|
|
346
455
|
font-family: var(--font-serif);
|
|
347
|
-
font-size:
|
|
456
|
+
font-size: 16px;
|
|
348
457
|
font-weight: 800;
|
|
349
458
|
line-height: 1.25;
|
|
350
459
|
letter-spacing: -0.01em;
|
|
351
460
|
color: #fff;
|
|
352
461
|
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
|
|
353
|
-
font-style: italic;
|
|
354
462
|
}
|
|
355
463
|
|
|
356
464
|
.play-btn {
|
|
@@ -400,7 +508,12 @@
|
|
|
400
508
|
padding: 40px 20px 20px;
|
|
401
509
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
|
|
402
510
|
display: flex;
|
|
403
|
-
|
|
511
|
+
align-items: center;
|
|
512
|
+
gap: 12px;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.video-card .avatar.photo {
|
|
516
|
+
border: 2px solid rgba(255, 255, 255, 0.7);
|
|
404
517
|
}
|
|
405
518
|
|
|
406
519
|
.video-card .name {
|
|
@@ -411,6 +524,10 @@
|
|
|
411
524
|
color: rgba(255, 255, 255, 0.75);
|
|
412
525
|
}
|
|
413
526
|
|
|
527
|
+
.video-card .company {
|
|
528
|
+
color: rgba(255, 255, 255, 0.9);
|
|
529
|
+
}
|
|
530
|
+
|
|
414
531
|
@media (max-width: 600px) {
|
|
415
532
|
.scroll-row {
|
|
416
533
|
--side-padding: max(20px, calc((100vw - 1000px) / 2));
|
|
@@ -2,12 +2,19 @@ interface TextReview {
|
|
|
2
2
|
type: 'text';
|
|
3
3
|
name: string;
|
|
4
4
|
role: string;
|
|
5
|
+
company?: string;
|
|
6
|
+
companyUrl?: string;
|
|
7
|
+
imageUrl?: string;
|
|
8
|
+
summary?: string;
|
|
5
9
|
quote: string;
|
|
6
10
|
}
|
|
7
11
|
interface VideoReview {
|
|
8
12
|
type: 'video';
|
|
9
13
|
name: string;
|
|
10
14
|
role: string;
|
|
15
|
+
company: string;
|
|
16
|
+
companyUrl?: string;
|
|
17
|
+
imageUrl?: string;
|
|
11
18
|
videoUrl?: string;
|
|
12
19
|
posterUrl?: string;
|
|
13
20
|
summary?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as Accordion } from './DetailsAccordion/DetailsAccordion.svelte';
|
|
2
2
|
export { default as Header } from './Header/Header.svelte';
|
|
3
3
|
export { default as HeaderNavLink } from './Header/HeaderNavLink.svelte';
|
|
4
|
+
export { default as HeaderNavMenu } from './Header/HeaderNavMenu.svelte';
|
|
4
5
|
export { default as HeaderLanguageToggle } from './Header/HeaderLanguageToggle.svelte';
|
|
5
6
|
export { buildLocalizedUrl } from './Header/language.js';
|
|
6
7
|
export type { LanguageOption } from './Header/language.js';
|
package/dist/marketing/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { default as Accordion } from './DetailsAccordion/DetailsAccordion.svelte
|
|
|
3
3
|
// ## Header
|
|
4
4
|
export { default as Header } from './Header/Header.svelte';
|
|
5
5
|
export { default as HeaderNavLink } from './Header/HeaderNavLink.svelte';
|
|
6
|
+
export { default as HeaderNavMenu } from './Header/HeaderNavMenu.svelte';
|
|
6
7
|
export { default as HeaderLanguageToggle } from './Header/HeaderLanguageToggle.svelte';
|
|
7
8
|
export { buildLocalizedUrl } from './Header/language.js';
|
|
8
9
|
// ## Footer
|