@hyvor/design 2.1.8 → 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.
Files changed (59) hide show
  1. package/dist/cloud/HyvorBar/BarNotice/BarNotice.svelte +1 -1
  2. package/dist/cloud/HyvorBar/BarSupport.svelte +1 -1
  3. package/dist/components/ButtonGroup/ButtonGroup.svelte +82 -0
  4. package/dist/components/{Button → ButtonGroup}/ButtonGroup.svelte.d.ts +1 -0
  5. package/dist/components/ColorPicker/ColorPicker.svelte +68 -9
  6. package/dist/components/ColorPicker/ColorPicker.svelte.d.ts +3 -0
  7. package/dist/components/Modal/ConfirmModalProvider.svelte +6 -9
  8. package/dist/components/Modal/ModalFooter.svelte +24 -27
  9. package/dist/components/Toast/ToastMessage.svelte +3 -6
  10. package/dist/components/Toast/ToastProvider.svelte +8 -4
  11. package/dist/components/index.d.ts +1 -1
  12. package/dist/components/index.js +1 -1
  13. package/dist/index.css +4 -0
  14. package/dist/marketing/AllFeaturesAccordion/AllFeaturesAccordion.svelte +251 -0
  15. package/dist/marketing/AllFeaturesAccordion/AllFeaturesAccordion.svelte.d.ts +23 -0
  16. package/dist/marketing/AllFeaturesAccordion/AllFeaturesAccordionFeature.svelte +70 -0
  17. package/dist/marketing/AllFeaturesAccordion/AllFeaturesAccordionFeature.svelte.d.ts +10 -0
  18. package/dist/marketing/FAQ/FAQ.svelte +227 -0
  19. package/dist/marketing/FAQ/FAQ.svelte.d.ts +13 -0
  20. package/dist/marketing/FeatureSplit/FeatureSplit.svelte +325 -0
  21. package/dist/marketing/FeatureSplit/FeatureSplit.svelte.d.ts +24 -0
  22. package/dist/marketing/Footer/Footer.svelte +395 -98
  23. package/dist/marketing/Footer/Footer.svelte.d.ts +18 -2
  24. package/dist/marketing/Footer/FooterLinkList.svelte +10 -3
  25. package/dist/marketing/FullTrialSignup/FullTrialSignup.svelte +164 -0
  26. package/dist/marketing/FullTrialSignup/FullTrialSignup.svelte.d.ts +18 -0
  27. package/dist/marketing/Header/Header.svelte +72 -28
  28. package/dist/marketing/Header/Header.svelte.d.ts +5 -2
  29. package/dist/marketing/Header/HeaderLanguageToggle.svelte +119 -0
  30. package/dist/marketing/Header/HeaderLanguageToggle.svelte.d.ts +14 -0
  31. package/dist/marketing/Header/HeaderNavLink.svelte +149 -0
  32. package/dist/marketing/Header/HeaderNavLink.svelte.d.ts +18 -0
  33. package/dist/marketing/Header/language.d.ts +16 -0
  34. package/dist/marketing/Header/language.js +28 -0
  35. package/dist/marketing/Hero/Hero.svelte +349 -0
  36. package/dist/marketing/Hero/Hero.svelte.d.ts +16 -0
  37. package/dist/marketing/LogoStrip/LogoStrip.svelte +199 -0
  38. package/dist/marketing/LogoStrip/LogoStrip.svelte.d.ts +19 -0
  39. package/dist/marketing/Seal/CcpaSeal.svelte +29 -0
  40. package/dist/marketing/Seal/CcpaSeal.svelte.d.ts +6 -0
  41. package/dist/marketing/Seal/GdprSeal.svelte +60 -0
  42. package/dist/marketing/Seal/GdprSeal.svelte.d.ts +6 -0
  43. package/dist/marketing/Seal/IsoSeal.svelte +44 -0
  44. package/dist/marketing/Seal/IsoSeal.svelte.d.ts +6 -0
  45. package/dist/marketing/Seal/Seal.svelte +62 -0
  46. package/dist/marketing/Seal/Seal.svelte.d.ts +8 -0
  47. package/dist/marketing/Seal/SsoSeal.svelte +31 -0
  48. package/dist/marketing/Seal/SsoSeal.svelte.d.ts +6 -0
  49. package/dist/marketing/Seal/ccpa.svg +131 -0
  50. package/dist/marketing/SpotlightSplit/SpotlightSplit.svelte +256 -0
  51. package/dist/marketing/SpotlightSplit/SpotlightSplit.svelte.d.ts +19 -0
  52. package/dist/marketing/Testimonials/Testimonials.svelte +441 -0
  53. package/dist/marketing/Testimonials/Testimonials.svelte.d.ts +24 -0
  54. package/dist/marketing/index.d.ts +20 -2
  55. package/dist/marketing/index.js +26 -2
  56. package/dist/marketing/social.d.ts +1 -1
  57. package/dist/marketing/social.js +1 -1
  58. package/package.json +1 -1
  59. package/dist/components/Button/ButtonGroup.svelte +0 -18
