@hyvor/design 2.1.8-beta.1 → 2.1.8-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,11 @@
36
36
  recordVisit?: boolean;
37
37
  center?: Snippet;
38
38
  max?: boolean;
39
+ logoAltText?: string;
40
+ copyEmailLabel?: string;
41
+ copiedLabel?: string;
42
+ gdprText?: string;
43
+ fromFranceText?: string;
39
44
  }
40
45
 
41
46
  let {
@@ -54,13 +59,13 @@
54
59
  affiliate = true,
55
60
  recordVisit = true,
56
61
  center,
57
- max = false
62
+ max = false,
63
+ logoAltText = 'Logo',
64
+ copyEmailLabel = 'Copy email',
65
+ copiedLabel = 'Copied!',
66
+ gdprText = 'GDPR Compliant',
67
+ fromFranceText = 'From France'
58
68
  }: Props = $props();
59
-
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
69
  const mascotLogo = $derived(
65
70
  logo || (product ? `${instance}/api/public/logo/${product}.svg` : undefined)
66
71
  );
@@ -134,7 +139,7 @@
134
139
  <div class="mascot-wrap" use:onView={() => (mascotInView = true)}>
135
140
  <img
136
141
  src={mascotLogo}
137
- alt="{name} Logo"
142
+ alt="{name} {logoAltText}"
138
143
  width="100"
139
144
  height="100"
140
145
  class:in-view={mascotInView}
@@ -159,7 +164,7 @@
159
164
  <IconEnvelope size={14} />
160
165
  {email}
161
166
  </a>
162
- <Tooltip text={emailCopied ? 'Copied!' : 'Copy email'} position="top">
167
+ <Tooltip text={emailCopied ? copiedLabel : copyEmailLabel} position="top">
163
168
  <IconButton
164
169
  size="small"
165
170
  variant="invisible"
@@ -210,13 +215,13 @@
210
215
  </svg>
211
216
  <span class="lock"><IconLockFill size={10} /></span>
212
217
  </span>
213
- <span class="gdpr-chip-text">GDPR Compliant</span>
218
+ <span class="gdpr-chip-text">{gdprText}</span>
214
219
  </a>
215
220
  </div>
216
221
  {/if}
217
222
 
218
223
  <div class="bottom-right">
219
- <div class="france">From France <span class="flag">🇫🇷</span></div>
224
+ <div class="france">{fromFranceText} <span class="flag">🇫🇷</span></div>
220
225
  </div>
221
226
  </div>
222
227
  </Container>
@@ -17,6 +17,11 @@ interface Props {
17
17
  recordVisit?: boolean;
18
18
  center?: Snippet;
19
19
  max?: boolean;
20
+ logoAltText?: string;
21
+ copyEmailLabel?: string;
22
+ copiedLabel?: string;
23
+ gdprText?: string;
24
+ fromFranceText?: string;
20
25
  }
21
26
  declare const Footer: import("svelte").Component<Props, {}, "">;
22
27
  type Footer = ReturnType<typeof Footer>;
@@ -20,6 +20,8 @@
20
20
  center?: Snippet;
21
21
  end?: Snippet;
22
22
  max?: boolean;
23
+ logoAltText?: string;
24
+ menuLabel?: string;
23
25
  }
24
26
 
25
27
  let {
@@ -32,7 +34,9 @@
32
34
  darkToggle = true,
33
35
  center,
34
36
  end,
35
- max = false
37
+ max = false,
38
+ logoAltText = 'Logo',
39
+ menuLabel = 'Menu'
36
40
  }: Props = $props();
37
41
 
38
42
  let scrolled = $state(false);
@@ -61,7 +65,7 @@
61
65
  <a class="nav-brand" {href}>
62
66
  <img
63
67
  src={logo || `${instance}/api/public/logo/${product}.svg`}
64
- alt="{name + (subName ? ' ' + subName : '')} Logo"
68
+ alt="{name + (subName ? ' ' + subName : '')} {logoAltText}"
65
69
  width="26"
66
70
  height="26"
67
71
  />
@@ -93,7 +97,7 @@
93
97
  <span class="mobile-nav-wrap">
94
98
  <Dropdown bind:show={mobileOpen} align="end" width={300} contentPadding={8}>
