@sofidevo/astro-dynamic-header 3.0.1 → 4.0.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/src/NavMenu.astro CHANGED
@@ -1,29 +1,15 @@
1
1
  ---
2
2
  import type { MenuItem, SecondaryMenuItem, TertiaryMenuItem } from "./index.js";
3
+ import ChevronIcon from "./ChevronIcon.astro";
3
4
 
4
5
  export interface Props {
5
- /**
6
- * Header type for styling.
7
- * @example "floating"
8
- */
9
6
  type?: "floating" | "fullscreen";
10
- /** Array of menu items to display. */
11
7
  menuItems?: MenuItem[];
12
- /**
13
- * Whether to show the home link in the menu.
14
- * @default true
15
- */
16
8
  showHomeLink?: boolean;
17
- /**
18
- * Text for the home link.
19
- * @default "Home"
20
- */
21
9
  homeText?: string;
22
- /** Extra CSS class(es) for the `<nav>` element. */
10
+ homeUrl?: string;
23
11
  header__menu__class?: string;
24
- /** Extra CSS class(es) for each top-level `<li>` item. */
25
12
  header__item__class?: string;
26
- /** Extra CSS class(es) for each top-level menu `<a>` link. */
27
13
  menu__link__class?: string;
28
14
  }
29
15
 
@@ -31,6 +17,7 @@ const {
31
17
  menuItems = [],
32
18
  showHomeLink = true,
33
19
  homeText = "Home",
20
+ homeUrl = "/",
34
21
  header__menu__class,
35
22
  header__item__class,
36
23
  menu__link__class,
@@ -40,115 +27,19 @@ const pagePathname = Astro.url.pathname;
40
27
  ---
41
28
 
42
29
  <script>
43
- function adjustSubsubmenuPositionOnResize() {
44
- const subsubmenus = document.querySelectorAll(
45
- ".subsubmenu",
46
- ) as NodeListOf<HTMLElement>;
47
-
48
- subsubmenus.forEach((subsubmenu) => {
49
- const rect = subsubmenu.getBoundingClientRect();
50
- const viewportWidth = window.innerWidth;
51
-
52
- if (rect.right > viewportWidth) {
53
- subsubmenu.style.right = "100%";
54
- subsubmenu.style.left = "0";
55
- } else {
56
- subsubmenu.style.left = "100%";
57
- subsubmenu.style.right = "0";
58
- }
59
- });
60
- }
30
+ import { initNavMenu } from "./scripts/nav-menu.js";
61
31
 
62
- function handleSubsubmenuInteraction() {
63
- const parentItems = document.querySelectorAll(".submenu__item--secondary");
64
-
65
- parentItems.forEach((parentItem) => {
66
- const subsubmenu = parentItem.querySelector(".subsubmenu");
67
-
68
- if (subsubmenu) {
69
- const adjustPosition = () => {
70
- const viewportWidth = window.innerWidth;
71
-
72
- const originalDisplay = (subsubmenu as HTMLElement).style.display;
73
- const originalVisibility = (subsubmenu as HTMLElement).style
74
- .visibility;
75
-
76
- (subsubmenu as HTMLElement).style.display = "block";
77
- (subsubmenu as HTMLElement).style.visibility = "hidden";
78
- (subsubmenu as HTMLElement).style.left = "100%";
79
- (subsubmenu as HTMLElement).style.transform = "auto";
80
-
81
- const rect = subsubmenu.getBoundingClientRect();
82
-
83
- if (rect.right > viewportWidth) {
84
- (subsubmenu as HTMLElement).style.left = "-100%";
85
- (subsubmenu as HTMLElement).style.right = "auto";
86
- const leftRect = subsubmenu.getBoundingClientRect();
87
-
88
- if (leftRect.left < 0) {
89
- (subsubmenu as HTMLElement).style.left = "100%";
90
- const overflow = rect.right - viewportWidth + 30;
91
- (subsubmenu as HTMLElement).style.transform =
92
- `translateX(-${overflow}px)`;
93
- }
94
- }
95
- (subsubmenu as HTMLElement).style.display = originalDisplay;
96
- (subsubmenu as HTMLElement).style.visibility = originalVisibility;
97
- };
98
-
99
- const showSubsubmenu = () => {
100
- adjustPosition();
101
-
102
- (subsubmenu as HTMLElement).style.opacity = "1";
103
- (subsubmenu as HTMLElement).style.pointerEvents = "all";
104
-
105
- const isPositionedLeft =
106
- (subsubmenu as HTMLElement).style.left === "-100%";
107
- const transformDirection = isPositionedLeft ? "-32px" : "32px";
108
-
109
- const currentTransform = (subsubmenu as HTMLElement).style.transform;
110
- if (currentTransform && currentTransform.includes("translateX")) {
111
- const existingTranslate = currentTransform.match(
112
- /translateX\(([^)]+)\)/,
113
- );
114
- if (existingTranslate) {
115
- const existingValue = existingTranslate[1];
116
- (subsubmenu as HTMLElement).style.transform =
117
- `translateX(calc(${existingValue} + ${transformDirection}))`;
118
- }
119
- } else {
120
- (subsubmenu as HTMLElement).style.transform =
121
- `${currentTransform} translateX(${transformDirection})`.trim();
122
- }
123
- };
124
-
125
- const hideSubsubmenu = () => {
126
- (subsubmenu as HTMLElement).style.opacity = "";
127
- (subsubmenu as HTMLElement).style.pointerEvents = "";
128
- (subsubmenu as HTMLElement).style.transform = "";
129
- };
130
-
131
- parentItem.addEventListener("mouseenter", showSubsubmenu);
132
- parentItem.addEventListener("mouseleave", hideSubsubmenu);
133
- }
134
- });
135
- }
136
-
137
- window.addEventListener("resize", adjustSubsubmenuPositionOnResize);
138
- document.addEventListener("DOMContentLoaded", () => {
139
- adjustSubsubmenuPositionOnResize();
140
- handleSubsubmenuInteraction();
141
- });
32
+ initNavMenu();
142
33
  </script>