@@ -12,7 +12,7 @@
12
12
  text="You have an unpaid invoice. Please settle to avoid service interruption."
13
13
  position="bottom"
14
14
  >
15
- <a href={instance + '/account/billing?pay-failed'}>
15
+ <a href={instance + '/account/org/billing?pay-failed'}>
16
16
  <Tag color="orange">
17
17
  {#snippet start()}
18
18
  <IconExclamationCircle size={14} />
@@ -111,7 +111,7 @@
111
111
  {/snippet}
112
112
  </ActionListItem>
113
113
  </a>
114
- <a href={SOCIAL_LINKS.blueksy} target="_blank">
114
+ <a href={SOCIAL_LINKS.bluesky} target="_blank">
115
115
  <ActionListItem>
116
116
  Bluesky
117
117
  {#snippet start()}
@@ -0,0 +1,82 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ column?: boolean;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { column = false, children }: Props = $props();
8
+ </script>
9
+
10
+ <div class="button-group" class:column>
11
+ {@render children?.()}
12
+ </div>
13
+
14
+ <style>.button-group {
15
+ display: inline-flex;
16
+ }
17
+
18
+ .button-group.column {
19
+ flex-direction: column;
20
+ align-items: stretch;
21
+ }
22
+
23
+ .button-group :global(> .button) {
24
+ position: relative;
25
+ }
26
+
27
+ .button-group :global(> .button:hover),
28
+ .button-group :global(> .button:focus-visible) {
29
+ z-index: 1;
30
+ }
31
+
32
+ .button-group:not(.column) :global(> .button:not(:first-child)) {
33
+ border-top-left-radius: 0;
34
+ border-bottom-left-radius: 0;
35
+ }
36
+
37
+ .button-group:not(.column) :global(> .button:not(:last-child)) {
38
+ border-top-right-radius: 0;
39
+ border-bottom-right-radius: 0;
40
+ }
41
+
42
+ .button-group.column :global(> .button:not(:first-child)) {
43
+ border-top-left-radius: 0;
44
+ border-top-right-radius: 0;
45
+ }
46
+
47
+ .button-group.column :global(> .button:not(:last-child)) {
48
+ border-bottom-left-radius: 0;
49
+ border-bottom-right-radius: 0;
50
+ }
51
+
52
+ .button-group:not(.column) :global(> .button:not(:first-child)) {
53
+ margin-left: -1px;
54
+ }
55
+
56
+ .button-group.column :global(> .button:not(:first-child)) {
57
+ margin-top: -1px;
58
+ }
59
+
60
+ .button-group:not(.column) :global(> .button:not(.outline):not(.outline-fill):not(:first-child))::before {
61
+ content: "";
62
+ position: absolute;
63
+ top: 22%;
64
+ bottom: 22%;
65
+ left: 0;
66
+ width: 1px;
67
+ background-color: currentColor;
68
+ opacity: 0.25;
69
+ pointer-events: none;
70
+ }
71
+
72
+ .button-group.column :global(> .button:not(.outline):not(.outline-fill):not(:first-child))::before {
73
+ content: "";
74
+ position: absolute;
75
+ left: 22%;
76
+ right: 22%;
77
+ top: 0;
78
+ height: 1px;
79
+ background-color: currentColor;
80
+ opacity: 0.25;
81
+ pointer-events: none;
82
+ }</style>
@@ -1,4 +1,5 @@
1
1
  interface Props {
2
+ column?: boolean;
2
3
  children?: import('svelte').Snippet;
3
4
  }
4
5
  declare const ButtonGroup: import("svelte").Component<Props, {}, "">;
@@ -2,6 +2,7 @@
2
2
  import ColorPicker from 'svelte-awesome-color-picker';
3
3
  import { clickOutside } from '../index.js';
4
4
  import { createEventDispatcher } from 'svelte';
5
+ import type { DropdownAlign, DropdownPosition } from '../Dropdown/dropdown.types.js';
5
6
 
6
7
  interface Props {
7
8
  color?: string;
@@ -9,6 +10,8 @@
9
10
  show?: boolean;
10
11
  'aria-label'?: string;
11
12
  alpha?: boolean;
13
+ align?: DropdownAlign;
14
+ position?: DropdownPosition;
12
15
  oninput?: (color: string) => void;
13
16
  onchange?: (color: string) => void;
14
17
  }
@@ -19,6 +22,8 @@
19
22
  show = $bindable(false),
20
23
  'aria-label': ariaLabel = '',
21
24
  alpha = false,
25
+ align = 'start',
26
+ position = 'bottom',
22
27
  oninput,
23
28
  onchange
24
29
  }: Props = $props();
@@ -29,25 +34,79 @@
29
34
  }>();
