@aspect-ops/exon-ui 0.3.0 → 0.4.0

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 (36) hide show
  1. package/README.md +0 -45
  2. package/dist/components/BottomNav/BottomNavItem.svelte +4 -3
  3. package/dist/components/BottomNav/BottomNavItem.svelte.d.ts +2 -1
  4. package/dist/components/DoughnutChart/DoughnutChart.svelte +4 -4
  5. package/dist/components/FlexibleGrid/FlexibleGrid.svelte +118 -0
  6. package/dist/components/FlexibleGrid/FlexibleGrid.svelte.d.ts +7 -0
  7. package/dist/components/FlexibleGrid/README.md +212 -0
  8. package/dist/components/FlexibleGrid/index.d.ts +2 -0
  9. package/dist/components/FlexibleGrid/index.js +1 -0
  10. package/dist/components/GlobalHeader/GlobalHeader.svelte +201 -87
  11. package/dist/components/HeroLeftAligned/HeroLeftAligned.svelte +182 -0
  12. package/dist/components/HeroLeftAligned/HeroLeftAligned.svelte.d.ts +8 -0
  13. package/dist/components/HeroLeftAligned/README.md +168 -0
  14. package/dist/components/HeroLeftAligned/index.d.ts +2 -0
  15. package/dist/components/HeroLeftAligned/index.js +1 -0
  16. package/dist/components/IconFeatureCard/IconFeatureCard.svelte +173 -0
  17. package/dist/components/IconFeatureCard/IconFeatureCard.svelte.d.ts +4 -0
  18. package/dist/components/IconFeatureCard/README.md +234 -0
  19. package/dist/components/IconFeatureCard/index.d.ts +2 -0
  20. package/dist/components/IconFeatureCard/index.js +1 -0
  21. package/dist/components/ImageTextCard/ImageTextCard.svelte +149 -0
  22. package/dist/components/ImageTextCard/ImageTextCard.svelte.d.ts +22 -0
  23. package/dist/components/ImageTextCard/README.md +177 -0
  24. package/dist/components/ImageTextCard/index.d.ts +2 -0
  25. package/dist/components/ImageTextCard/index.js +1 -0
  26. package/dist/components/Sidebar/SidebarGroup.svelte +1 -4
  27. package/dist/index.d.ts +7 -1
  28. package/dist/index.js +4 -1
  29. package/dist/types/index.d.ts +49 -1
  30. package/dist/types/layout.d.ts +20 -0
  31. package/package.json +9 -2
  32. package/dist/components/Mermaid/Mermaid.svelte +0 -320
  33. package/dist/components/Mermaid/Mermaid.svelte.d.ts +0 -38
  34. package/dist/components/Mermaid/index.d.ts +0 -1
  35. package/dist/components/Mermaid/index.js +0 -1
  36. package/dist/components/Mermaid/mermaid.d.ts +0 -21
@@ -42,22 +42,27 @@
42
42
 
43
43
  let mobileMenuOpen = $state(false);
44
44
  let openDropdown = $state<number | null>(null);
45
+ let openMobileSubmenu = $state<number | null>(null);
45
46
  let isScrollingUp = $state(true);
46
47
  let lastScrollY = $state(0);
47
48
  let headerElement: HTMLElement | null = $state(null);
48
49
 
