@hyvor/design 2.1.14 → 2.1.16

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.
@@ -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%;
@@ -1,5 +1,6 @@
1
1
  interface Props {
2
2
  column?: boolean;
3
+ divider?: boolean;
3
4
  children?: import('svelte').Snippet;
4
5
  }
5
6
  declare const ButtonGroup: import("svelte").Component<Props, {}, "">;
@@ -9,7 +9,8 @@
9
9
 
10
10
  let { label, content, children }: Props = $props();
11
11
 
12
- const labelId = `detail-card-${Math.random().toString(36).substring(2, 15)}`;
12
+ const uid = $props.id();
13
+ const labelId = `detail-card-${uid}`;
13
14
  </script>
14
15
 
15
16
  <div class="detail-card" role="region" aria-labelledby={labelId}>
@@ -46,13 +46,10 @@
46
46
 
47
47
  if (position === 'bottom') {
48
48
  contentWrap.style.top = triggerRect.bottom + GAP + 'px';
49
- if (contentRect.height + triggerRect.bottom > window.innerHeight) {
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 = triggerRect.top - contentRect.height - GAP + 'px';
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>
@@ -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
- display: none;
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: flex;
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: 150px;
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
- <div class="mobile-divider"></div>
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 once a language link inside it is clicked
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
- <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>
65
+ {#if mobile.current}
66
+ <HeaderMobileSection {label}>
67
+ {#snippet trailing()}
68
+ <span class="flag">{currentLanguage.flag}</span>
70
69
  {/snippet}
71
- </Dropdown>
72
- </div>
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;
@@ -0,0 +1,2 @@
1
+ // width (px) at/below which the header collapses into the hamburger menu
2
+ export const HEADER_MOBILE_BREAKPOINT = 992;