@yatoday/astro-ui 0.17.32 → 0.18.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.
package/astro.d.ts CHANGED
@@ -48,6 +48,7 @@ import type { WidgetHero1Props as YtWidgetHero1Props } from './components/Widget
48
48
  import type { WidgetHero2Props as YtWidgetHero2Props } from './components/WidgetHero2/types'
49
49
  import type { WidgetHeroSliderProps as YtWidgetHeroSliderProps } from './components/WidgetHeroSlider/types'
50
50
  import type { WidgetNavbarProps as YtWidgetNavbarProps } from './components/WidgetNavbar/types'
51
+ import type { WidgetNavbarFlyoutProps as YtWidgetNavbarFlyoutProps } from './components/WidgetNavbarFlyout/types'
51
52
  import type { WidgetStatsProps as YtWidgetStatsProps } from './components/WidgetStats/types'
52
53
  import type { WidgetSteps1Props as YtWidgetSteps1Props } from './components/WidgetSteps1/types'
53
54
  import type { WidgetStepsTimelineProps as YtWidgetStepsTimelineProps } from './components/WidgetStepsTimeline/types'
@@ -107,6 +108,7 @@ declare module '@yatoday/astro-ui/astro' {
107
108
  export function WidgetHero2(_props: YtWidgetHero2Props): any
108
109
  export function WidgetHeroSlider(_props: YtWidgetHeroSliderProps): any
109
110
  export function WidgetNavbar(_props: YtWidgetNavbarProps): any
111
+ export function WidgetNavbarFlyout(_props: YtWidgetNavbarFlyoutProps): any
110
112
  export function WidgetStats(_props: YtWidgetStatsProps): any
111
113
  export function WidgetSteps1(_props: YtWidgetSteps1Props): any
112
114
  export function WidgetStepsTimeline(_props: YtWidgetStepsTimelineProps): any
@@ -165,6 +167,7 @@ declare module '@yatoday/astro-ui/astro' {
165
167
  export type WidgetHero2Props = YtWidgetHero2Props
166
168
  export type WidgetHeroSliderProps = YtWidgetHeroSliderProps
167
169
  export type WidgetNavbarProps = YtWidgetNavbarProps
170
+ export type WidgetNavbarFlyoutProps = YtWidgetNavbarFlyoutProps
168
171
  export type WidgetStatsProps = YtWidgetStatsProps
169
172
  export type WidgetSteps1Props = YtWidgetSteps1Props
170
173
  export type WidgetStepsTimelineProps = YtWidgetStepsTimelineProps
package/astro.js CHANGED
@@ -48,6 +48,7 @@ import WidgetHero1Component from './components/WidgetHero1/WidgetHero1.astro'
48
48
  import WidgetHero2Component from './components/WidgetHero2/WidgetHero2.astro'
49
49
  import WidgetHeroSliderComponent from './components/WidgetHeroSlider/WidgetHeroSlider.astro'
50
50
  import WidgetNavbarComponent from './components/WidgetNavbar/WidgetNavbar.astro'
51
+ import WidgetNavbarFlyoutComponent from './components/WidgetNavbarFlyout/WidgetNavbarFlyout.astro'
51
52
  import WidgetStatsComponent from './components/WidgetStats/WidgetStats.astro'
52
53
  import WidgetSteps1Component from './components/WidgetSteps1/WidgetSteps1.astro'
53
54
  import WidgetStepsTimelineComponent from './components/WidgetStepsTimeline/WidgetStepsTimeline.astro'
@@ -106,6 +107,7 @@ export const WidgetHero1 = WidgetHero1Component
106
107
  export const WidgetHero2 = WidgetHero2Component
107
108
  export const WidgetHeroSlider = WidgetHeroSliderComponent
108
109
  export const WidgetNavbar = WidgetNavbarComponent
110
+ export const WidgetNavbarFlyout = WidgetNavbarFlyoutComponent
109
111
  export const WidgetStats = WidgetStatsComponent
110
112
  export const WidgetSteps1 = WidgetSteps1Component
111
113
  export const WidgetStepsTimeline = WidgetStepsTimelineComponent
@@ -30,6 +30,8 @@ const {
30
30
  links: linksClass = 'hover:text-link dark:hover:text-white',
31
31
  dropdown: dropdownClass = 'bg-background text-foreground md:shadow-lg',
32
32
  dropdownLinks: dropdownLinksClass = 'hover:bg-black/10 hover:text-link dark:hover:text-white dark:hover:bg-white/10',
33
+ megaMenu: megaMenuClass = 'bg-background text-foreground md:shadow-lg',
34
+ megaHeading: megaHeadingClass = 'text-foreground/50',
33
35
  darkModeIcon: darkModeIconClass = '',
34
36
  pinned: pinnedClasses = 'bg-background md:bg-background/90 md:backdrop-blur-md',
35
37
  unpinned: unpinnedClasses = '',
@@ -81,9 +83,82 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
81
83
  class="flex flex-col md:flex-row md:self-center w-full md:w-auto text-xl md:text-[0.9375rem] tracking-[0.01rem] font-medium md:justify-center"
82
84
  >
83
85
  {
84
- links.map(({ text, href, links }) => (
86
+ links.map(({ text, href, links, mega, icon }) => (
85
87
  <li class={links?.length ? 'dropdown' : ''}>
86
- {links?.length ? (
88
+ {links?.length && mega ? (
89
+ <>
90
+ <button
91
+ type="button"
92
+ class={cn('px-4 py-3 flex items-center whitespace-nowrap cursor-pointer', linksClass)}
93
+ >
94
+ {text}{' '}
95
+ <Icon name="tabler:chevron-down" class="w-3.5 h-3.5 ml-0.5 rtl:ml-0 rtl:mr-0.5 hidden md:inline" />
96
+ </button>
97
+ <div
98
+ class={cn(
99
+ 'dropdown-menu rounded-sm md:absolute md:left-0 md:right-0 md:top-full pl-4 md:pl-0 pb-1 md:pb-0 md:hidden font-medium',
100
+ megaMenuClass
101
+ )}
102
+ >
103
+ <div class="mega-grid md:grid md:gap-x-8 md:gap-y-4 md:p-8" style={`--mega-cols: ${links.length}`}>
104
+ {links.map((column) => (
105
+ <div class="py-2 md:py-0">
106
+ {column.href ? (
107
+ <a
108
+ href={column.href}
109
+ class={cn(
110
+ 'flex items-center gap-1.5 mb-2 text-xs font-semibold uppercase tracking-wider hover:text-link',
111
+ megaHeadingClass
112
+ )}
113
+ >
114
+ {column.icon && <Icon name={column.icon} class="w-4 h-4" />}
115
+ {column.text}
116
+ </a>
117
+ ) : (
118
+ <span
119
+ class={cn(
120
+ 'flex items-center gap-1.5 mb-2 text-xs font-semibold uppercase tracking-wider',
121
+ megaHeadingClass
122
+ )}
123
+ >
124
+ {column.icon && <Icon name={column.icon} class="w-4 h-4" />}
125
+ {column.text}
126
+ </span>
127
+ )}
128
+ <ul class="flex flex-col">
129
+ {column.links?.map(({ text: text2, href: href2, icon: icon2, description }) => (
130
+ <li>
131
+ <a
132
+ class={cn(
133
+ 'group flex items-start gap-3 rounded-md py-2 px-3 -mx-3 md:px-2 md:-mx-2',
134
+ { 'link-active': href2 === currentPath },
135
+ dropdownLinksClass
136
+ )}
137
+ href={href2}
138
+ >
139
+ {icon2 && (
140
+ <span class="flex-none mt-0.5 flex items-center justify-center w-8 h-8 rounded-md bg-foreground/[0.06] text-foreground/70 group-hover:bg-foreground/10">
141
+ <Icon name={icon2} class="w-4.5 h-4.5" />
142
+ </span>
143
+ )}
144
+ <span class="flex flex-col">
145
+ <span class="font-medium leading-tight">{text2}</span>
146
+ {description && (
147
+ <span class="mt-0.5 text-xs font-normal text-muted-foreground leading-snug normal-case">
148
+ {description}
149
+ </span>
150
+ )}
151
+ </span>
152
+ </a>
153
+ </li>
154
+ ))}
155
+ </ul>
156
+ </div>
157
+ ))}
158
+ </div>
159
+ </div>
160
+ </>
161
+ ) : links?.length ? (
87
162
  <>
88
163
  <button
89
164
  type="button"
@@ -117,12 +192,13 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
117
192
  ) : (
118
193
  <a
119
194
  class={cn([
120
- 'px-4 py-3 flex items-center whitespace-nowrap',
195
+ 'px-4 py-3 flex items-center gap-1.5 whitespace-nowrap',
121
196
  { 'link-active': href === currentPath },
122
197
  linksClass,
123
198
  ])}
124
199
  href={href}
125
200
  >
201
+ {icon && <Icon name={icon} class="w-4 h-4" />}
126
202
  {text}
127
203
  </a>
128
204
  )}
@@ -140,7 +216,14 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
140
216
  >
141
217
  <div class="items-center flex justify-between w-full md:w-auto">
142
218
  <div class="flex">
143
- {showToggleTheme && <DarkMode initialMode={initialMode} iconClass={cn('w-6 h-6 md:w-5 md:h-5 md:inline-block', darkModeIconClass)} />}
219
+ {
220
+ showToggleTheme && (
221
+ <DarkMode
222
+ initialMode={initialMode}
223
+ iconClass={cn('w-6 h-6 md:w-5 md:h-5 md:inline-block', darkModeIconClass)}
224
+ />
225
+ )
226
+ }
144
227
  </div>
145
228
 
146
229
  <!-- Actions Buttons -->
@@ -149,7 +232,10 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
149
232
  actions?.length ? (
150
233
  <span class="ml-4 rtl:ml-0 rtl:mr-4">
151
234
  {actions.map(({ class: actionClass, ...btnProps }) => (
152
- <Button {...btnProps} class={cn('ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto', actionClass)} />
235
+ <Button
236
+ {...btnProps}
237
+ class={cn('ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto', actionClass)}
238
+ />
153
239
  ))}
154
240
  </span>
155
241
  ) : (
@@ -177,6 +263,12 @@ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
177
263
  .dropdown:hover .dropdown-menu {
178
264
  display: block;
179
265
  }
266
+
267
+ @media (min-width: 768px) {
268
+ .mega-grid {
269
+ grid-template-columns: repeat(var(--mega-cols, 3), minmax(0, 1fr));
270
+ }
271
+ }
180
272
  </style>
181
273
 
182
274
  <script>
@@ -20,10 +20,23 @@ export interface ILink {
20
20
  href?: string;
21
21
  ariaLabel?: string;
22
22
  icon?: string;
23
+ /**
24
+ * Optional one-line supporting text. In a mega panel, leaf items with a
25
+ * `description` render Stripe-style: an icon tile, a bold title, and this
26
+ * muted subtitle beneath it. Ignored by regular dropdowns.
27
+ */
28
+ description?: string;
23
29
  }
24
30
 
25
31
  export interface IMenuLink extends ILink {
26
32
  links?: IMenuLink[];
33
+ /**
34
+ * When true on a top-level item, its `links` are rendered as a full-width
35
+ * multi-column mega panel: each child becomes a column whose `text`/`href`
36
+ * is the column heading and whose nested `links` are the column items.
37
+ * Defaults to a standard single-column dropdown.
38
+ */
39
+ mega?: boolean;
27
40
  }
28
41
 
29
42
  export type SvelteWidgetNavbarProps = {
@@ -0,0 +1,576 @@
1
+ ---
2
+ /**
3
+ * WidgetNavbarFlyout
4
+ *
5
+ * A navbar whose dropdowns share a single, persistent flyout panel that stays
6
+ * open and smoothly morphs (width / height / horizontal position) as the cursor
7
+ * moves between top-level items — the Stripe "one container that resizes"
8
+ * pattern — instead of each menu abruptly closing and the next one opening.
9
+ *
10
+ * Any top-level item with `links` becomes a flyout panel. Items flagged `mega`
11
+ * render multi-column Stripe-style rows (icon tile + title + description);
12
+ * plain items render a single-column list. Desktop interaction is JS-driven
13
+ * (hover with open/close delays, keyboard + Escape, outside-click). Mobile keeps
14
+ * an always-expanded accordion and needs no JS.
15
+ *
16
+ * Drop-in compatible with WidgetNavbar's props and slots.
17
+ */
18
+ import type { WidgetNavbarProps as Props } from '../WidgetNavbar/types';
19
+
20
+ import { SITE } from 'vendor:config';
21
+ import { Icon } from 'astro-icon/components';
22
+ import DarkMode from '../DarkMode/DarkMode.astro';
23
+ import ToggleMenu from '../WidgetNavbar/ToggleMenu.astro';
24
+ import Button from '../Button/Button.astro';
25
+ import WidgetWrapper from '../WidgetWrapper/WidgetWrapper.astro';
26
+ import { cn, trimSlash } from '../../utils';
27
+ import { twMerge } from 'tailwind-merge';
28
+
29
+ const {
30
+ links = [],
31
+ actions = [],
32
+ isSticky = false,
33
+ isDark = false,
34
+ showToggleTheme = false,
35
+ initialMode = 'system',
36
+ position = 'center',
37
+ classes = {},
38
+ scrollOffset = 60,
39
+ as = 'header',
40
+ bg = await Astro.slots.render('bg'),
41
+ } = Astro.props;
42
+
43
+ const {
44
+ header: headerClass = '',
45
+ container: containerClass = 'max-w-7xl 2xl:max-w-[96rem] md:mx-auto',
46
+ links: linksClass = 'hover:text-link dark:hover:text-white',
47
+ dropdownLinks: dropdownLinksClass = 'hover:bg-black/10 hover:text-link dark:hover:text-white dark:hover:bg-white/10',
48
+ megaMenu: megaMenuClass = 'bg-background text-foreground md:shadow-lg',
49
+ megaHeading: megaHeadingClass = 'text-foreground/50',
50
+ darkModeIcon: darkModeIconClass = '',
51
+ pinned: pinnedClasses = 'bg-background md:bg-background/90 md:backdrop-blur-md',
52
+ unpinned: unpinnedClasses = '',
53
+ } = classes;
54
+
55
+ const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
56
+
57
+ // Top-level items that get a shared flyout panel (anything with children).
58
+ const panelItems = links.map((link, index) => ({ link, index })).filter(({ link }) => link.links?.length);
59
+ ---
60
+
61
+ <WidgetWrapper
62
+ id="header"
63
+ isDark={isDark}
64
+ bg={bg}
65
+ as={as}
66
+ class={cn(
67
+ { sticky: isSticky, relative: !isSticky, dark: isDark },
68
+ '-top-[1px] z-40 flex-none w-full transition-[opacity] ease-in-out',
69
+ headerClass,
70
+ unpinnedClasses
71
+ )}
72
+ containerClass={twMerge('py-0 md:py-0 lg:py-0', containerClass)}
73
+ {...isSticky ? { 'data-yt-sticky-header': true } : {}}
74
+ data-scroll-offset={scrollOffset}
75
+ data-pinned-classes={pinnedClasses}
76
+ data-unpinned-classes={unpinnedClasses}
77
+ data-yt-flyout-header
78
+ >
79
+ <div
80
+ class={cn(
81
+ 'relative text-default py-3 mx-auto w-full',
82
+ { 'md:flex md:justify-between': position !== 'center' },
83
+ { 'md:grid md:grid-cols-3 md:items-center': position === 'center' }
84
+ )}
85
+ >
86
+ <div class:list={[{ 'mr-auto rtl:mr-0 rtl:ml-auto': position === 'right' }, 'flex justify-between items-center']}>
87
+ <!-- Logo -->
88
+ <slot name="logo">
89
+ <span>{SITE.name || 'Logo'}</span>
90
+ </slot>
91
+ <div class="flex items-center md:hidden">
92
+ <slot name="toggle-menu">
93
+ <ToggleMenu />
94
+ </slot>
95
+ </div>
96
+ </div>
97
+
98
+ <nav
99
+ class="items-center w-full md:w-auto hidden md:flex md:mx-5 text-default overflow-y-auto overflow-x-hidden md:overflow-y-visible md:overflow-x-auto md:justify-self-center"
100
+ aria-label="Main navigation"
101
+ data-yt-flyout-nav
102
+ >
103
+ <ul
104
+ class="flex flex-col md:flex-row md:self-center w-full md:w-auto text-xl md:text-[0.9375rem] tracking-[0.01rem] font-medium md:justify-center"
105
+ >
106
+ {
107
+ links.map((item, index) => {
108
+ const hasPanel = !!item.links?.length;
109
+ return (
110
+ <li class="md:static">
111
+ {/* Desktop trigger / link */}
112
+ {hasPanel ? (
113
+ <button
114
+ type="button"
115
+ data-flyout-trigger
116
+ data-flyout-key={index}
117
+ aria-haspopup="true"
118
+ aria-expanded="false"
119
+ class={cn('hidden md:flex px-4 py-3 items-center whitespace-nowrap cursor-pointer', linksClass)}
120
+ >
121
+ {item.text}{' '}
122
+ <Icon name="tabler:chevron-down" class="w-3.5 h-3.5 ml-0.5 rtl:ml-0 rtl:mr-0.5" />
123
+ </button>
124
+ ) : (
125
+ <a
126
+ href={item.href}
127
+ data-flyout-dismiss
128
+ class={cn(
129
+ 'hidden md:flex px-4 py-3 items-center gap-1.5 whitespace-nowrap',
130
+ { 'link-active': item.href === currentPath },
131
+ linksClass
132
+ )}
133
+ >
134
+ {item.icon && <Icon name={item.icon} class="w-4 h-4" />}
135
+ {item.text}
136
+ </a>
137
+ )}
138
+
139
+ {/* Mobile accordion (always expanded, no JS required) */}
140
+ <div class="md:hidden">
141
+ {hasPanel ? (
142
+ <>
143
+ <span class={cn('block px-4 py-3 text-base font-semibold uppercase tracking-wider', megaHeadingClass)}>
144
+ {item.text}
145
+ </span>
146
+ <ul class="flex flex-col pb-2">
147
+ {item.mega
148
+ ? item.links?.flatMap((column) =>
149
+ (column.links ?? []).map((leaf) => (
150
+ <li>
151
+ <a
152
+ href={leaf.href}
153
+ class={cn(
154
+ 'block rounded-sm py-2 px-6 text-base',
155
+ { 'link-active': leaf.href === currentPath },
156
+ dropdownLinksClass
157
+ )}
158
+ >
159
+ {leaf.text}
160
+ </a>
161
+ </li>
162
+ ))
163
+ )
164
+ : item.links?.map((leaf) => (
165
+ <li>
166
+ <a
167
+ href={leaf.href}
168
+ class={cn(
169
+ 'block rounded-sm py-2 px-6 text-base',
170
+ { 'link-active': leaf.href === currentPath },
171
+ dropdownLinksClass
172
+ )}
173
+ >
174
+ {leaf.text}
175
+ </a>
176
+ </li>
177
+ ))}
178
+ </ul>
179
+ </>
180
+ ) : (
181
+ <a
182
+ href={item.href}
183
+ class={cn('block px-4 py-3 text-xl', { 'link-active': item.href === currentPath })}
184
+ >
185
+ {item.text}
186
+ </a>
187
+ )}
188
+ </div>
189
+ </li>
190
+ );
191
+ })
192
+ }
193
+ </ul>
194
+ </nav>
195
+
196
+ {/* Shared morphing flyout (desktop only) */}
197
+ {
198
+ panelItems.length > 0 && (
199
+ <div
200
+ class="flyout hidden md:block absolute left-0 right-0 top-full z-40"
201
+ data-flyout
202
+ role="region"
203
+ aria-label="Menu details"
204
+ >
205
+ <div class={cn('flyout-bg relative overflow-hidden rounded-xl border border-border', megaMenuClass)} data-flyout-bg>
206
+ {panelItems.map(({ link, index }) => (
207
+ <div class="flyout-panel absolute top-0 left-0" data-flyout-panel data-flyout-key={index}>
208
+ {link.mega ? (
209
+ <div class="flex gap-8 p-8">
210
+ {link.links?.map((column) => (
211
+ <div class="min-w-[12rem]">
212
+ {column.href ? (
213
+ <a
214
+ href={column.href}
215
+ class={cn(
216
+ 'flex items-center gap-1.5 mb-2 text-xs font-semibold uppercase tracking-wider hover:text-link',
217
+ megaHeadingClass
218
+ )}
219
+ >
220
+ {column.icon && <Icon name={column.icon} class="w-4 h-4" />}
221
+ {column.text}
222
+ </a>
223
+ ) : (
224
+ <span
225
+ class={cn(
226
+ 'flex items-center gap-1.5 mb-2 text-xs font-semibold uppercase tracking-wider',
227
+ megaHeadingClass
228
+ )}
229
+ >
230
+ {column.icon && <Icon name={column.icon} class="w-4 h-4" />}
231
+ {column.text}
232
+ </span>
233
+ )}
234
+ <ul class="flex flex-col">
235
+ {column.links?.map((leaf) => (
236
+ <li>
237
+ <a
238
+ href={leaf.href}
239
+ class={cn(
240
+ 'group flex items-start gap-3 rounded-md py-2 px-2 -mx-2',
241
+ { 'link-active': leaf.href === currentPath },
242
+ dropdownLinksClass
243
+ )}
244
+ >
245
+ {leaf.icon && (
246
+ <span class="flex-none mt-0.5 flex items-center justify-center w-8 h-8 rounded-md bg-foreground/[0.06] text-foreground/70 group-hover:bg-foreground/10">
247
+ <Icon name={leaf.icon} class="w-4.5 h-4.5" />
248
+ </span>
249
+ )}
250
+ <span class="flex flex-col">
251
+ <span class="font-medium leading-tight whitespace-nowrap">{leaf.text}</span>
252
+ {leaf.description && (
253
+ <span class="mt-0.5 text-xs font-normal text-muted-foreground leading-snug normal-case whitespace-nowrap">
254
+ {leaf.description}
255
+ </span>
256
+ )}
257
+ </span>
258
+ </a>
259
+ </li>
260
+ ))}
261
+ </ul>
262
+ </div>
263
+ ))}
264
+ </div>
265
+ ) : (
266
+ <ul class="flex flex-col p-3 min-w-[14rem]">
267
+ {link.links?.map((leaf) => (
268
+ <li>
269
+ <a
270
+ href={leaf.href}
271
+ class={cn(
272
+ 'flex items-center gap-2 rounded-md py-2 px-3 whitespace-nowrap',
273
+ { 'link-active': leaf.href === currentPath },
274
+ dropdownLinksClass
275
+ )}
276
+ >
277
+ {leaf.icon && <Icon name={leaf.icon} class="w-4 h-4 opacity-70" />}
278
+ {leaf.text}
279
+ </a>
280
+ </li>
281
+ ))}
282
+ </ul>
283
+ )}
284
+ </div>
285
+ ))}
286
+ </div>
287
+ </div>
288
+ )
289
+ }
290
+
291
+ <div
292
+ class:list={[
293
+ { 'ml-auto rtl:ml-0 rtl:mr-auto': position === 'left' },
294
+ 'hidden md:self-center md:flex items-center md:mb-0 fixed w-full md:w-auto md:static justify-end left-0 rtl:left-auto rtl:right-0 bottom-0 p-3 md:p-0 md:justify-self-end',
295
+ ]}
296
+ data-yt-actions
297
+ >
298
+ <div class="items-center flex justify-between w-full md:w-auto">
299
+ <div class="flex">
300
+ {showToggleTheme && (
301
+ <DarkMode initialMode={initialMode} iconClass={cn('w-6 h-6 md:w-5 md:h-5 md:inline-block', darkModeIconClass)} />
302
+ )}
303
+ </div>
304
+
305
+ <!-- Actions Buttons -->
306
+ <slot name="actions">
307
+ {
308
+ actions?.length ? (
309
+ <span class="ml-4 rtl:ml-0 rtl:mr-4">
310
+ {actions.map(({ class: actionClass, ...btnProps }) => (
311
+ <Button
312
+ {...btnProps}
313
+ class={cn('ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto', actionClass)}
314
+ />
315
+ ))}
316
+ </span>
317
+ ) : (
318
+ ''
319
+ )
320
+ }
321
+ </slot>
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </WidgetWrapper>
326
+
327
+ <style>
328
+ #header.expanded nav {
329
+ position: fixed;
330
+ top: 100px;
331
+ left: 0;
332
+ right: 0;
333
+ bottom: 70px !important;
334
+ padding: 0 5px;
335
+ }
336
+
337
+ /* Flyout wrapper: fades + slides in, hidden (not just transparent) when closed
338
+ so it never intercepts clicks. */
339
+ .flyout {
340
+ opacity: 0;
341
+ visibility: hidden;
342
+ transform: translateY(6px);
343
+ transition:
344
+ opacity 0.18s ease,
345
+ transform 0.18s ease,
346
+ visibility 0s linear 0.18s;
347
+ }
348
+ .flyout.is-open {
349
+ opacity: 1;
350
+ visibility: visible;
351
+ transform: translateY(0);
352
+ transition:
353
+ opacity 0.18s ease,
354
+ transform 0.18s ease;
355
+ }
356
+
357
+ /* The visible box that morphs between panels. JS sets width/height/translateX. */
358
+ .flyout-bg {
359
+ width: 0;
360
+ height: 0;
361
+ transition:
362
+ width 0.28s cubic-bezier(0.4, 0, 0.2, 1),
363
+ height 0.28s cubic-bezier(0.4, 0, 0.2, 1),
364
+ transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
365
+ will-change: width, height, transform;
366
+ }
367
+ /* Suppress the size transition on first open so it doesn't grow from 0. */
368
+ .flyout.no-anim .flyout-bg {
369
+ transition: none;
370
+ }
371
+
372
+ /* Panels stack in the same spot and cross-fade. */
373
+ .flyout-panel {
374
+ opacity: 0;
375
+ pointer-events: none;
376
+ transition: opacity 0.16s ease;
377
+ }
378
+ .flyout-panel.is-active {
379
+ opacity: 1;
380
+ pointer-events: auto;
381
+ }
382
+
383
+ @media (prefers-reduced-motion: reduce) {
384
+ .flyout,
385
+ .flyout-bg,
386
+ .flyout-panel {
387
+ transition-duration: 0.01ms;
388
+ }
389
+ }
390
+ </style>
391
+
392
+ <script>
393
+ import { attachEvent, toggleClasses, addClasses, removeClasses } from '../../utils/DOMUtils';
394
+
395
+ // ---- Sticky header pin/unpin on scroll + mobile menu toggle (parity with WidgetNavbar) ----
396
+ let headerBound = false;
397
+ const addHeaderListeners = () => {
398
+ if (headerBound) return;
399
+ const header = document.querySelector('[data-yt-sticky-header]') as HTMLElement;
400
+ if (!header) return;
401
+ headerBound = true;
402
+
403
+ const scrollOffset = Number(header.dataset.scrollOffset) || 0;
404
+ const pinnedClasses = header.dataset.pinnedClasses || '';
405
+ const unpinnedClasses = header.dataset.unpinnedClasses || '';
406
+
407
+ let lastKnownScrollPosition = window.scrollY;
408
+ let ticking = true;
409
+
410
+ attachEvent('[data-yt-toggle-menu]', 'click', (_: Event, elem: HTMLElement) => {
411
+ requestAnimationFrame(() => {
412
+ toggleClasses(elem, 'expanded');
413
+ toggleClasses(document.body, 'overflow-hidden');
414
+ toggleClasses(document.getElementById('header'), 'h-screen', 'expanded');
415
+ toggleClasses('[data-yt-flyout-nav]', 'hidden');
416
+ toggleClasses('[data-yt-actions]', 'hidden');
417
+ });
418
+ });
419
+
420
+ attachEvent('[data-yt-flyout-nav]', 'click', () => hiddenNav());
421
+ attachEvent([window.matchMedia('(max-width: 767px)')], 'change', () => hiddenNav());
422
+
423
+ function hiddenNav() {
424
+ requestAnimationFrame(() => {
425
+ removeClasses('[data-yt-toggle-menu]', 'expanded');
426
+ removeClasses(document.body, 'overflow-hidden');
427
+ removeClasses(document.getElementById('header'), 'h-screen', 'expanded');
428
+ addClasses('[data-yt-flyout-nav]', 'hidden');
429
+ addClasses('[data-yt-actions]', 'hidden');
430
+ });
431
+ }
432
+
433
+ function applyHeaderStylesOnScroll() {
434
+ if (lastKnownScrollPosition > scrollOffset && !header.classList.contains('scroll')) {
435
+ addClasses(header, 'scroll', ...pinnedClasses.split(' '));
436
+ removeClasses(header, ...unpinnedClasses.split(' '));
437
+ } else if (lastKnownScrollPosition <= scrollOffset && header.classList.contains('scroll')) {
438
+ removeClasses(header, 'scroll', ...pinnedClasses.split(' '));
439
+ addClasses(header, ...unpinnedClasses.split(' '));
440
+ }
441
+ ticking = false;
442
+ }
443
+ applyHeaderStylesOnScroll();
444
+
445
+ attachEvent([document], 'scroll', function () {
446
+ lastKnownScrollPosition = window.scrollY;
447
+ if (!ticking) {
448
+ window.requestAnimationFrame(applyHeaderStylesOnScroll);
449
+ ticking = true;
450
+ }
451
+ });
452
+ };
453
+
454
+ // ---- Persistent morphing flyout ----
455
+ const initFlyout = (header: HTMLElement) => {
456
+ if (header.dataset.flyoutBound === 'true') return;
457
+ const positioner = header.querySelector('.relative') as HTMLElement | null;
458
+ const flyout = header.querySelector('[data-flyout]') as HTMLElement | null;
459
+ const bgEl = header.querySelector('[data-flyout-bg]') as HTMLElement | null;
460
+ if (!positioner || !flyout || !bgEl) return;
461
+ header.dataset.flyoutBound = 'true';
462
+
463
+ const triggers = Array.from(header.querySelectorAll<HTMLElement>('[data-flyout-trigger]'));
464
+ const panels = new Map<string, HTMLElement>();
465
+ flyout.querySelectorAll<HTMLElement>('[data-flyout-panel]').forEach((p) => {
466
+ panels.set(p.dataset.flyoutKey!, p);
467
+ });
468
+
469
+ let open = false;
470
+ let currentKey: string | null = null;
471
+ let closeTimer: number | undefined;
472
+
473
+ const triggerByKey = (key: string) => triggers.find((t) => t.dataset.flyoutKey === key) || null;
474
+
475
+ const sizeTo = (key: string) => {
476
+ const panel = panels.get(key);
477
+ const trigger = triggerByKey(key);
478
+ if (!panel) return;
479
+ const contWidth = positioner.clientWidth;
480
+ const w = Math.min(panel.offsetWidth, contWidth);
481
+ const h = panel.offsetHeight;
482
+ let x = 0;
483
+ if (trigger) {
484
+ const cont = positioner.getBoundingClientRect();
485
+ x = Math.max(0, Math.min(trigger.getBoundingClientRect().left - cont.left, contWidth - w));
486
+ }
487
+ bgEl.style.width = `${w}px`;
488
+ bgEl.style.height = `${h}px`;
489
+ bgEl.style.transform = `translateX(${x}px)`;
490
+ panels.forEach((p, k) => p.classList.toggle('is-active', k === key));
491
+ triggers.forEach((t) => t.setAttribute('aria-expanded', String(t.dataset.flyoutKey === key)));
492
+ currentKey = key;
493
+ };
494
+
495
+ const openTo = (key: string) => {
496
+ cancelClose();
497
+ if (key === currentKey && open) return;
498
+ if (!open) {
499
+ // First open: set the target size with no transition so it doesn't grow from 0.
500
+ flyout.classList.add('no-anim');
501
+ sizeTo(key);
502
+ void bgEl.offsetWidth; // force reflow
503
+ flyout.classList.remove('no-anim');
504
+ flyout.classList.add('is-open');
505
+ open = true;
506
+ } else {
507
+ sizeTo(key); // morph
508
+ }
509
+ };
510
+
511
+ const close = () => {
512
+ cancelClose();
513
+ if (!open) return;
514
+ flyout.classList.remove('is-open');
515
+ panels.forEach((p) => p.classList.remove('is-active'));
516
+ triggers.forEach((t) => t.setAttribute('aria-expanded', 'false'));
517
+ open = false;
518
+ currentKey = null;
519
+ };
520
+
521
+ const scheduleClose = () => {
522
+ cancelClose();
523
+ closeTimer = window.setTimeout(close, 140);
524
+ };
525
+ const cancelClose = () => {
526
+ if (closeTimer) {
527
+ clearTimeout(closeTimer);
528
+ closeTimer = undefined;
529
+ }
530
+ };
531
+
532
+ triggers.forEach((trigger) => {
533
+ const key = trigger.dataset.flyoutKey!;
534
+ trigger.addEventListener('mouseenter', () => openTo(key));
535
+ trigger.addEventListener('focus', () => openTo(key));
536
+ trigger.addEventListener('click', (e) => {
537
+ e.preventDefault();
538
+ open && key === currentKey ? close() : openTo(key);
539
+ });
540
+ });
541
+
542
+ // Hovering a plain (non-dropdown) top-level link closes the flyout.
543
+ header.querySelectorAll<HTMLElement>('[data-flyout-dismiss]').forEach((el) => {
544
+ el.addEventListener('mouseenter', scheduleClose);
545
+ });
546
+
547
+ // Keep open while over the panel; close shortly after leaving panel or nav.
548
+ flyout.addEventListener('mouseenter', cancelClose);
549
+ flyout.addEventListener('mouseleave', scheduleClose);
550
+ const nav = header.querySelector('[data-yt-flyout-nav]');
551
+ nav?.addEventListener('mouseleave', scheduleClose);
552
+
553
+ document.addEventListener('keydown', (e) => {
554
+ if (e.key === 'Escape') close();
555
+ });
556
+ document.addEventListener('click', (e) => {
557
+ if (open && !flyout.contains(e.target as Node) && !nav?.contains(e.target as Node)) close();
558
+ });
559
+ window.addEventListener('resize', () => {
560
+ if (open && currentKey) {
561
+ flyout.classList.add('no-anim');
562
+ sizeTo(currentKey);
563
+ void bgEl.offsetWidth;
564
+ flyout.classList.remove('no-anim');
565
+ }
566
+ });
567
+ };
568
+
569
+ const init = () => {
570
+ addHeaderListeners();
571
+ document.querySelectorAll<HTMLElement>('[data-yt-flyout-header]').forEach(initFlyout);
572
+ };
573
+
574
+ init();
575
+ document.addEventListener('astro:after-swap', init);
576
+ </script>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yatoday/astro-ui",
3
3
  "type": "module",
4
- "version": "0.17.32",
4
+ "version": "0.18.0",
5
5
  "scripts": {
6
6
  "prepare": "husky",
7
7
  "pre-commit": "lint-staged",
package/styles/styles.css CHANGED
@@ -84,7 +84,10 @@
84
84
  --tracking-tighter: -0.05em;
85
85
  --tracking-tight: -0.025em;
86
86
  --tracking-wide: 0.025em;
87
+ --tracking-wider: 0.05em;
87
88
  --tracking-widest: 0.1em;
89
+ --leading-tight: 1.25;
90
+ --leading-snug: 1.375;
88
91
  --radius-sm: 0.25rem;
89
92
  --radius-md: 0.375rem;
90
93
  --radius-lg: 0.5rem;
@@ -311,6 +314,9 @@
311
314
  .top-2 {
312
315
  top: calc(var(--spacing) * 2);
313
316
  }
317
+ .top-full {
318
+ top: 100%;
319
+ }
314
320
  .-right-5 {
315
321
  right: calc(var(--spacing) * -5);
316
322
  }
@@ -413,6 +419,12 @@
413
419
  .m-auto {
414
420
  margin: auto;
415
421
  }
422
+ .-mx-2 {
423
+ margin-inline: calc(var(--spacing) * -2);
424
+ }
425
+ .-mx-3 {
426
+ margin-inline: calc(var(--spacing) * -3);
427
+ }
416
428
  .mx-1 {
417
429
  margin-inline: calc(var(--spacing) * 1);
418
430
  }
@@ -606,6 +618,9 @@
606
618
  .h-4 {
607
619
  height: calc(var(--spacing) * 4);
608
620
  }
621
+ .h-4\.5 {
622
+ height: calc(var(--spacing) * 4.5);
623
+ }
609
624
  .h-5 {
610
625
  height: calc(var(--spacing) * 5);
611
626
  }
@@ -690,6 +705,9 @@
690
705
  .w-4 {
691
706
  width: calc(var(--spacing) * 4);
692
707
  }
708
+ .w-4\.5 {
709
+ width: calc(var(--spacing) * 4.5);
710
+ }
693
711
  .w-5 {
694
712
  width: calc(var(--spacing) * 5);
695
713
  }
@@ -777,6 +795,12 @@
777
795
  .max-w-xs {
778
796
  max-width: var(--container-xs);
779
797
  }
798
+ .min-w-\[12rem\] {
799
+ min-width: 12rem;
800
+ }
801
+ .min-w-\[14rem\] {
802
+ min-width: 14rem;
803
+ }
780
804
  .min-w-full {
781
805
  min-width: 100%;
782
806
  }
@@ -856,6 +880,9 @@
856
880
  .cursor-zoom-out {
857
881
  cursor: zoom-out;
858
882
  }
883
+ .resize {
884
+ resize: both;
885
+ }
859
886
  .list-none {
860
887
  list-style-type: none;
861
888
  }
@@ -1249,6 +1276,9 @@
1249
1276
  .p-6 {
1250
1277
  padding: calc(var(--spacing) * 6);
1251
1278
  }
1279
+ .p-8 {
1280
+ padding: calc(var(--spacing) * 8);
1281
+ }
1252
1282
  .\!px-0 {
1253
1283
  padding-inline: calc(var(--spacing) * 0) !important;
1254
1284
  }
@@ -1330,6 +1360,9 @@
1330
1360
  .pb-1 {
1331
1361
  padding-bottom: calc(var(--spacing) * 1);
1332
1362
  }
1363
+ .pb-2 {
1364
+ padding-bottom: calc(var(--spacing) * 2);
1365
+ }
1333
1366
  .pb-4 {
1334
1367
  padding-bottom: calc(var(--spacing) * 4);
1335
1368
  }
@@ -1448,6 +1481,14 @@
1448
1481
  --tw-leading: calc(var(--spacing) * 6);
1449
1482
  line-height: calc(var(--spacing) * 6);
1450
1483
  }
1484
+ .leading-snug {
1485
+ --tw-leading: var(--leading-snug);
1486
+ line-height: var(--leading-snug);
1487
+ }
1488
+ .leading-tight {
1489
+ --tw-leading: var(--leading-tight);
1490
+ line-height: var(--leading-tight);
1491
+ }
1451
1492
  .font-bold {
1452
1493
  --tw-font-weight: var(--font-weight-bold);
1453
1494
  font-weight: var(--font-weight-bold);
@@ -1480,6 +1521,10 @@
1480
1521
  --tw-tracking: var(--tracking-wide);
1481
1522
  letter-spacing: var(--tracking-wide);
1482
1523
  }
1524
+ .tracking-wider {
1525
+ --tw-tracking: var(--tracking-wider);
1526
+ letter-spacing: var(--tracking-wider);
1527
+ }
1483
1528
  .tracking-widest {
1484
1529
  --tw-tracking: var(--tracking-widest);
1485
1530
  letter-spacing: var(--tracking-widest);
@@ -1550,6 +1595,9 @@
1550
1595
  .capitalize {
1551
1596
  text-transform: capitalize;
1552
1597
  }
1598
+ .normal-case {
1599
+ text-transform: none;
1600
+ }
1553
1601
  .uppercase {
1554
1602
  text-transform: uppercase;
1555
1603
  }
@@ -1572,6 +1620,9 @@
1572
1620
  .opacity-25 {
1573
1621
  opacity: 25%;
1574
1622
  }
1623
+ .opacity-70 {
1624
+ opacity: 70%;
1625
+ }
1575
1626
  .opacity-75 {
1576
1627
  opacity: 75%;
1577
1628
  }
@@ -2117,6 +2168,21 @@
2117
2168
  position: static;
2118
2169
  }
2119
2170
  }
2171
+ .md\:top-full {
2172
+ @media (width >= 48rem) {
2173
+ top: 100%;
2174
+ }
2175
+ }
2176
+ .md\:right-0 {
2177
+ @media (width >= 48rem) {
2178
+ right: calc(var(--spacing) * 0);
2179
+ }
2180
+ }
2181
+ .md\:left-0 {
2182
+ @media (width >= 48rem) {
2183
+ left: calc(var(--spacing) * 0);
2184
+ }
2185
+ }
2120
2186
  .md\:order-1 {
2121
2187
  @media (width >= 48rem) {
2122
2188
  order: 1;
@@ -2127,6 +2193,11 @@
2127
2193
  grid-column: span 3 / span 3;
2128
2194
  }
2129
2195
  }
2196
+ .md\:-mx-2 {
2197
+ @media (width >= 48rem) {
2198
+ margin-inline: calc(var(--spacing) * -2);
2199
+ }
2200
+ }
2130
2201
  .md\:-mx-4 {
2131
2202
  @media (width >= 48rem) {
2132
2203
  margin-inline: calc(var(--spacing) * -4);
@@ -2308,6 +2379,16 @@
2308
2379
  gap: calc(var(--spacing) * 16);
2309
2380
  }
2310
2381
  }
2382
+ .md\:gap-x-8 {
2383
+ @media (width >= 48rem) {
2384
+ column-gap: calc(var(--spacing) * 8);
2385
+ }
2386
+ }
2387
+ .md\:gap-y-4 {
2388
+ @media (width >= 48rem) {
2389
+ row-gap: calc(var(--spacing) * 4);
2390
+ }
2391
+ }
2311
2392
  .md\:self-center {
2312
2393
  @media (width >= 48rem) {
2313
2394
  align-self: center;
@@ -2354,6 +2435,16 @@
2354
2435
  padding: calc(var(--spacing) * 0);
2355
2436
  }
2356
2437
  }
2438
+ .md\:p-8 {
2439
+ @media (width >= 48rem) {
2440
+ padding: calc(var(--spacing) * 8);
2441
+ }
2442
+ }
2443
+ .md\:px-2 {
2444
+ @media (width >= 48rem) {
2445
+ padding-inline: calc(var(--spacing) * 2);
2446
+ }
2447
+ }
2357
2448
  .md\:px-6 {
2358
2449
  @media (width >= 48rem) {
2359
2450
  padding-inline: calc(var(--spacing) * 6);
@@ -2419,6 +2510,11 @@
2419
2510
  padding-top: 76px;
2420
2511
  }
2421
2512
  }
2513
+ .md\:pb-0 {
2514
+ @media (width >= 48rem) {
2515
+ padding-bottom: calc(var(--spacing) * 0);
2516
+ }
2517
+ }
2422
2518
  .md\:pb-8 {
2423
2519
  @media (width >= 48rem) {
2424
2520
  padding-bottom: calc(var(--spacing) * 8);
package/svelte.d.ts CHANGED
@@ -49,6 +49,7 @@ import type { SvelteWidgetHero1Props as YtSvelteWidgetHero1Props } from './compo
49
49
  import type { SvelteWidgetHero2Props as YtSvelteWidgetHero2Props } from './components/WidgetHero2/types'
50
50
  import type { SvelteWidgetHeroSliderProps as YtSvelteWidgetHeroSliderProps } from './components/WidgetHeroSlider/types'
51
51
  import type { SvelteWidgetNavbarProps as YtSvelteWidgetNavbarProps } from './components/WidgetNavbar/types'
52
+ import type { SvelteWidgetNavbarFlyoutProps as YtSvelteWidgetNavbarFlyoutProps } from './components/WidgetNavbarFlyout/types'
52
53
  import type { SvelteWidgetStatsProps as YtSvelteWidgetStatsProps } from './components/WidgetStats/types'
53
54
  import type { SvelteWidgetSteps1Props as YtSvelteWidgetSteps1Props } from './components/WidgetSteps1/types'
54
55
  import type { SvelteWidgetStepsTimelineProps as YtSvelteWidgetStepsTimelineProps } from './components/WidgetStepsTimeline/types'
@@ -108,6 +109,7 @@ declare module '@yatoday/astro-ui/svelte' {
108
109
  export const WidgetHero2: Component<YtSvelteWidgetHero2Props>
109
110
  export const WidgetHeroSlider: Component<YtSvelteWidgetHeroSliderProps>
110
111
  export const WidgetNavbar: Component<YtSvelteWidgetNavbarProps>
112
+ export const WidgetNavbarFlyout: Component<YtSvelteWidgetNavbarFlyoutProps>
111
113
  export const WidgetStats: Component<YtSvelteWidgetStatsProps>
112
114
  export const WidgetSteps1: Component<YtSvelteWidgetSteps1Props>
113
115
  export const WidgetStepsTimeline: Component<YtSvelteWidgetStepsTimelineProps>
@@ -166,6 +168,7 @@ declare module '@yatoday/astro-ui/svelte' {
166
168
  export type WidgetHero2Props = YtSvelteWidgetHero2Props
167
169
  export type WidgetHeroSliderProps = YtSvelteWidgetHeroSliderProps
168
170
  export type WidgetNavbarProps = YtSvelteWidgetNavbarProps
171
+ export type WidgetNavbarFlyoutProps = YtSvelteWidgetNavbarFlyoutProps
169
172
  export type WidgetStatsProps = YtSvelteWidgetStatsProps
170
173
  export type WidgetSteps1Props = YtSvelteWidgetSteps1Props
171
174
  export type WidgetStepsTimelineProps = YtSvelteWidgetStepsTimelineProps