49
50
  function toggleMobileMenu() {
50
51
  mobileMenuOpen = !mobileMenuOpen;
51
- if (mobileMenuOpen) {
52
- document.body.style.overflow = 'hidden';
53
- } else {
54
- document.body.style.overflow = '';
52
+ if (typeof document !== 'undefined') {
53
+ if (mobileMenuOpen) {
54
+ document.body.style.overflow = 'hidden';
55
+ } else {
56
+ document.body.style.overflow = '';
57
+ }
55
58
  }
56
59
  }
57
60
 
58
61
  function closeMobileMenu() {
59
62
  mobileMenuOpen = false;
60
- document.body.style.overflow = '';
63
+ if (typeof document !== 'undefined') {
64
+ document.body.style.overflow = '';
65
+ }
61
66
  }
62
67
 
63
68
  function toggleDropdown(index: number) {
@@ -68,6 +73,10 @@
68
73
  openDropdown = null;
69
74
  }
70
75
 
76
+ function toggleMobileSubmenu(index: number) {
77
+ openMobileSubmenu = openMobileSubmenu === index ? null : index;
78
+ }
79
+
71
80
  function handleKeydown(event: KeyboardEvent) {
72
81
  if (event.key === 'Escape') {
73
82
  if (mobileMenuOpen) {
@@ -102,10 +111,12 @@
102
111
  });
103
112
 
104
113
  onDestroy(() => {
105
- if (showOnScroll) {
114
+ if (typeof window !== 'undefined' && showOnScroll) {
106
115
  window.removeEventListener('scroll', handleScroll);
107
116
  }
108
- document.body.style.overflow = '';
117
+ if (typeof document !== 'undefined') {
118
+ document.body.style.overflow = '';
119
+ }
109
120
  });
110
121
 
111
122
  const variantClass = $derived(
@@ -140,21 +151,49 @@
140
151
  <nav class="global-header__nav" aria-label="Main navigation">
141
152
  <ul class="global-header__nav-list">
142
153
  {#each navItems as item, index}
143
- <li class="global-header__nav-item">
154
+ <li
155
+ class="global-header__nav-item"
156
+ onmouseenter={() => item.children && item.children.length > 0 && (openDropdown = index)}
157
+ onmouseleave={() => item.children && item.children.length > 0 && (openDropdown = null)}
158
+ >
144
159
  {#if item.children && item.children.length > 0}
145
- <button
146
- type="button"
147
- class="global-header__nav-link global-header__nav-link--dropdown"
148
- onclick={() => toggleDropdown(index)}
149
- aria-expanded={openDropdown === index}
150
- aria-haspopup="true"
151
- >
152
- {#if item.icon}
153
- <span class="global-header__nav-icon">{item.icon}</span>
154
- {/if}
155
- {item.label}
156
- <span class="global-header__dropdown-arrow" aria-hidden="true">▼</span>
157
- </button>
160
+ {#if item.href}
161
+ <div class="global-header__nav-item-split">
162
+ <a
163
+ href={item.href}
164
+ class="global-header__nav-link global-header__nav-link--with-dropdown"
165
+ >
166
+ {#if item.icon}
167
+ <span class="global-header__nav-icon">{item.icon}</span>
168
+ {/if}
169
+ {item.label}
170
+ </a>
171
+ <button
172
+ type="button"
173
+ class="global-header__dropdown-toggle"
174
+ onclick={() => toggleDropdown(index)}
175
+ aria-expanded={openDropdown === index}
176
+ aria-haspopup="true"
177
+ aria-label="Toggle {item.label} menu"
178
+ >
179
+ <span class="global-header__dropdown-arrow" aria-hidden="true">▼</span>
180
+ </button>
181
+ </div>
182
+ {:else}
183
+ <button
184
+ type="button"
185
+ class="global-header__nav-link global-header__nav-link--dropdown"
186
+ onclick={() => toggleDropdown(index)}
187
+ aria-expanded={openDropdown === index}
188
+ aria-haspopup="true"
189
+ >
190
+ {#if item.icon}
191
+ <span class="global-header__nav-icon">{item.icon}</span>
192
+ {/if}
193
+ {item.label}
194
+ <span class="global-header__dropdown-arrow" aria-hidden="true">▼</span>
195
+ </button>
196
+ {/if}
158
197
 
159
198
  {#if openDropdown === index}
160
199
  <ul class="global-header__dropdown" role="menu">
@@ -224,32 +263,51 @@
224
263
  {:else}
225
264
  <nav class="global-header__mobile-nav" aria-label="Mobile navigation">
226
265
  <ul class="global-header__mobile-nav-list">
227
- {#each navItems as item}
266
+ {#each navItems as item, index}
228
267
  <li class="global-header__mobile-nav-item">
229
268
  {#if item.children && item.children.length > 0}
230
269
  <div class="global-header__mobile-nav-group">
231
- <span class="global-header__mobile-nav-label">
232
- {#if item.icon}
233
- <span class="global-header__nav-icon">{item.icon}</span>
234
- {/if}
235
- {item.label}
236
- </span>
237
- <ul class="global-header__mobile-nav-sublist">
238
- {#each item.children as child}
239
- <li>
240
- <a
241
- href={child.href || '#'}
242
- class="global-header__mobile-nav-link"
243
- onclick={closeMobileMenu}
244
- >
245
- {#if child.icon}
246
- <span class="global-header__nav-icon">{child.icon}</span>
247
- {/if}
248
- {child.label}
249
- </a>
250
- </li>
251
- {/each}
252
- </ul>
270
+ <div class="global-header__mobile-nav-header">
271
+ <a
272
+ href={item.href || '#'}
273
+ class="global-header__mobile-nav-link global-header__mobile-nav-link--parent"
274
+ onclick={closeMobileMenu}
275
+ >
276
+ {#if item.icon}
277
+ <span class="global-header__nav-icon">{item.icon}</span>
278
+ {/if}
279
+ {item.label}
280
+ </a>
281
+ <button
282
+ type="button"
283
+ class="global-header__mobile-toggle"
284
+ onclick={() => toggleMobileSubmenu(index)}
285
+ aria-expanded={openMobileSubmenu === index}
286
+ aria-label="Toggle {item.label} submenu"
287
+ >
288
+ <span class="global-header__mobile-toggle-icon">
289
+ {openMobileSubmenu === index ? '−' : '+'}
290
+ </span>
291
+ </button>
292
+ </div>
293
+ {#if openMobileSubmenu === index}
294
+ <ul class="global-header__mobile-nav-sublist">
295
+ {#each item.children as child}
296
+ <li>
297
+ <a
298
+ href={child.href || '#'}
299
+ class="global-header__mobile-nav-link global-header__mobile-nav-link--child"
300
+ onclick={closeMobileMenu}
301
+ >
302
+ {#if child.icon}
303
+ <span class="global-header__nav-icon">{child.icon}</span>
304
+ {/if}
305
+ {child.label}
306
+ </a>
307
+ </li>
308
+ {/each}
309
+ </ul>
310
+ {/if}
253
311
  </div>
254
312
  {:else}
255
313
  <a
@@ -275,12 +333,6 @@
275
333
  </nav>
276
334
  {/if}
277
335
  </div>
278
- <button
279
- type="button"
280
- class="global-header__overlay"
281
- aria-label="Close menu"
282
- onclick={closeMobileMenu}
283
- ></button>
284
336
  {/if}
285
337
  </header>
286
338
 
@@ -408,6 +460,44 @@
408
460
  text-align: left;
409
461
  }
410
462
 
463
+ .global-header__nav-item-split {
464
+ display: flex;
465
+ align-items: center;
466
+ gap: 0;
467
+ }
468
+
469
+ .global-header__nav-link--with-dropdown {
470
+ flex: 1;
471
+ padding-right: var(--space-xs, 0.25rem);
472
+ }
473
+
474
+ .global-header__dropdown-toggle {
475
+ display: flex;
476
+ align-items: center;
477
+ justify-content: center;
478
+ padding: var(--space-sm, 0.5rem);
479
+ background: transparent;
480
+ border: none;
481
+ cursor: pointer;
482
+ font-family: inherit;
483
+ color: var(--header-text-color);
484
+ border-radius: var(--radius-md, 0.375rem);
485
+ transition: all var(--transition-fast, 150ms ease);
486
+ /* Touch target minimum */
487
+ min-height: var(--touch-target-min, 44px);
488
+ min-width: 2rem;
489
+ }
490
+
491
+ .global-header__dropdown-toggle:hover {
492
+ background: var(--color-bg-subtle, rgba(0, 0, 0, 0.05));
493
+ color: var(--color-primary, #3b82f6);
494
+ }
495
+
496
+ .global-header__dropdown-toggle:focus-visible {
497
+ outline: 2px solid var(--color-primary, #3b82f6);
498
+ outline-offset: 2px;
499
+ }
500
+
411
501
  .global-header__nav-icon {
412
502
  display: inline-flex;
413
503
  align-items: center;
@@ -419,7 +509,8 @@
419
509
  transition: transform var(--transition-fast, 150ms ease);
420
510
  }
421
511
 
422
- .global-header__nav-link[aria-expanded='true'] .global-header__dropdown-arrow {
512
+ .global-header__nav-link[aria-expanded='true'] .global-header__dropdown-arrow,
513
+ .global-header__dropdown-toggle[aria-expanded='true'] .global-header__dropdown-arrow {
423
514
  transform: rotate(180deg);
424
515
  }
425
516
 
@@ -536,27 +627,21 @@
536
627
  }
537
628
 
538
629
  .global-header__mobile-menu {
539
- position: fixed;
540
- top: 0;
630
+ position: absolute;
631
+ top: 100%;
632
+ left: 0;
541
633
  right: 0;
542
- bottom: 0;
543
- width: min(80vw, 20rem);
634
+ width: 100%;
544
635
  background: var(--color-bg, #ffffff);
545
- border-left: 1px solid var(--header-border-color);
636
+ border-top: 1px solid var(--header-border-color);
637
+ border-bottom: 1px solid var(--header-border-color);
546
638
  padding: var(--space-lg, 1.5rem);
547
639
  overflow-y: auto;
548
- z-index: calc(var(--header-z-index) + 2);
549
- animation: slide-in 200ms ease-out;
640
+ max-height: calc(100vh - var(--header-height));
641
+ z-index: calc(var(--header-z-index) - 1);
642
+ animation: slide-down 200ms ease-out;
550
643
  font-family: inherit;
551
- }
552
-
553
- .global-header__overlay {
554
- position: fixed;
555
- inset: 0;
556
- background: rgba(0, 0, 0, 0.4);
557
- z-index: calc(var(--header-z-index) + 1);
558
- animation: fade-in 200ms ease-out;
559
- cursor: pointer;
644
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
560
645
  }
561
646
 
562
647
  .global-header__mobile-nav-list {
@@ -602,25 +687,62 @@
602
687
  gap: var(--space-xs, 0.25rem);
603
688
  }
604
689
 
605
- .global-header__mobile-nav-label {
690
+ .global-header__mobile-nav-header {
606
691
  display: flex;
607
692
  align-items: center;
608
- gap: var(--space-sm, 0.5rem);
609
- padding: var(--space-md, 1rem);
693
+ gap: var(--space-xs, 0.25rem);
694
+ }
695
+
696
+ .global-header__mobile-nav-link--parent {
697
+ flex: 1;
698
+ padding-right: var(--space-xs, 0.25rem);
699
+ }
700
+
701
+ .global-header__mobile-toggle {
702
+ display: flex;
703
+ align-items: center;
704
+ justify-content: center;
705
+ min-width: var(--touch-target-min, 44px);
706
+ min-height: var(--touch-target-min, 44px);
707
+ padding: var(--space-sm, 0.5rem);
708
+ background: transparent;
709
+ border: 1px solid var(--color-border, #e5e7eb);
710
+ border-radius: var(--radius-md, 0.375rem);
711
+ cursor: pointer;
712
+ transition: all var(--transition-fast, 150ms ease);
713
+ color: var(--header-text-color);
714
+ }
715
+
716
+ .global-header__mobile-toggle:hover {
717
+ background: var(--color-bg-subtle, rgba(0, 0, 0, 0.05));
718
+ border-color: var(--color-primary, #3b82f6);
719
+ }
720
+
721
+ .global-header__mobile-toggle:focus-visible {
722
+ outline: 2px solid var(--color-primary, #3b82f6);
723
+ outline-offset: 2px;
724
+ }
725
+
726
+ .global-header__mobile-toggle-icon {
727
+ font-size: 1.25rem;
610
728
  font-weight: 600;
611
- font-size: var(--text-sm, 0.875rem);
612
- text-transform: uppercase;
613
- letter-spacing: 0.05em;
614
- color: var(--color-text-muted, #6b7280);
729
+ line-height: 1;
615
730
  }
616
731
 
617
732
  .global-header__mobile-nav-sublist {
618
733
  list-style: none;
619
734
  margin: 0;
620
- padding-left: var(--space-md, 1rem);
735
+ padding-left: var(--space-lg, 1.5rem);
621
736
  display: flex;
622
737
  flex-direction: column;
623
738
  gap: var(--space-xs, 0.25rem);
739
+ margin-top: var(--space-xs, 0.25rem);
740
+ animation: slide-down 200ms ease-out;
741
+ }
742
+
743
+ .global-header__mobile-nav-link--child {
744
+ font-size: var(--text-sm, 0.875rem);
745
+ padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
624
746
  }
625
747
 
626
748
  .global-header__mobile-actions {
@@ -632,21 +754,14 @@
632
754
  gap: var(--space-sm, 0.5rem);
633
755
  }
634
756
 
635
- @keyframes slide-in {
636
- from {
637
- transform: translateX(100%);
638
- }
639
- to {
640
- transform: translateX(0);
641
- }
642
- }
643
-
644
- @keyframes fade-in {
757
+ @keyframes slide-down {
645
758
  from {
646
759
  opacity: 0;
760
+ transform: translateY(-1rem);
647
761
  }
648
762
  to {
649
763
  opacity: 1;
764
+ transform: translateY(0);
650
765
  }
651
766
  }
652
767
 
@@ -675,8 +790,7 @@
675
790
  display: none;
676
791
  }
677
792
 
678
- .global-header__mobile-menu,
679
- .global-header__overlay {
793
+ .global-header__mobile-menu {
680
794
  display: none;
681
795
  }
682
796
  }
@@ -0,0 +1,182 @@
1
+ <script lang="ts">
2
+ import type { HeroLeftAlignedProps } from '../../types/index.js';
3
+
4
+ interface Props extends HeroLeftAlignedProps {
5
+ children?: import('svelte').Snippet;
6
+ actions?: import('svelte').Snippet;
7
+ }
8
+
9
+ let {
10
+ title,
11
+ subtitle,
12
+ backgroundImage,
13
+ backgroundColor = 'var(--color-primary, #3b82f6)',
14
+ textColor = 'var(--color-text-inverse, #ffffff)',
15
+ overlayOpacity = 0.7,
16
+ height = 'auto',
17
+ class: className = '',
18
+ children,
19
+ actions
20
+ }: Props = $props();
21
+ </script>
22
+
23
+ <section
24
+ class="hero-left-aligned {className}"
25
+ style="background-color: {backgroundColor}; color: {textColor}; --overlay-opacity: {overlayOpacity}; min-height: {height};"
26
+ role="banner"
27
+ >
28
+ <!-- Background Image with Overlay -->
29
+ {#if backgroundImage}
30
+ <div
31
+ class="hero-left-aligned__background"
32
+ style="background-image: url({backgroundImage});"
33
+ aria-hidden="true"
34
+ ></div>
35
+ <div class="hero-left-aligned__overlay" style="background-color: {backgroundColor};"></div>
36
+ {/if}
37
+
38
+ <!-- Content Container -->
39
+ <div class="hero-left-aligned__container">
40
+ <div class="hero-left-aligned__content">
41
+ <!-- Title -->
42
+ <h1 class="hero-left-aligned__title">
43
+ {@html title}
44
+ </h1>
45
+
46
+ <!-- Subtitle -->
47
+ {#if subtitle}
48
+ <p class="hero-left-aligned__subtitle">
49
+ {@html subtitle}
50
+ </p>
51
+ {/if}
52
+
53
+ <!-- Custom Content Slot -->
54
+ {#if children}
55
+ <div class="hero-left-aligned__body">
56
+ {@render children()}
57
+ </div>
58
+ {/if}
59
+
60
+ <!-- Actions (Buttons) -->
61
+ {#if actions}
62
+ <div class="hero-left-aligned__actions">
63
+ {@render actions()}
64
+ </div>
65
+ {/if}
66
+ </div>
67
+ </div>
68
+ </section>
69
+
70
+ <style>
71
+ .hero-left-aligned {
72
+ position: relative;
73
+ overflow: hidden;
74
+ min-height: 700px;
75
+ font-family: var(--font-family, system-ui, sans-serif);
76
+ animation: heroFadeIn 1s ease-out;
77
+ }
78
+
79
+ @keyframes heroFadeIn {
80
+ from {
81
+ opacity: 0;
82
+ transform: translateY(20px);
83
+ }
84
+ to {
85
+ opacity: 1;
86
+ transform: translateY(0);
87
+ }
88
+ }
89
+
90
+ .hero-left-aligned__background {
91
+ position: absolute;
92
+ inset: 0;
93
+ background-size: contain;
94
+ background-repeat: no-repeat;
95
+ background-position: 120% center;
96
+ opacity: calc(1 - var(--overlay-opacity, 0.7));
97
+ }
98
+
99
+ .hero-left-aligned__overlay {
100
+ position: absolute;
101
+ inset: 0;
102
+ background: linear-gradient(to right, var(--color-primary, currentColor) 0%, transparent 60%);
103
+ opacity: 0.85;
104
+ }
105
+
106
+ .hero-left-aligned__container {
107
+ position: relative;
108
+ z-index: 10;
109
+ max-width: var(--max-width, 80rem);
110
+ margin: 0 auto;
111
+ padding: var(--space-xl, 3rem) var(--space-lg, 1.5rem);
112
+ padding-right: var(--space-2xl, 4rem);
113
+ }
114
+
115
+ .hero-left-aligned__content {
116
+ max-width: 64rem;
117
+ text-align: left;
118
+ }
119
+
120
+ .hero-left-aligned__title {
121
+ font-size: var(--text-5xl, 3rem);
122
+ font-weight: 700;
123
+ line-height: 1.1;
124
+ letter-spacing: -0.02em;
125
+ margin-bottom: var(--space-lg, 1.5rem);
126
+ color: inherit;
127
+ }
128
+
129
+ .hero-left-aligned__subtitle {
130
+ font-size: var(--text-lg, 1.125rem);
131
+ font-weight: 600;
132
+ line-height: 1.6;
133
+ margin-bottom: var(--space-xl, 3rem);
134
+ max-width: 48rem;
135
+ opacity: 0.95;
136
+ }
137
+
138
+ .hero-left-aligned__body {
139
+ margin-bottom: var(--space-xl, 3rem);
140
+ }
141
+
142
+ .hero-left-aligned__actions {
143
+ display: flex;
144
+ flex-wrap: wrap;
145
+ gap: var(--space-md, 1rem);
146
+ }
147
+
148
+ /* Responsive Styles */
149
+ @media (min-width: 768px) {
150
+ .hero-left-aligned__container {
151
+ padding: var(--space-2xl, 4rem) var(--space-xl, 3rem);
152
+ padding-right: var(--space-3xl, 6rem);
153
+ }
154
+
155
+ .hero-left-aligned__title {
156
+ font-size: var(--text-6xl, 3.75rem);
157
+ margin-bottom: var(--space-xl, 3rem);
158
+ }
159
+ }
160
+
161
+ @media (min-width: 1024px) {
162
+ .hero-left-aligned__container {
163
+ padding: var(--space-3xl, 6rem) var(--space-2xl, 4rem);
164
+ }
165
+
166
+ .hero-left-aligned__title {
167
+ font-size: var(--text-7xl, 4.5rem);
168
+ }
169
+
170
+ .hero-left-aligned__subtitle {
171
+ font-size: var(--text-xl, 1.25rem);
172
+ }
173
+ }
174
+
175
+ /* Dark mode support */
176
+ @media (prefers-color-scheme: dark) {
177
+ .hero-left-aligned {
178
+ --color-primary: var(--color-primary-dark, #3b82f6);
179
+ --color-text-inverse: var(--color-text-inverse-dark, #f9fafb);
180
+ }
181
+ }
182
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { HeroLeftAlignedProps } from '../../types/index.js';
2
+ interface Props extends HeroLeftAlignedProps {
3
+ children?: import('svelte').Snippet;
4
+ actions?: import('svelte').Snippet;
5
+ }
6
+ declare const HeroLeftAligned: import("svelte").Component<Props, {}, "">;
7
+ type HeroLeftAligned = ReturnType<typeof HeroLeftAligned>;
8
+ export default HeroLeftAligned;