30
35
 
31
36
  let buttonElement: HTMLButtonElement;
37
+ let wrapElement: HTMLElement | undefined = $state();
32
38
  let pickerTop = $state(0);
33
39
  let pickerLeft = $state(0);
34
40
 
41
+ const GAP = 5;
42
+ const SPACE_AROUND = 15;
43
+
35
44
  function updatePosition() {
36
- if (buttonElement) {
37
- const rect = buttonElement.getBoundingClientRect();
38
- pickerLeft = rect.right;
39
- pickerTop = rect.bottom;
45
+ if (!buttonElement || !wrapElement) return;
46
+
47
+ const triggerRect = buttonElement.getBoundingClientRect();
48
+ const contentRect = wrapElement.getBoundingClientRect();
49
+ const documentWidth = document.documentElement.clientWidth;
50
+ const width = contentRect.width;
51
+ const height = contentRect.height;
52
+
53
+ let top = triggerRect.bottom + GAP;
54
+ let left = triggerRect.left;
55
+
56
+ if (position === 'bottom') {
57
+ top = triggerRect.bottom + GAP;
58
+ } else if (position === 'top') {
59
+ top = triggerRect.top - height - GAP;
60
+ } else if (position === 'left') {
61
+ left = triggerRect.left - width - GAP;
62
+ } else if (position === 'right') {
63
+ left = triggerRect.right + GAP;
64
+ }
65
+
66
+ if (position === 'bottom' || position === 'top') {
67
+ if (align === 'start') {
68
+ left = Math.min(triggerRect.left, documentWidth - width - SPACE_AROUND);
69
+ } else if (align === 'center') {
70
+ left = triggerRect.left + triggerRect.width / 2 - width / 2;
71
+ } else if (align === 'end') {
72
+ left = triggerRect.right - width;
73
+ }
74
+ left = Math.max(left, SPACE_AROUND);
75
+ } else {
76
+ if (align === 'start') {
77
+ top = triggerRect.top;
78
+ } else if (align === 'center') {
79
+ top = triggerRect.top + triggerRect.height / 2 - height / 2;
80
+ } else if (align === 'end') {
81
+ top = triggerRect.bottom - height;
82
+ }
83
+ top = Math.max(top, SPACE_AROUND);
40
84
  }
85
+
86
+ pickerTop = top;
87
+ pickerLeft = left;
41
88
  }
42
89
 
43
90
  $effect(() => {
44
- if (show) {
91
+ // rerun when positioning inputs change
92
+ position;
93
+ align;
94
+ show;
95
+
96
+ if (show && wrapElement) {
45
97
  updatePosition();
46
98
  window.addEventListener('scroll', updatePosition, true);
47
99
  window.addEventListener('resize', updatePosition);
48
- } else {
49
- window.removeEventListener('scroll', updatePosition, true);
50
- window.removeEventListener('resize', updatePosition);
100
+
101
+ // the color picker widget can change size after it mounts
102
+ const observer = new MutationObserver(updatePosition);
103
+ observer.observe(wrapElement, { subtree: true, childList: true });
104
+
105
+ return () => {
106
+ observer.disconnect();
107
+ window.removeEventListener('scroll', updatePosition, true);
108
+ window.removeEventListener('resize', updatePosition);
109
+ };
51
110
  }
52
111
 
53
112
  return () => {
@@ -87,6 +146,7 @@
87
146
  {#if show}
88
147
  <div
89
148
  class="color-picker-wrap"
149
+ bind:this={wrapElement}
90
150
  style:top="{pickerTop}px"
91
151
  style:left="{pickerLeft}px"
92
152
  use:clickOutside={{
@@ -114,7 +174,6 @@
114
174
  }
115
175
  div {
116
176
  position: fixed;
117
- width: 0;
118
177
  z-index: 1000;
119
178
  }
120
179
  </style>
@@ -1,10 +1,13 @@
1
1
  import ColorPicker from 'svelte-awesome-color-picker';
2
+ import type { DropdownAlign, DropdownPosition } from '../Dropdown/dropdown.types.js';
2
3
  interface Props {
3
4
  color?: string;
4
5
  size?: number;
5
6
  show?: boolean;
6
7
  'aria-label'?: string;
7
8
  alpha?: boolean;
9
+ align?: DropdownAlign;
10
+ position?: DropdownPosition;
8
11
  oninput?: (color: string) => void;
9
12
  onchange?: (color: string) => void;
10
13
  }
@@ -2,7 +2,6 @@
2
2
  import Modal from './Modal.svelte';
3
3
  import { confirmStore } from './confirm.js';
4
4
  import Button from './../Button/Button.svelte';
5
- import ButtonGroup from './../Button/ButtonGroup.svelte';
6
5
 
7
6
  let show = $state(true);
8
7
 
@@ -32,14 +31,12 @@
32
31
  {/if}
33
32
 
34
33
  {#snippet footer()}
35
- <ButtonGroup>
36
- <Button variant="invisible" on:click={handleCancel}>
37
- {$confirmStore.cancelText || 'Cancel'}
38
- </Button>
39
- <Button color={$confirmStore.danger ? 'red' : 'accent'} on:click={handleConfirm}>
40
- {$confirmStore.confirmText || 'Confirm'}
41
- </Button>
42
- </ButtonGroup>
34
+ <Button variant="invisible" on:click={handleCancel}>
35
+ {$confirmStore.cancelText || 'Cancel'}
36
+ </Button>
37
+ <Button color={$confirmStore.danger ? 'red' : 'accent'} on:click={handleConfirm}>
38
+ {$confirmStore.confirmText || 'Confirm'}
39
+ </Button>
43
40
  {/snippet}
44
41
  </Modal>
45
42
  {/if}
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher } from 'svelte';
3
3
  import Button from '../Button/Button.svelte';
4
- import ButtonGroup from '../Button/ButtonGroup.svelte';
5
4
  import type { Footer } from './modal-types.ts';
6
5
 
7
6
  interface Props {
@@ -14,30 +13,28 @@
14
13
  const dispatch = createEventDispatcher();
15
14
  </script>
16
15
 
17
- <ButtonGroup>
18
- {#if footer.cancel !== false}
19
- <Button
20
- variant="invisible"
21
- on:click={() => {
22
- show = false;
23
- dispatch('cancel');
24
- }}
25
- {...footer.cancel?.props}
26
- disabled={footer.cancel?.disabled}
27
- >
28
- {footer.cancel?.text || 'Cancel'}
29
- </Button>
30
- {/if}
16
+ {#if footer.cancel !== false}
17
+ <Button
18
+ variant="invisible"
19
+ on:click={() => {
20
+ show = false;
21
+ dispatch('cancel');
22
+ }}
23
+ {...footer.cancel?.props}
24
+ disabled={footer.cancel?.disabled}
25
+ >
26
+ {footer.cancel?.text || 'Cancel'}
27
+ </Button>
28
+ {/if}
31
29
 
32
- {#if footer.confirm !== false}
33
- <Button
34
- variant="fill"
35
- color={footer.confirm?.danger ? 'red' : 'accent'}
36
- on:click={() => dispatch('confirm')}
37
- {...footer.confirm?.props}
38
- disabled={footer.confirm?.disabled}
39
- >
40
- {footer.confirm?.text || 'Confirm'}
41
- </Button>
42
- {/if}
43
- </ButtonGroup>
30
+ {#if footer.confirm !== false}
31
+ <Button
32
+ variant="fill"
33
+ color={footer.confirm?.danger ? 'red' : 'accent'}
34
+ on:click={() => dispatch('confirm')}
35
+ {...footer.confirm?.props}
36
+ disabled={footer.confirm?.disabled}
37
+ >
38
+ {footer.confirm?.text || 'Confirm'}
39
+ </Button>
40
+ {/if}
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import ToastIcon from './ToastIcon.svelte';
3
3
  import type { Toast } from './toast.ts';
4
- import { fade } from 'svelte/transition';
4
+ import { fade, fly } from 'svelte/transition';
5
+ import { cubicOut } from 'svelte/easing';
5
6
 
6
7
  interface Props {
7
8
  toast: Toast;
@@ -10,7 +11,7 @@
10
11
  let { toast }: Props = $props();
11
12
  </script>
12
13
 
13
- <div class="toast" out:fade={{ duration: 200 }} in:fade={{ duration: 50 }}>
14
+ <div class="toast" in:fly={{ y: 20, duration: 250, easing: cubicOut }} out:fade={{ duration: 150 }}>
14
15
  {#if toast.type !== 'blank'}
15
16
  <div class="icon-wrap">
16
17
  <ToastIcon {toast} />
@@ -28,7 +29,6 @@
28
29
 
29
30
  <style>
30
31
  .toast {
31
- margin-bottom: 10px;
32
32
  box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
33
33
  border-radius: var(--box-radius);
34
34
  background-color: var(--box-background);
@@ -38,9 +38,6 @@
38
38
  max-width: 350px;
39
39
  pointer-events: initial;
40
40
  }
41
- .toast:first-child {
42
- margin-top: 10px;
43
- }
44
41
  .icon-wrap {
45
42
  margin-right: 8px;
46
43
  display: inline-flex;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { flip } from 'svelte/animate';
2
3
  import ToastMessage from './ToastMessage.svelte';
3
4
  import { toastStore } from './toast.js';
4
5
  import { useCleaner } from './cleaner.js';
@@ -8,21 +9,24 @@
8
9
 
9
10
  <div id="toasts-wrap">
10
11
  {#each $toastStore as toast (toast.id)}
11
- <ToastMessage {toast} />
12
+ <div class="toast-item" animate:flip={{ duration: 200 }}>
13
+ <ToastMessage {toast} />
14
+ </div>
12
15
  {/each}
13
16
  </div>
14
17
 
15
18
  <style>
16
19
  #toasts-wrap {
17
20
  position: fixed;
18
- top: 0;
21
+ bottom: 0;
19
22
  left: 0;
20
23
  width: 100%;
21
24
  z-index: 20000000;
22
25
  display: flex;
23
- flex-direction: column;
26
+ flex-direction: column-reverse;
24
27
  align-items: center;
25
- justify-content: center;
28
+ gap: 10px;
29
+ padding: 10px 0 24px;
26
30
  pointer-events: none;
27
31
  }
28
32
  </style>
@@ -7,7 +7,7 @@ export { default as AvatarStack } from './Avatar/AvatarStack.svelte';
7
7
  export { default as Base } from './Base/Base.svelte';
8
8
  export { default as Box } from './Box/Box.svelte';
9
9
  export { default as Button } from './Button/Button.svelte';
10
- export { default as ButtonGroup } from './Button/ButtonGroup.svelte';
10
+ export { default as ButtonGroup } from './ButtonGroup/ButtonGroup.svelte';
11
11
  export { default as BoxShadowPicker } from './BoxShadowPicker/BoxShadowPicker.svelte';
12
12
  export { default as Callout } from './Callout/Callout.svelte';
13
13
  export { default as Checkbox } from './Checkbox/Checkbox.svelte';
@@ -7,7 +7,7 @@ export { default as AvatarStack } from './Avatar/AvatarStack.svelte';
7
7
  export { default as Base } from './Base/Base.svelte';
8
8
  export { default as Box } from './Box/Box.svelte';
9
9
  export { default as Button } from './Button/Button.svelte';
10
- export { default as ButtonGroup } from './Button/ButtonGroup.svelte';
10
+ export { default as ButtonGroup } from './ButtonGroup/ButtonGroup.svelte';
11
11
  export { default as BoxShadowPicker } from './BoxShadowPicker/BoxShadowPicker.svelte';
12
12
  export { default as Callout } from './Callout/Callout.svelte';
13
13
  export { default as Checkbox } from './Checkbox/Checkbox.svelte';
package/dist/index.css CHANGED
@@ -95,3 +95,7 @@ a {
95
95
  font-weight: var(--shiki-dark-font-weight) !important;
96
96
  text-decoration: var(--shiki-dark-text-decoration) !important;
97
97
  }
98
+
99
+ ::selection {
100
+ background-color: var(--selection, var(--accent-light));
101
+ }
@@ -0,0 +1,251 @@
1
+ <script lang="ts">
2
+ import type { Component } from 'svelte';
3
+ import Feature from './AllFeaturesAccordionFeature.svelte';
4
+ import IconChevronDown from '@hyvor/icons/IconChevronDown';
5
+
6
+ interface FeatureItem {
7
+ icon: Component;
8
+ title: string;
9
+ description: string;
10
+ }
11
+
12
+ interface Category {
13
+ label: string;
14
+ icon: Component;
15
+ color?: string;
16
+ features: FeatureItem[];
17
+ }
18
+
19
+ interface Props {
20
+ title?: string;
21
+ description?: string;
22
+ categories: Category[];
23
+ defaultOpenIndex?: number;
24
+ accentColor?: string;
25
+ accentColorDark?: string;
26
+ }
27
+
28
+ let {
29
+ title = "And There's More...",
30
+ description = '',
31
+ categories,
32
+ defaultOpenIndex = 0,
33
+ accentColor,
34
+ accentColorDark
35
+ }: Props = $props();
36
+
37
+ let openIndex = $state(defaultOpenIndex);
38
+
39
+ function toggle(i: number) {
40
+ openIndex = openIndex === i ? -1 : i;
41
+ }
42
+ </script>
43
+
44
+ <section
45
+ class="hds-all-features-accordion hds-container"
46
+ style:--afa-accent={accentColor}
47
+ style:--afa-accent-dark={accentColorDark}
48
+ >
49
+ <div class="section-header">
50
+ <h2>{title}</h2>
51
+ {#if description}
52
+ <p>{description}</p>
53
+ {/if}
54
+ </div>
55
+
56
+ <div class="accordion">
57
+ {#each categories as cat, i}
58
+ {@const CatIcon = cat.icon}
59
+ {@const catColor = cat.color || 'var(--accent)'}
60
+ <div class="accordion-item" class:open={openIndex === i}>
61
+ <h3 class="accordion-heading">
62
+ <button
63
+ class="accordion-trigger"
64
+ id="hds-accordion-trigger-{i}"
65
+ onclick={() => toggle(i)}
66
+ aria-expanded={openIndex === i}
67
+ aria-controls="hds-accordion-panel-{i}"
68
+ style="--cat-color: {catColor}"
69
+ >
70
+ <span class="cat-icon" aria-hidden="true">
71
+ <CatIcon size={18} />
72
+ </span>
73
+ <span class="cat-label">{cat.label}</span>
74
+ <span class="cat-count">{cat.features.length} features</span>
75
+ <span class="chevron" class:rotated={openIndex === i} aria-hidden="true">
76
+ <IconChevronDown size={14} />
77
+ </span>
78
+ </button>
79
+ </h3>
80
+
81
+ <div
82
+ class="accordion-body"
83
+ class:open={openIndex === i}
84
+ role="region"
85
+ id="hds-accordion-panel-{i}"
86
+ aria-labelledby="hds-accordion-trigger-{i}"
87
+ >
88
+ <div class="accordion-body-inner">
89
+ <div class="accordion-content">
90
+ <ul class="features-grid">
91
+ {#each cat.features as feat}
92
+ <Feature
93
+ icon={feat.icon}
94
+ title={feat.title}
95
+ description={feat.description}
96
+ color={catColor}
97
+ />
98
+ {/each}
99
+ </ul>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ {/each}
105
+ </div>
106
+ </section>
107
+
108
+ <style>
109
+ .hds-all-features-accordion {
110
+ padding: 80px 0;
111
+ }
112
+
113
+ .section-header {
114
+ text-align: center;
115
+ margin-bottom: 48px;
116
+ }
117
+
118
+ .section-header h2 {
119
+ font-size: 32px;
120
+ font-weight: 700;
121
+ margin: 0 0 6px;
122
+ font-family: var(--font-serif);
123
+ }
124
+
125
+ .section-header p {
126
+ font-size: 1rem;
127
+ color: var(--text-light);
128
+ margin: 0;
129
+ }
130
+
131
+ .accordion {
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 4px;
135
+ }
136
+
137
+ .accordion-item {
138
+ border-radius: 16px;
139
+ transition: background 0.2s ease;
140
+ }
141
+
142
+ .accordion-item.open {
143
+ background: color-mix(in srgb, var(--afa-accent, var(--accent)) 6%, transparent);
144
+ }
145
+
146
+ :global(:root.dark) .accordion-item.open {
147
+ background: color-mix(
148
+ in srgb,
149
+ var(--afa-accent-dark, var(--afa-accent, var(--accent))) 6%,
150
+ transparent
151
+ );
152
+ }
153
+
154
+ .accordion-heading {
155
+ margin: 0;
156
+ font-size: inherit;
157
+ font-weight: inherit;
158
+ }
159
+
160
+ .accordion-trigger {
161
+ width: 100%;
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 14px;
165
+ padding: 18px 20px;
166
+ background: none;
167
+ border: none;
168
+ border-radius: inherit;
169
+ cursor: pointer;
170
+ text-align: left;
171
+ transition: background 0.15s;
172
+ color: var(--text);
173
+ }
174
+
175
+ .accordion-item:not(.open) .accordion-trigger:hover {
176
+ background: var(--hover);
177
+ }
178
+
179
+ .cat-icon {
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ width: 34px;
184
+ height: 34px;
185
+ border-radius: 10px;
186
+ color: var(--cat-color);
187
+ background: color-mix(in srgb, var(--cat-color) 16%, transparent);
188
+ flex-shrink: 0;
189
+ }
190
+
191
+ .cat-label {
192
+ font-size: 16px;
193
+ font-weight: 600;
194
+ flex: 1;
195
+ }
196
+
197
+ .cat-count {
198
+ font-size: 13px;
199
+ color: var(--text-light);
200
+ }
201
+
202
+ .chevron {
203
+ display: flex;
204
+ align-items: center;
205
+ color: var(--text-light);
206
+ transition: transform 0.2s;
207
+ }
208
+
209
+ .chevron.rotated {
210
+ transform: rotate(180deg);
211
+ }
212
+
213
+ /* CSS grid row trick: content stays in DOM for SEO, height animates via grid */
214
+ .accordion-body {
215
+ display: grid;
216
+ grid-template-rows: 0fr;
217
+ transition: grid-template-rows 0.22s ease;
218
+ }
219
+
220
+ .accordion-body.open {
221
+ grid-template-rows: 1fr;
222
+ }
223
+
224
+ .accordion-body-inner {
225
+ overflow: hidden;
226
+ }
227
+
228
+ .accordion-content {
229
+ padding: 8px 20px 28px;
230
+ }
231
+
232
+ .features-grid {
233
+ display: grid;
234
+ grid-template-columns: repeat(3, 1fr);
235
+ gap: 28px 20px;
236
+ margin: 0;
237
+ padding: 0;
238
+ }
239
+
240
+ /* Override AllFeaturesAccordionFeature's width since we're using grid now */
241
+ /* TODO */
242
+ .features-grid :global(.feature) {
243
+ width: auto;
244
+ }
245
+
246
+ @media (max-width: 768px) {
247
+ .features-grid {
248
+ grid-template-columns: 1fr;
249
+ }
250
+ }
251
+ </style>