1use dioxus::prelude::*;
11use crate::ui::router::Route;
12use crate::ui::components::icon::{Icon, IconVariant, IconSize};
13use crate::content::ContentEngine;
14use crate::ui::pages::guide::content::SECTIONS;
15
16pub(crate) const LOGO_SVG: &str = include_str!("../../../assets/logo.svg");
18
19#[derive(Clone, Copy, PartialEq, Default)]
21pub enum ActivePage {
22 #[default]
23 Guide,
24 Crates,
25 Learn,
26 Studio,
27 Roadmap,
28 Pricing,
29 Registry,
30 Profile,
31 News,
32 Benchmarks,
33 Other,
34}
35
36impl ActivePage {
37 pub fn from_route(route: &Route) -> Self {
39 match route {
40 Route::Landing {} => ActivePage::Other,
41 Route::Guide {} => ActivePage::Guide,
42 Route::Crates {} => ActivePage::Crates,
43 Route::Learn {} => ActivePage::Learn,
44 Route::Studio { .. } => ActivePage::Studio,
45 Route::Workspace { .. } => ActivePage::Studio,
46 Route::Roadmap {} => ActivePage::Roadmap,
47 Route::Pricing {} => ActivePage::Pricing,
48 Route::Success { .. } => ActivePage::Pricing,
49 Route::Registry { .. } => ActivePage::Registry,
50 Route::PackageDetail { .. } => ActivePage::Registry,
51 Route::Profile {} => ActivePage::Profile,
52 Route::News { .. } => ActivePage::News,
53 Route::NewsArticle { .. } => ActivePage::News,
54 Route::Benchmarks {} => ActivePage::Benchmarks,
55 _ => ActivePage::Other,
56 }
57 }
58}
59
60const MAIN_NAV_STYLE: &str = r#"
61.main-nav {
62 position: sticky;
63 top: 0;
64 z-index: 150;
65 backdrop-filter: blur(18px);
66 -webkit-backdrop-filter: blur(18px);
67 background: linear-gradient(180deg, rgba(7,10,18,0.72), rgba(7,10,18,0.44));
68 border-bottom: 1px solid rgba(255,255,255,0.06);
69}
70
71.main-nav-inner {
72 display: flex;
73 align-items: center;
74 justify-content: space-between;
75 padding: var(--spacing-lg) var(--spacing-xl);
76 max-width: 1280px;
77 margin: 0 auto;
78 gap: var(--spacing-lg);
79}
80
81.main-nav-brand {
82 display: flex;
83 align-items: center;
84 gap: var(--spacing-md);
85 text-decoration: none;
86 color: var(--text-primary);
87 z-index: 1001;
88}
89
90.main-nav-logo {
91 width: 64px;
92 height: 64px;
93 border-radius: var(--radius-lg);
94 overflow: hidden;
95 flex-shrink: 0;
96}
97
98.main-nav-logo svg {
99 width: 100%;
100 height: 100%;
101 filter: invert(1);
102}
103
104.main-nav-brand-text {
105 display: flex;
106 flex-direction: column;
107 line-height: 1.05;
108}
109
110.main-nav-brand-name {
111 font-weight: 800;
112 letter-spacing: -0.5px;
113 font-size: var(--font-body-md);
114}
115
116.main-nav-brand-subtitle {
117 font-size: var(--font-caption-sm);
118 color: var(--text-tertiary);
119}
120
121.main-nav-links {
122 display: flex;
123 gap: var(--spacing-xs);
124 align-items: center;
125}
126
127.main-nav-link {
128 position: relative;
129 text-decoration: none;
130 padding: 10px 14px;
131 font-size: var(--font-body-md);
132 font-weight: 500;
133 color: var(--text-secondary);
134 transition: color 0.18s ease;
135 border-radius: var(--radius-md);
136}
137
138.main-nav-link:hover {
139 color: var(--text-primary);
140 background: rgba(255,255,255,0.04);
141}
142
143/* Active underline indicator */
144.main-nav-link::after {
145 content: "";
146 position: absolute;
147 bottom: 2px;
148 left: 14px;
149 right: 14px;
150 height: 2px;
151 background: linear-gradient(90deg, var(--color-accent-blue), var(--color-accent-purple));
152 border-radius: 2px;
153 opacity: 0;
154 transform: scaleX(0);
155 transition: opacity 0.18s ease, transform 0.18s ease;
156}
157
158.main-nav-link.active {
159 color: var(--text-primary);
160}
161
162.main-nav-link.active::after {
163 opacity: 1;
164 transform: scaleX(1);
165}
166
167.main-nav-cta {
168 display: flex;
169 gap: 10px;
170 align-items: center;
171}
172
173.main-nav-btn {
174 display: inline-flex;
175 align-items: center;
176 justify-content: center;
177 gap: var(--spacing-sm);
178 padding: 10px var(--spacing-lg);
179 border-radius: var(--radius-lg);
180 border: 1px solid rgba(255,255,255,0.10);
181 background: rgba(255,255,255,0.05);
182 text-decoration: none;
183 font-weight: 600;
184 font-size: var(--font-body-md);
185 color: var(--text-primary);
186 transition: transform 0.18s ease, background 0.18s ease, border-color 0.18s ease;
187}
188
189.main-nav-btn:hover {
190 transform: translateY(-1px);
191 background: rgba(255,255,255,0.08);
192 border-color: rgba(255,255,255,0.18);
193}
194
195.main-nav-btn.primary {
196 background: linear-gradient(135deg, rgba(0,212,255,0.95), rgba(129,140,248,0.95));
197 border-color: rgba(255,255,255,0.20);
198 color: #09090b;
199 box-shadow: 0 12px 30px rgba(0,212,255,0.18);
200}
201
202.main-nav-btn.primary:hover {
203 background: linear-gradient(135deg, rgba(0,212,255,1.0), rgba(129,140,248,1.0));
204}
205
206.main-nav-btn.ghost {
207 background: rgba(255,255,255,0.03);
208}
209
210.main-nav-btn-icon {
211 padding: 10px;
212 background: rgba(255,255,255,0.03);
213}
214
215.main-nav-btn-icon svg {
216 width: 20px;
217 height: 20px;
218 fill: currentColor;
219}
220
221/* Mobile hamburger button */
222.mobile-menu-btn {
223 display: none;
224 width: 44px;
225 height: 44px;
226 border-radius: 5px;
227 border: 2px solid rgba(255,255,255,0.2);
228 background: rgba(255,255,255,0.05);
229 color: var(--text-primary);
230 cursor: pointer;
231 align-items: center;
232 justify-content: center;
233 z-index: 1001;
234}
235
236.mobile-menu-btn:hover {
237 background: rgba(255,255,255,0.1);
238 border-color: rgba(255,255,255,0.3);
239}
240
241/* Mobile drawer overlay */
242.mobile-drawer-overlay {
243 display: none;
244 position: fixed;
245 inset: 0;
246 background: rgba(0,0,0,0.6);
247 z-index: 999;
248 opacity: 0;
249 pointer-events: none;
250 transition: opacity 0.3s ease;
251}
252
253.mobile-drawer-overlay.open {
254 opacity: 1;
255 pointer-events: auto;
256}
257
258/* Mobile drawer */
259.mobile-drawer {
260 position: fixed;
261 top: 0;
262 right: 0;
263 width: 320px;
264 max-width: 85vw;
265 height: 100vh;
266 background: rgba(12, 16, 24, 0.98);
267 backdrop-filter: blur(20px);
268 -webkit-backdrop-filter: blur(20px);
269 border-left: 1px solid rgba(255,255,255,0.08);
270 z-index: 1000;
271 transform: translateX(100%);
272 transition: transform 0.3s ease;
273 overflow-y: auto;
274 display: none;
275}
276
277.mobile-drawer.open {
278 transform: translateX(0);
279}
280
281.mobile-drawer-header {
282 display: flex;
283 align-items: center;
284 justify-content: space-between;
285 padding: 16px var(--spacing-lg) var(--spacing-lg);
286 border-bottom: 1px solid rgba(255,255,255,0.06);
287}
288
289.mobile-drawer-title {
290 font-weight: 700;
291 font-size: var(--font-body-lg);
292 color: var(--text-primary);
293}
294
295.mobile-drawer-close {
296 width: 36px;
297 height: 36px;
298 border-radius: var(--radius-md);
299 border: 1px solid rgba(255,255,255,0.1);
300 background: rgba(255,255,255,0.03);
301 color: var(--text-secondary);
302 cursor: pointer;
303 display: flex;
304 align-items: center;
305 justify-content: center;
306}
307
308.mobile-drawer-close:hover {
309 background: rgba(255,255,255,0.08);
310 color: var(--text-primary);
311}
312
313.mobile-drawer-content {
314 padding: var(--spacing-md);
315}
316
317/* Mobile nav section */
318.mobile-nav-section {
319 margin-bottom: var(--spacing-md);
320}
321
322.mobile-nav-header {
323 display: flex;
324 align-items: center;
325 justify-content: space-between;
326 padding: var(--spacing-md);
327 border-radius: var(--radius-md);
328 cursor: pointer;
329 transition: background 0.15s ease;
330}
331
332.mobile-nav-header:hover {
333 background: rgba(255,255,255,0.04);
334}
335
336.mobile-nav-header-left {
337 display: flex;
338 align-items: center;
339 gap: var(--spacing-sm);
340}
341
342.mobile-nav-header-icon {
343 color: var(--color-accent-blue);
344}
345
346.mobile-nav-header-title {
347 font-weight: 600;
348 font-size: var(--font-body-md);
349 color: var(--text-primary);
350}
351
352.mobile-nav-header-chevron {
353 color: var(--text-tertiary);
354 transition: transform 0.2s ease;
355}
356
357.mobile-nav-header-chevron.expanded {
358 transform: rotate(90deg);
359}
360
361.mobile-nav-tree {
362 max-height: 0;
363 overflow: hidden;
364 transition: max-height 0.3s ease;
365}
366
367.mobile-nav-tree.expanded {
368 max-height: 2000px;
369}
370
371.mobile-nav-tree-inner {
372 padding: var(--spacing-xs) 0 var(--spacing-sm) var(--spacing-xl);
373 border-left: 2px solid rgba(255,255,255,0.06);
374 margin-left: var(--spacing-lg);
375}
376
377.mobile-nav-item {
378 display: block;
379 padding: var(--spacing-sm) var(--spacing-md);
380 border-radius: var(--radius-sm);
381 color: var(--text-secondary);
382 font-size: var(--font-body-sm);
383 text-decoration: none;
384 transition: all 0.15s ease;
385}
386
387.mobile-nav-item:hover {
388 background: rgba(255,255,255,0.04);
389 color: var(--text-primary);
390}
391
392.mobile-nav-item.active {
393 background: rgba(0,212,255,0.12);
394 color: #00d4ff;
395}
396
397/* Simple mobile link (no tree) */
398.mobile-nav-link {
399 display: flex;
400 align-items: center;
401 gap: var(--spacing-sm);
402 padding: var(--spacing-md);
403 border-radius: var(--radius-md);
404 color: var(--text-secondary);
405 text-decoration: none;
406 transition: all 0.15s ease;
407}
408
409.mobile-nav-link:hover {
410 background: rgba(255,255,255,0.04);
411 color: var(--text-primary);
412}
413
414.mobile-nav-link.active {
415 background: rgba(0,212,255,0.12);
416 color: #00d4ff;
417}
418
419.mobile-nav-link-icon {
420 color: var(--color-accent-blue);
421}
422
423.mobile-nav-divider {
424 height: 1px;
425 background: rgba(255,255,255,0.06);
426 margin: var(--spacing-md) 0;
427}
428
429/* Responsive */
430@media (max-width: 980px) {
431 .main-nav-links {
432 display: none;
433 }
434 .main-nav-brand-text {
435 display: none;
436 }
437 .mobile-menu-btn {
438 display: flex;
439 }
440 .mobile-drawer {
441 display: block;
442 }
443 .mobile-drawer-overlay {
444 display: block;
445 }
446}
447
448@media (max-width: 640px) {
449 .main-nav-inner {
450 padding: var(--spacing-md) var(--spacing-lg);
451 }
452 .main-nav-btn {
453 padding: var(--spacing-sm) var(--spacing-md);
454 font-size: var(--font-caption-md);
455 }
456 .main-nav-logo {
457 width: 48px;
458 height: 48px;
459 }
460}
461"#;
462
463#[component]
465pub fn MainNav(
466 #[props(default)]
468 active: ActivePage,
469 #[props(default)]
471 subtitle: Option<&'static str>,
472 #[props(default = true)]
474 show_nav_links: bool,
475) -> Element {
476 let mut drawer_open = use_signal(|| false);
477 let mut guide_expanded = use_signal(|| false);
478 let mut learn_expanded = use_signal(|| false);
479
480 let content_engine = ContentEngine::new();
482 let eras = content_engine.eras();
483
484 let guide_parts: Vec<(&str, Vec<&crate::ui::pages::guide::content::Section>)> = {
486 let mut parts: Vec<(&str, Vec<&crate::ui::pages::guide::content::Section>)> = Vec::new();
487 for section in SECTIONS {
488 if let Some(part) = parts.iter_mut().find(|(p, _)| *p == section.part) {
489 part.1.push(section);
490 } else {
491 parts.push((section.part, vec![section]));
492 }
493 }
494 parts
495 };
496
497 rsx! {
498 style { "{MAIN_NAV_STYLE}" }
499
500 header { class: "main-nav",
501 div { class: "main-nav-inner",
502 Link {
504 to: Route::Landing {},
505 class: "main-nav-brand",
506 div {
507 class: "main-nav-logo",
508 dangerous_inner_html: "{LOGO_SVG}"
509 }
510 div { class: "main-nav-brand-text",
511 span { class: "main-nav-brand-name", "LOGICAFFEINE" }
512 if let Some(sub) = subtitle {
513 span { class: "main-nav-brand-subtitle", "{sub}" }
514 } else {
515 span { class: "main-nav-brand-subtitle", "Debug your thoughts." }
516 }
517 }
518 }
519
520 if show_nav_links {
522 nav { class: "main-nav-links",
523 Link {
524 to: Route::Guide {},
525 class: if active == ActivePage::Guide { "main-nav-link active" } else { "main-nav-link" },
526 "Syntax Guide"
527 }
528 Link {
529 to: Route::Crates {},
530 class: if active == ActivePage::Crates { "main-nav-link active" } else { "main-nav-link" },
531 "Crates"
532 }
533 Link {
534 to: Route::Learn {},
535 class: if active == ActivePage::Learn { "main-nav-link active" } else { "main-nav-link" },
536 "Learn Logic"
537 }
538 Link {
539 to: Route::Studio { file: None },
540 class: if active == ActivePage::Studio { "main-nav-link active" } else { "main-nav-link" },
541 "Studio"
542 }
543 Link {
544 to: Route::Roadmap {},
545 class: if active == ActivePage::Roadmap { "main-nav-link active" } else { "main-nav-link" },
546 "Roadmap"
547 }
548 Link {
549 to: Route::Pricing {},
550 class: if active == ActivePage::Pricing { "main-nav-link active" } else { "main-nav-link" },
551 "Contact"
552 }
553 Link {
554 to: Route::News { tag: None },
555 class: if active == ActivePage::News { "main-nav-link active" } else { "main-nav-link" },
556 "News"
557 }
558 Link {
559 to: Route::Benchmarks {},
560 class: if active == ActivePage::Benchmarks { "main-nav-link active" } else { "main-nav-link" },
561 "Benchmarks"
562 }
563 }
564 }
565
566 div { class: "main-nav-cta",
568 a {
570 href: "https://github.com/Brahmastra-Labs/logicaffeine",
571 target: "_blank",
572 class: "main-nav-btn main-nav-btn-icon",
573 title: "View on GitHub",
574 svg {
575 xmlns: "http://www.w3.org/2000/svg",
576 view_box: "0 0 24 24",
577 path {
578 d: "M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"
579 }
580 }
581 }
582 Link {
584 to: Route::Profile {},
585 class: if active == ActivePage::Profile { "main-nav-btn main-nav-btn-icon active" } else { "main-nav-btn main-nav-btn-icon" },
586 title: "Your Profile",
587 svg {
588 xmlns: "http://www.w3.org/2000/svg",
589 view_box: "0 0 24 24",
590 path {
591 d: "M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
592 }
593 }
594 }
595 button {
597 class: "mobile-menu-btn",
598 onclick: move |_| drawer_open.set(true),
599 Icon { variant: IconVariant::Menu, size: IconSize::Medium }
600 }
601 }
602 }
603 }
604
605 div {
607 class: if *drawer_open.read() { "mobile-drawer-overlay open" } else { "mobile-drawer-overlay" },
608 onclick: move |_| drawer_open.set(false),
609 }
610
611 div {
613 class: if *drawer_open.read() { "mobile-drawer open" } else { "mobile-drawer" },
614
615 div { class: "mobile-drawer-header",
617 span { class: "mobile-drawer-title", "Navigation" }
618 button {
619 class: "mobile-drawer-close",
620 onclick: move |_| drawer_open.set(false),
621 Icon { variant: IconVariant::Close, size: IconSize::Medium }
622 }
623 }
624
625 div { class: "mobile-drawer-content",
627 div { class: "mobile-nav-section",
629 div {
630 class: "mobile-nav-header",
631 onclick: move |_| {
632 let current = *guide_expanded.read();
633 guide_expanded.set(!current);
634 },
635 div { class: "mobile-nav-header-left",
636 span { class: "mobile-nav-header-icon",
637 Icon { variant: IconVariant::Book, size: IconSize::Medium }
638 }
639 span { class: "mobile-nav-header-title", "Syntax Guide" }
640 }
641 span {
642 class: if *guide_expanded.read() { "mobile-nav-header-chevron expanded" } else { "mobile-nav-header-chevron" },
643 Icon { variant: IconVariant::ChevronRight, size: IconSize::Small }
644 }
645 }
646 div {
647 class: if *guide_expanded.read() { "mobile-nav-tree expanded" } else { "mobile-nav-tree" },
648 div { class: "mobile-nav-tree-inner",
649 for (part_name, sections) in guide_parts.iter() {
650 div { style: "margin-bottom: var(--spacing-sm);",
651 div {
652 style: "font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-tertiary); padding: var(--spacing-xs) var(--spacing-md); margin-bottom: var(--spacing-xs);",
653 "{part_name}"
654 }
655 for section in sections.iter() {
656 a {
657 href: "/guide#{section.id}",
658 class: "mobile-nav-item",
659 onclick: move |_| drawer_open.set(false),
660 "{section.number}. {section.title}"
661 }
662 }
663 }
664 }
665 }
666 }
667 }
668
669 div { class: "mobile-nav-section",
671 div {
672 class: "mobile-nav-header",
673 onclick: move |_| {
674 let current = *learn_expanded.read();
675 learn_expanded.set(!current);
676 },
677 div { class: "mobile-nav-header-left",
678 span { class: "mobile-nav-header-icon",
679 Icon { variant: IconVariant::GraduationCap, size: IconSize::Medium }
680 }
681 span { class: "mobile-nav-header-title", "Learn Logic" }
682 }
683 span {
684 class: if *learn_expanded.read() { "mobile-nav-header-chevron expanded" } else { "mobile-nav-header-chevron" },
685 Icon { variant: IconVariant::ChevronRight, size: IconSize::Small }
686 }
687 }
688 div {
689 class: if *learn_expanded.read() { "mobile-nav-tree expanded" } else { "mobile-nav-tree" },
690 div { class: "mobile-nav-tree-inner",
691 for era in eras.iter() {
692 div { style: "margin-bottom: var(--spacing-sm);",
693 div {
694 style: "font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-tertiary); padding: var(--spacing-xs) var(--spacing-md); margin-bottom: var(--spacing-xs);",
695 "{era.meta.title}"
696 }
697 for module in era.modules.iter() {
698 a {
699 href: "/learn#{module.meta.id}",
700 class: "mobile-nav-item",
701 onclick: move |_| drawer_open.set(false),
702 "{module.meta.title}"
703 }
704 }
705 }
706 }
707 }
708 }
709 }
710
711 div { class: "mobile-nav-divider" }
712
713 Link {
715 to: Route::Studio { file: None },
716 class: if active == ActivePage::Studio { "mobile-nav-link active" } else { "mobile-nav-link" },
717 onclick: move |_| drawer_open.set(false),
718 span { class: "mobile-nav-link-icon",
719 Icon { variant: IconVariant::Beaker, size: IconSize::Medium }
720 }
721 "Studio"
722 }
723 Link {
724 to: Route::Crates {},
725 class: if active == ActivePage::Crates { "mobile-nav-link active" } else { "mobile-nav-link" },
726 onclick: move |_| drawer_open.set(false),
727 span { class: "mobile-nav-link-icon",
728 Icon { variant: IconVariant::Package, size: IconSize::Medium }
729 }
730 "Crates"
731 }
732 Link {
733 to: Route::Roadmap {},
734 class: if active == ActivePage::Roadmap { "mobile-nav-link active" } else { "mobile-nav-link" },
735 onclick: move |_| drawer_open.set(false),
736 span { class: "mobile-nav-link-icon",
737 Icon { variant: IconVariant::Map, size: IconSize::Medium }
738 }
739 "Roadmap"
740 }
741 Link {
742 to: Route::Pricing {},
743 class: if active == ActivePage::Pricing { "mobile-nav-link active" } else { "mobile-nav-link" },
744 onclick: move |_| drawer_open.set(false),
745 span { class: "mobile-nav-link-icon",
746 Icon { variant: IconVariant::Diamond, size: IconSize::Medium }
747 }
748 "Contact"
749 }
750 Link {
751 to: Route::News { tag: None },
752 class: if active == ActivePage::News { "mobile-nav-link active" } else { "mobile-nav-link" },
753 onclick: move |_| drawer_open.set(false),
754 span { class: "mobile-nav-link-icon",
755 Icon { variant: IconVariant::Newspaper, size: IconSize::Medium }
756 }
757 "News"
758 }
759 Link {
760 to: Route::Benchmarks {},
761 class: if active == ActivePage::Benchmarks { "mobile-nav-link active" } else { "mobile-nav-link" },
762 onclick: move |_| drawer_open.set(false),
763 span { class: "mobile-nav-link-icon",
764 Icon { variant: IconVariant::Lightning, size: IconSize::Medium }
765 }
766 "Benchmarks"
767 }
768
769 div { class: "mobile-nav-divider" }
770
771 Link {
772 to: Route::Profile {},
773 class: if active == ActivePage::Profile { "mobile-nav-link active" } else { "mobile-nav-link" },
774 onclick: move |_| drawer_open.set(false),
775 span { class: "mobile-nav-link-icon",
776 Icon { variant: IconVariant::User, size: IconSize::Medium }
777 }
778 "Profile"
779 }
780 a {
781 href: "https://github.com/Brahmastra-Labs/logicaffeine",
782 target: "_blank",
783 class: "mobile-nav-link",
784 span { class: "mobile-nav-link-icon",
785 Icon { variant: IconVariant::Github, size: IconSize::Medium }
786 }
787 "GitHub"
788 }
789 }
790 }
791 }
792}