143
34
 
144
35
  <nav class:list={["header__menu", header__menu__class]} id="header-menu">
145
36
  <ul class="menu">
146
37
  {
147
- showHomeLink && pagePathname !== "/" && (
38
+ showHomeLink && pagePathname !== homeUrl && (
148
39
  <li class:list={["header__item", header__item__class]}>
149
40
  <a
150
41
  class:list={["menu__link", menu__link__class]}
151
- href="/"
42
+ href={homeUrl}
152
43
  data-astro-prefetch="hover"
153
44
  >
154
45
  {homeText}
@@ -162,12 +53,7 @@ const pagePathname = Astro.url.pathname;
162
53
  <a class:list={["menu__link", menu__link__class]} href={item.link}>
163
54
  {item.text}
164
55
  {item.submenu && (
165
- <iconify-icon
166
- class="iconify-arrow"
167
- icon="codicon:triangle-down"
168
- width="15"
169
- height="15"
170
- />
56
+ <ChevronIcon class="arrow-down" width={15} height={15} />
171
57
  )}
172
58
  </a>
173
59
  {item.submenu && (
@@ -177,11 +63,10 @@ const pagePathname = Astro.url.pathname;
177
63
  <a href={sub.link}>
178
64
  {sub.text}
179
65
  {sub.submenu && sub.submenu.length > 0 && (
180
- <iconify-icon
181
- class="iconify-arrow"
182
- icon="codicon:triangle-down"
183
- width="15"
184
- height="15"
66
+ <ChevronIcon
67
+ class="ChevronIcon-arrow"
68
+ width={15}
69
+ height={15}
185
70
  />
186
71
  )}
187
72
  </a>
@@ -204,186 +89,195 @@ const pagePathname = Astro.url.pathname;
204
89
  </ul>
205
90
  </nav>
206
91
 
207
- <style is:inline>
208
- .header__menu {
209
- @media (width < 768px) {
210
- display: none;
92
+ <style>
93
+ /*
94
+ * base (resets) < components (ours) < utilities (consumer classNames).
95
+ * Order statement keeps it deterministic regardless of bundling order.
96
+ */
97
+ @layer theme, base, components, utilities;
98
+
99
+ @layer components {
100
+ .header__menu {
101
+ @media (width < 768px) {
102
+ display: none;
103
+ }
211
104
  }
212
- }
213
105
 
214
- .menu {
215
- list-style: none;
216
- display: flex;
217
- align-items: center;
218
- flex-wrap: wrap;
106
+ .menu {
107
+ list-style: none;
108
+ display: flex;
109
+ align-items: center;
110
+ flex-wrap: wrap;
219
111
 
220
- @media (width < 768px) {
221
- flex-direction: column;
112
+ @media (width < 768px) {
113
+ flex-direction: column;
114
+ }
222
115
  }
223
- }
224
116
 
225
- .menu__link {
226
- text-decoration: none;
227
- padding: 0.5em;
228
- display: flex;
229
- align-items: center;
230
- gap: 0.8em;
231
- color: color-mix(
232
- in srgb,
233
- var(--accent-color, #00ffff) 30%,
234
- var(--text-color, #ffffff) 70%
235
- );
236
- position: relative;
237
- }
238
-
239
- @media screen and (min-width: 768px) {
240
- .menu__link::after {
241
- position: absolute;
242
- top: 33px;
243
- left: 0;
244
- content: "";
245
- width: 100%;
246
- transform: scale(0);
247
- transform-origin: 1;
248
- height: 2px;
249
- transition: transform 0.3s ease;
250
- background: var(--accent-color, #00ffff);
117
+ .menu__link {
118
+ text-decoration: none;
119
+ padding: 0.5em;
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 0.8em;
123
+ color: color-mix(
124
+ in srgb,
125
+ var(--accent-color, #00ffff) 30%,
126
+ var(--text-color, #ffffff) 70%
127
+ );
128
+ position: relative;
251
129
  }
252
130
 
253
- .menu__link:hover::after {
254
- transform: scale(1);
131
+ @media screen and (min-width: 768px) {
132
+ .menu__link::after {
133
+ position: absolute;
134
+ top: 33px;
135
+ left: 0;
136
+ content: "";
137
+ width: 100%;
138
+ transform: scale(0);
139
+ transform-origin: 1;
140
+ height: 2px;
141
+ transition: transform 0.3s ease;
142
+ background: var(--accent-color, #00ffff);
143
+ }
144
+
145
+ .menu__link:hover::after {
146
+ transform: scale(1);
147
+ }
255
148
  }
256
- }
257
149
 
258
- .iconify-arrow {
259
- transition: transform 0.3s linear;
260
- }
150
+ .ChevronIcon-arrow {
151
+ transition: transform 0.3s linear;
152
+ }
261
153
 
262
- .menu__link:hover > .iconify-arrow {
263
- transform: rotate(180deg);
264
- }
154
+ .menu__link:hover > .ChevronIcon-arrow {
155
+ transform: rotate(180deg);
156
+ }
265
157
 
266
- .submenu,
267
- .subsubmenu {
268
- color: var(--text-color, #ffffff);
269
- background-color: var(--bg-color-opaque, #171717);
270
- border: 0.5px solid
271
- color-mix(in srgb, var(--accent-color, #3e1c71) 30%, transparent);
272
- position: absolute;
273
- flex-direction: column;
274
- gap: 0.4em;
275
- padding: 1em;
276
- border-radius: 10px;
277
- transition: transform 0.3s ease-in-out;
278
- pointer-events: none;
279
- opacity: 0;
280
- text-wrap: nowrap;
281
- }
158
+ .submenu,
159
+ .subsubmenu {
160
+ color: var(--text-color, #ffffff);
161
+ background-color: var(--bg-color-opaque, #171717);
162
+ border: 0.5px solid
163
+ color-mix(in srgb, var(--accent-color, #3e1c71) 30%, transparent);
164
+ position: absolute;
165
+ flex-direction: column;
166
+ gap: 0.4em;
167
+ padding: 1em;
168
+ border-radius: 10px;
169
+ transition: transform 0.3s ease-in-out;
170
+ pointer-events: none;
171
+ opacity: 0;
172
+ text-wrap: nowrap;
173
+ }
282
174
 
283
- .menu__item {
284
- padding: 6px;
285
- position: relative;
286
- list-style: none;
287
- }
175
+ .menu__item {
176
+ padding: 6px;
177
+ position: relative;
178
+ list-style: none;
179
+ }
288
180
 
289
- .submenu a,
290
- .subsubmenu a {
291
- display: flex;
292
- justify-content: space-between;
293
- align-items: center;
294
- width: 100%;
295
- height: 64px;
296
- border-radius: 6px;
297
- color: var(--text-color, #fff);
298
- }
181
+ .submenu a,
182
+ .subsubmenu a {
183
+ display: flex;
184
+ justify-content: space-between;
185
+ align-items: center;
186
+ width: 100%;
187
+ height: 64px;
188
+ border-radius: 6px;
189
+ color: var(--text-color, #fff);
190
+ }
299
191
 
300
- .submenu__item--secondary,
301
- .submenu__item--tertiary {
302
- position: relative;
303
- }
192
+ .submenu__item--secondary,
193
+ .submenu__item--tertiary {
194
+ position: relative;
195
+ }
304
196
 
305
- .submenu__item--secondary a,
306
- .submenu__item--tertiary a {
307
- padding: 26px;
308
- }
197
+ .submenu__item--secondary a,
198
+ .submenu__item--tertiary a {
199
+ padding: 26px;
200
+ }
309
201
 
310
- .submenu__item--secondary a:hover,
311
- .submenu__item--tertiary a:hover {
312
- background: color-mix(
313
- in srgb,
314
- var(--accent-color, #00ffff) 15%,
315
- transparent
316
- );
317
- color: var(--text-color, white);
318
- }
202
+ .submenu__item--secondary a:hover,
203
+ .submenu__item--tertiary a:hover {
204
+ background: color-mix(
205
+ in srgb,
206
+ var(--accent-color, #00ffff) 15%,
207
+ transparent
208
+ );
209
+ color: var(--text-color, white);
210
+ }
319
211
 
320
- .submenu {
321
- padding: 0.9em;
322
- right: calc(50% - 80px);
323
- }
212
+ .submenu {
213
+ padding: 0.9em;
214
+ right: calc(50% - 80px);
215
+ }
324
216
 
325
- .submenu::after {
326
- content: "";
327
- position: absolute;
328
- top: -4em;
329
- width: 100%;
330
- z-index: -3;
331
- pointer-events: none;
332
- height: 10em;
333
- }
217
+ .submenu::after {
218
+ content: "";
219
+ position: absolute;
220
+ top: -1.1em;
221
+ width: 100%;
222
+ z-index: -3;
223
+ pointer-events: none;
224
+ height: 7em;
225
+ right: 0;
226
+ }
334
227
 
335
- .subsubmenu {
336
- top: 0;
337
- left: auto;
338
- right: auto;
339
- min-width: 193px;
340
- }
228
+ .subsubmenu {
229
+ top: 0;
230
+ left: auto;
231
+ right: auto;
232
+ min-width: 193px;
233
+ }
341
234
 
342
- .subsubmenu::before {
343
- content: "";
344
- position: absolute;
345
- top: -4em;
346
- z-index: -1;
347
- pointer-events: none;
348
- height: 10em;
349
- width: 20em;
350
- height: 14em;
351
- }
235
+ .subsubmenu::before {
236
+ content: "";
237
+ position: absolute;
238
+ top: -4em;
239
+ z-index: -1;
240
+ pointer-events: none;
241
+ height: 10em;
242
+ width: 20em;
243
+ height: 14em;
244
+ }
352
245
 
353
- .subsubmenu::after {
354
- content: "";
355
- position: absolute;
356
- top: -14px;
357
- right: 3em;
358
- width: 100%;
359
- z-index: -3;
360
- height: 21em;
361
- }
246
+ .subsubmenu::after {
247
+ content: "";
248
+ position: absolute;
249
+ top: -14px;
250
+ right: 3em;
251
+ width: 100%;
252
+ z-index: -3;
253
+ height: 21em;
254
+ }
362
255
 
363
- .submenu li {
364
- width: 100%;
365
- }
256
+ .submenu li {
257
+ width: 100%;
258
+ }
366
259
 
367
- .menu__item:hover > .submenu {
368
- opacity: 1;
369
- pointer-events: all;
370
- transform: translateY(1.4em);
371
- }
260
+ .menu__item:hover > .submenu {
261
+ opacity: 1;
262
+ pointer-events: all;
263
+ transform: translateY(1.4em);
264
+ }
372
265
 
373
- .menu__item:hover > .submenu::after {
374
- pointer-events: all;
375
- }
266
+ .menu__item:hover > .submenu::after {
267
+ pointer-events: all;
268
+ }
376
269
 
377
- .submenu li:hover > .subsubmenu {
378
- opacity: 1;
379
- pointer-events: all;
380
- }
270
+ .submenu li:hover > .subsubmenu {
271
+ opacity: 1;
272
+ pointer-events: all;
273
+ }
381
274
 
382
- .submenu li:hover > .subsubmenu::before {
383
- pointer-events: all;
384
- }
275
+ .submenu li:hover > .subsubmenu::before {
276
+ pointer-events: all;
277
+ }
385
278
 
386
- .submenu li:hover > .subsubmenu::after {
387
- pointer-events: all;
279
+ .submenu li:hover > .subsubmenu::after {
280
+ pointer-events: all;
281
+ }
388
282
  }
389
283
  </style>
@@ -0,0 +1,42 @@
1
+ ---
2
+ const props = Astro.props;
3
+
4
+ const viewBox = "0 0 24 24";
5
+ const content = `<g class="gutruu"><path fill-opacity="0" class="xkoryc"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.4s" values="34;0"/><animate fill="freeze" attributeName="fill-opacity" begin="0.5s" dur="0.4s" to="1"/></path><g class="ft5dvn"><path d="M12 21v1M21 12h1M12 3v-1M3 12h-1" opacity="0"><animateTransform attributeName="transform" dur="30s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/><set fill="freeze" attributeName="opacity" begin="1s" to="1"/><animate fill="freeze" attributeName="d" begin="1s" dur="0.2s" values="M12 19v1M19 12h1M12 5v-1M5 12h-1;M12 21v1M21 12h1M12 3v-1M3 12h-1"/></path><path d="M18.5 18.5l0.5 0.5M18.5 5.5l0.5 -0.5M5.5 5.5l-0.5 -0.5M5.5 18.5l-0.5 0.5" opacity="0"><animateTransform attributeName="transform" dur="30s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/><set fill="freeze" attributeName="opacity" begin="1.2s" to="1"/><animate fill="freeze" attributeName="d" begin="1.2s" dur="0.2s" values="M17 17l0.5 0.5M17 7l0.5 -0.5M7 7l-0.5 -0.5M7 17l-0.5 0.5;M18.5 18.5l0.5 0.5M18.5 5.5l0.5 -0.5M5.5 5.5l-0.5 -0.5M5.5 18.5l-0.5 0.5"/></path></g></g>`;
6
+ ---
7
+
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ width="25px"
11
+ height="25px"
12
+ viewBox={viewBox}
13
+ {...props}
14
+ set:html={content}
15
+ />
16
+
17
+ <style is:inline>
18
+ /*
19
+ * base (resets) < components (ours) < utilities (consumer classNames).
20
+ * Order statement keeps it deterministic regardless of bundling order.
21
+ */
22
+ @layer theme, base, components, utilities;
23
+
24
+ @layer components {
25
+ .gutruu {
26
+ stroke: currentColor;
27
+ stroke-linecap: round;
28
+ stroke-linejoin: round;
29
+ stroke-width: var(--svg-stroke-width--2px, 2px);
30
+ }
31
+ .xkoryc {
32
+ fill: currentColor;
33
+ stroke-dasharray: 34;
34
+ d: path(
35
+ "M12 7c2.76 0 5 2.24 5 5c0 2.76 -2.24 5 -5 5c-2.76 0 -5 -2.24 -5 -5c0 -2.76 2.24 -5 5 -5Z"
36
+ );
37
+ }
38
+ .ft5dvn {
39
+ fill: none;
40
+ }
41
+ }
42
+ </style>
package/src/index.ts CHANGED
@@ -129,8 +129,12 @@ export interface DualThemeConfig {
129
129
  * Custom CSS class names for high-level layout & appearance customization.
130
130
  *
131
131
  * These target the structural wrapper elements of the Header. For fine-grained
132
- * control over individual nav links or the logo internals, use the nested
133
- * `xxx__class` props inside the `navigation` or `logo` config objects instead.
132
+ * control over the nav items use the `xxx__class` props inside `navigation`,
133
+ * and style the logo directly on the markup you pass to the `logo` slot.
134
+ *
135
+ * The component styles live in `@layer components`, so any class passed here
136
+ * (Tailwind utilities included) wins over the built-in styles — no
137
+ * `!important` and no `twMerge()` required.
134
138
  *
135
139
  * @example
136
140
  * ```astro
@@ -142,10 +146,6 @@ export interface HeaderClassNames {
142
146
  container?: string;
143
147
  /** Inner `<header>` element — best place for shadows, borders, transitions. */
144
148
  header?: string;
145
- /** Logo anchor `<a>` — add hover states or focus rings here. */
146
- logo?: string;
147
- /** Logo text `<span>` — override typography here. */
148
- logoText?: string;
149
149
  /** Desktop nav wrapper `<div>` — adjust spacing between logo and menu. */
150
150
  nav?: string;
151
151
  /** Mobile nav panel `<nav>` — add slide-in overrides or z-index tweaks. */
@@ -192,18 +192,25 @@ export interface HeaderProps {
192
192
  }
193
193
 
194
194
  export interface NavMenuProps {
195
- menuItems?: MenuItemType[];
195
+ type?: "floating" | "fullscreen";
196
+ menuItems?: MenuItem[];
196
197
  showHomeLink?: boolean;
197
198
  homeText?: string;
199
+ header__menu__class?: string;
200
+ header__item__class?: string;
201
+ menu__link__class?: string;
198
202
  }
199
203
 
200
204
  export interface MobileNavProps {
201
- menuItems?: MenuItemType[];
205
+ type?: "floating" | "fullscreen";
206
+ menuItems?: MenuItem[];
202
207
  showHomeLink?: boolean;
203
208
  homeText?: string;
209
+ mobileNav__class?: string;
204
210
  accentColor?: string;
205
211
  }
206
212
 
207
213
  export interface HamburgerButtonProps {
208
214
  color?: string;
209
215
  }
216
+
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Behaviour for the hamburger button + mobile panel.
3
+ * Extracted from `Header.astro` so it can be unit tested.
4
+ */
5
+
6
+ export class HamburgerController {
7
+ private readonly hamburgerBtn: HTMLElement | null;
8
+ private readonly mobileMenu: HTMLElement | null;
9
+ private readonly links: NodeListOf<HTMLElement>;
10
+
11
+ constructor(doc: Document = document) {
12
+ this.hamburgerBtn = doc.getElementById("hamburger-btn");
13
+ this.mobileMenu = doc.getElementById("mobile-header-menu");
14
+ this.links = doc.querySelectorAll(".mobile-menu__link");
15
+ this.init();
16
+ }
17
+
18
+ private init(): void {
19
+ if (!this.hamburgerBtn || !this.mobileMenu) return;
20
+
21
+ this.hamburgerBtn.addEventListener("click", () => this.toggleMenu());
22
+ this.links.forEach((link) => {
23
+ link.addEventListener("click", () => this.closeMenu());
24
+ });
25
+ }
26
+
27
+ closeMenu(): void {
28
+ this.hamburgerBtn?.classList.remove("is-active");
29
+ this.mobileMenu?.classList.remove("is-active");
30
+ }
31
+
32
+ toggleMenu(): void {
33
+ this.hamburgerBtn?.classList.toggle("is-active");
34
+ this.mobileMenu?.classList.toggle("is-active");
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Wires the hamburger as soon as the DOM is ready (module scripts may run
40
+ * before or after `DOMContentLoaded`, so both paths are handled).
41
+ */
42
+ export function initHamburger(doc: Document = document): HamburgerController | null {
43
+ if (doc.readyState === "loading") {
44
+ let controller: HamburgerController | null = null;
45
+ doc.addEventListener(
46
+ "DOMContentLoaded",
47
+ () => {
48
+ controller = new HamburgerController(doc);
49
+ },
50
+ { once: true },
51
+ );
52
+ return controller;
53
+ }
54
+ return new HamburgerController(doc);
55
+ }