95
99
  {#snippet trigger()}
96
- <IconButton variant="invisible" aria-label="Menu">
100
+ <IconButton variant="invisible" aria-label={menuLabel} aria-expanded={mobileOpen}>
97
101
  {#if mobileOpen}
98
102
  <IconX size={18} />
99
103
  {:else}
@@ -10,6 +10,8 @@ interface Props {
10
10
  center?: Snippet;
11
11
  end?: Snippet;
12
12
  max?: boolean;
13
+ logoAltText?: string;
14
+ menuLabel?: string;
13
15
  }
14
16
  declare const Header: import("svelte").Component<Props, {}, "">;
15
17
  type Header = ReturnType<typeof Header>;
@@ -0,0 +1,121 @@
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
+ /* a short divider, centered on the toggle, separating it from nav links,
84
+ rather than a full-height border-left */
85
+ .header-language-toggle::before {
86
+ content: '';
87
+ position: absolute;
88
+ left: 0;
89
+ top: 50%;
90
+ transform: translateY(-50%);
91
+ width: 1px;
92
+ height: 16px;
93
+ background: var(--border);
94
+ }
95
+
96
+ .flag {
97
+ font-size: 15px;
98
+ line-height: 1;
99
+ }
100
+
101
+ .menu {
102
+ display: flex;
103
+ flex-direction: column;
104
+ gap: 2px;
105
+ }
106
+
107
+ .lang-row {
108
+ display: flex;
109
+ align-items: center;
110
+ gap: 8px;
111
+ width: 100%;
112
+ }
113
+
114
+ .code {
115
+ margin-left: auto;
116
+ padding-left: 8px;
117
+ font-size: 11px;
118
+ font-weight: 600;
119
+ color: var(--text-light);
120
+ }
121
+ </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,18 @@
1
+ export interface LanguageOption {
2
+ /** language/locale code, e.g. "en", "fr" */
3
+ code: string;
4
+ /** an emoji flag shown next to the name */
5
+ flag: string;
6
+ name: string;
7
+ }
8
+ /**
9
+ * Rewrites `path` (the current pathname) from `currentLang` to `targetLang`,
10
+ * for the common convention where the default language is served without a
11
+ * prefix and every other language is served under `/{code}`.
12
+ *
13
+ * @example
14
+ * buildLocalizedUrl('/pricing', 'en', 'fr', 'en') // '/fr/pricing'
15
+ * buildLocalizedUrl('/fr/pricing', 'fr', 'en', 'en') // '/pricing'
16
+ * buildLocalizedUrl('/fr', 'fr', 'en', 'en') // '/'
17
+ */
18
+ export declare function buildLocalizedUrl(path: string, currentLang: string, targetLang: string, defaultLang: string): string;
@@ -0,0 +1,35 @@
1
+ // Shared helpers for marketing sites that serve translated pages on their own
2
+ // URLs (e.g. a SvelteKit `[[lang]]` route param) rather than switching
3
+ // language client-side on one page. This is a different case from
4
+ // `InternationalizationService`/`LanguageToggle` in
5
+ // `$lib/components/Internationalization`, which swap translated strings in
6
+ // place on a single page; use that instead if you don't have per-language
7
+ // routes.
8
+ /**
9
+ * Rewrites `path` (the current pathname) from `currentLang` to `targetLang`,
10
+ * for the common convention where the default language is served without a
11
+ * prefix and every other language is served under `/{code}`.
12
+ *
13
+ * @example
14
+ * buildLocalizedUrl('/pricing', 'en', 'fr', 'en') // '/fr/pricing'
15
+ * buildLocalizedUrl('/fr/pricing', 'fr', 'en', 'en') // '/pricing'
16
+ * buildLocalizedUrl('/fr', 'fr', 'en', 'en') // '/'
17
+ */
18
+ export function buildLocalizedUrl(path, currentLang, targetLang, defaultLang) {
19
+ let basePath = path;
20
+ // strip the current language prefix, if any, to get the language-agnostic path
21
+ if (currentLang !== defaultLang) {
22
+ const prefix = `/${currentLang}`;
23
+ if (basePath === prefix) {
24
+ basePath = '/';
25
+ }
26
+ else if (basePath.startsWith(`${prefix}/`)) {
27
+ basePath = basePath.slice(prefix.length);
28
+ }
29
+ }
30
+ // the default language is served without a prefix
31
+ if (targetLang === defaultLang) {
32
+ return basePath;
33
+ }
34
+ return basePath === '/' ? `/${targetLang}` : `/${targetLang}${basePath}`;
35
+ }
@@ -1,6 +1,9 @@
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 HeaderLanguageToggle } from './Header/HeaderLanguageToggle.svelte';
5
+ export { buildLocalizedUrl } from './Header/language.js';
6
+ export type { LanguageOption } from './Header/language.js';
4
7
  export { default as Container } from './Container/Container.svelte';
5
8
  export { default as Docs } from './Docs/Docs.svelte';
6
9
  export { default as DocsImage } from './Docs/DocsImage.svelte';
@@ -1,6 +1,8 @@
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 HeaderLanguageToggle } from './Header/HeaderLanguageToggle.svelte';
5
+ export { buildLocalizedUrl } from './Header/language.js';
4
6
  export { default as Container } from './Container/Container.svelte';
5
7
  export { default as Docs } from './Docs/Docs.svelte';
6
8
  export { default as DocsImage } from './Docs/DocsImage.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyvor/design",
3
- "version": "2.1.8-beta.1",
3
+ "version": "2.1.8-beta.2",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "repository": {