Skip to main content

logicaffeine_web/ui/pages/
roadmap.rs

1//! Development roadmap page.
2//!
3//! Two tiers: the curated `roadmap_data::get_milestones` capability cards on
4//! top (rendered by `MilestoneItem`), and a terse, scrollable release history
5//! below, generated from `CHANGELOG.md` into
6//! [`roadmap_history`](super::roadmap_history). Each release links to its news
7//! article when one exists. Milestones carry a [`Status`], feature tags, and
8//! interactive English → FOL examples (toggle between Simple FOL and Unicode
9//! output).
10//!
11//! # Route
12//!
13//! Accessed via [`Route::Roadmap`].
14
15use dioxus::prelude::*;
16#[cfg(all(feature = "split", target_arch = "wasm32"))]
17use dioxus::wasm_split;
18use crate::ui::components::main_nav::{MainNav, ActivePage};
19use crate::ui::components::footer::Footer;
20use crate::ui::components::icon::{Icon, IconVariant, IconSize};
21use crate::ui::pages::roadmap_data::{get_milestones, Milestone, Status};
22use crate::ui::pages::roadmap_history::{get_history, news_slug_for, Release};
23use crate::ui::router::Route;
24use crate::ui::seo::{JsonLdMultiple, PageHead, organization_schema, roadmap_schema, breadcrumb_schema, BreadcrumbItem, pages as seo_pages};
25
26const ROADMAP_STYLE: &str = r#"
27.roadmap-container {
28    min-height: 100vh;
29    background: linear-gradient(135deg, #070a12 0%, #0b1022 50%, #070a12 100%);
30    color: #e5e7eb;
31    font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
32}
33
34.roadmap-nav {
35    position: sticky;
36    top: 0;
37    z-index: 50;
38    backdrop-filter: blur(18px);
39    background: linear-gradient(180deg, rgba(7,10,18,0.72), rgba(7,10,18,0.44));
40    border-bottom: 1px solid rgba(255,255,255,0.06);
41    padding: 16px 20px;
42}
43
44.roadmap-nav-inner {
45    max-width: 1000px;
46    margin: 0 auto;
47    display: flex;
48    align-items: center;
49    justify-content: space-between;
50}
51
52.roadmap-brand {
53    display: flex;
54    align-items: center;
55    gap: 12px;
56    text-decoration: none;
57    color: #e5e7eb;
58}
59
60.roadmap-logo {
61    width: 36px;
62    height: 36px;
63    border-radius: 12px;
64    background:
65        radial-gradient(circle at 30% 30%, rgba(96,165,250,0.85), transparent 55%),
66        radial-gradient(circle at 65% 60%, rgba(167,139,250,0.85), transparent 55%),
67        rgba(255,255,255,0.06);
68    border: 1px solid rgba(255,255,255,0.10);
69}
70
71.roadmap-brand-name {
72    font-weight: 800;
73    font-size: 14px;
74    letter-spacing: -0.5px;
75}
76
77.roadmap-back {
78    color: #a78bfa;
79    text-decoration: none;
80    font-size: 14px;
81    padding: 8px 16px;
82    border-radius: 8px;
83    border: 1px solid rgba(167,139,250,0.3);
84    transition: all 0.2s ease;
85}
86
87.roadmap-back:hover {
88    background: rgba(167,139,250,0.1);
89    border-color: rgba(167,139,250,0.5);
90}
91
92.roadmap-hero {
93    text-align: center;
94    padding: 60px 20px 40px;
95    max-width: 800px;
96    margin: 0 auto;
97}
98
99.roadmap-hero h1 {
100    font-size: 42px;
101    font-weight: 800;
102    letter-spacing: -1px;
103    background: linear-gradient(180deg, #ffffff 0%, rgba(229,231,235,0.78) 100%);
104    -webkit-background-clip: text;
105    -webkit-text-fill-color: transparent;
106    margin-bottom: 12px;
107}
108
109.roadmap-hero .version {
110    display: inline-block;
111    font-size: 14px;
112    padding: 6px 14px;
113    border-radius: 20px;
114    background: rgba(167,139,250,0.15);
115    border: 1px solid rgba(167,139,250,0.3);
116    color: #a78bfa;
117    margin-bottom: 16px;
118}
119
120.roadmap-hero p {
121    color: rgba(229,231,235,0.72);
122    font-size: 18px;
123    line-height: 1.6;
124}
125
126.timeline {
127    max-width: 700px;
128    margin: 0 auto;
129    padding: 0 20px 80px;
130    position: relative;
131}
132
133.timeline::before {
134    content: "";
135    position: absolute;
136    left: 28px;
137    top: 0;
138    bottom: 80px;
139    width: 3px;
140    background: linear-gradient(
141        180deg,
142        #22c55e 0%,
143        #22c55e 86%,
144        #a78bfa 90%,
145        #a78bfa 100%
146    );
147    border-radius: 2px;
148}
149
150.milestone {
151    position: relative;
152    padding-left: 70px;
153    margin-bottom: 40px;
154}
155
156.milestone-dot {
157    position: absolute;
158    left: 16px;
159    top: 4px;
160    width: 24px;
161    height: 24px;
162    border-radius: 50%;
163    display: flex;
164    align-items: center;
165    justify-content: center;
166    font-size: 12px;
167    font-weight: 600;
168}
169
170.milestone-dot.done {
171    background: linear-gradient(135deg, #22c55e, #16a34a);
172    box-shadow: 0 0 20px rgba(34,197,94,0.4);
173}
174
175.milestone-dot.progress {
176    background: linear-gradient(135deg, #a78bfa, #8b5cf6);
177    box-shadow: 0 0 20px rgba(167,139,250,0.4);
178    animation: pulse 2s ease-in-out infinite;
179}
180
181.milestone-dot.planned {
182    background: rgba(255,255,255,0.1);
183    border: 2px solid rgba(255,255,255,0.2);
184}
185
186@keyframes pulse {
187    0%, 100% { transform: scale(1); }
188    50% { transform: scale(1.1); }
189}
190
191.milestone-content {
192    background: rgba(255,255,255,0.03);
193    border: 1px solid rgba(255,255,255,0.08);
194    border-radius: 16px;
195    padding: 24px;
196    transition: all 0.2s ease;
197}
198
199.milestone-content:hover {
200    background: rgba(255,255,255,0.05);
201    border-color: rgba(255,255,255,0.12);
202}
203
204.milestone-header {
205    display: flex;
206    align-items: center;
207    gap: 12px;
208    margin-bottom: 12px;
209}
210
211.milestone-title {
212    font-size: 20px;
213    font-weight: 700;
214    color: #fff;
215}
216
217.milestone-badge {
218    font-size: 11px;
219    font-weight: 600;
220    padding: 4px 10px;
221    border-radius: 12px;
222    text-transform: uppercase;
223    letter-spacing: 0.5px;
224}
225
226.milestone-badge.done {
227    background: rgba(34,197,94,0.15);
228    color: #22c55e;
229    border: 1px solid rgba(34,197,94,0.3);
230}
231
232.milestone-badge.progress {
233    background: rgba(167,139,250,0.15);
234    color: #a78bfa;
235    border: 1px solid rgba(167,139,250,0.3);
236}
237
238.milestone-badge.planned {
239    background: rgba(255,255,255,0.05);
240    color: rgba(255,255,255,0.5);
241    border: 1px solid rgba(255,255,255,0.1);
242}
243
244.milestone-desc {
245    color: rgba(229,231,235,0.72);
246    font-size: 14px;
247    line-height: 1.6;
248    margin-bottom: 16px;
249}
250
251.milestone-features {
252    display: flex;
253    flex-wrap: wrap;
254    gap: 8px;
255}
256
257.feature-tag {
258    font-size: 12px;
259    padding: 6px 12px;
260    border-radius: 8px;
261    background: rgba(255,255,255,0.04);
262    border: 1px solid rgba(255,255,255,0.08);
263    color: rgba(229,231,235,0.8);
264}
265
266.feature-tag.done {
267    background: rgba(34,197,94,0.08);
268    border-color: rgba(34,197,94,0.2);
269    color: #86efac;
270}
271
272.roadmap-footer {
273    border-top: 1px solid rgba(255,255,255,0.06);
274    padding: 24px 20px;
275    text-align: center;
276    color: rgba(229,231,235,0.56);
277    font-size: 13px;
278}
279
280.roadmap-footer a {
281    color: rgba(229,231,235,0.72);
282    text-decoration: none;
283    margin: 0 8px;
284}
285
286.roadmap-footer a:hover {
287    color: #a78bfa;
288}
289
290.roadmap-nav-links {
291    display: flex;
292    align-items: center;
293    gap: 12px;
294}
295
296.roadmap-github {
297    display: flex;
298    align-items: center;
299    justify-content: center;
300    width: 36px;
301    height: 36px;
302    border-radius: 8px;
303    border: 1px solid rgba(255,255,255,0.1);
304    background: rgba(255,255,255,0.03);
305    color: rgba(229,231,235,0.72);
306    transition: all 0.2s ease;
307}
308
309.roadmap-github:hover {
310    background: rgba(255,255,255,0.08);
311    color: #e5e7eb;
312    border-color: rgba(255,255,255,0.2);
313}
314
315.roadmap-github svg {
316    width: 18px;
317    height: 18px;
318    fill: currentColor;
319}
320
321.github-link {
322    display: inline-flex;
323    align-items: center;
324    gap: 6px;
325}
326
327@media (max-width: 600px) {
328    .timeline::before {
329        left: 18px;
330    }
331    .milestone {
332        padding-left: 50px;
333    }
334    .milestone-dot {
335        left: 6px;
336        width: 20px;
337        height: 20px;
338    }
339    .milestone-title {
340        font-size: 18px;
341    }
342}
343
344.milestone-examples {
345    margin-top: 16px;
346    border-top: 1px solid rgba(255,255,255,0.06);
347    padding-top: 16px;
348}
349
350.milestone-tabs {
351    display: flex;
352    gap: 6px;
353    margin-bottom: 12px;
354    flex-wrap: wrap;
355}
356
357.milestone-tab {
358    padding: 6px 12px;
359    border-radius: 6px;
360    border: 1px solid rgba(255,255,255,0.1);
361    background: rgba(255,255,255,0.03);
362    color: #94a3b8;
363    cursor: pointer;
364    font-size: 12px;
365    font-weight: 500;
366    transition: all 0.2s ease;
367}
368
369.milestone-tab:hover {
370    background: rgba(255,255,255,0.08);
371    color: #e8e8e8;
372}
373
374.milestone-tab.active {
375    background: linear-gradient(135deg, #667eea, #764ba2);
376    color: white;
377    border-color: transparent;
378}
379
380.milestone-code {
381    background: rgba(0,0,0,0.25);
382    border-radius: 8px;
383    padding: 16px;
384    font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
385    font-size: 13px;
386}
387
388.milestone-english {
389    color: #e8e8e8;
390    font-style: italic;
391    margin-bottom: 8px;
392    white-space: pre-wrap;
393    line-height: 1.5;
394}
395
396.milestone-english.source {
397    font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
398    font-style: normal;
399    font-size: 12px;
400    color: #cbd5e1;
401}
402
403.milestone-arrow {
404    color: #667eea;
405    margin: 8px 0;
406    font-size: 16px;
407}
408
409.milestone-output {
410    color: #98c379;
411    white-space: pre-wrap;
412    line-height: 1.4;
413}
414
415.format-toggle {
416    display: flex;
417    gap: 4px;
418    margin-bottom: 8px;
419}
420
421.format-btn {
422    padding: 3px 8px;
423    border-radius: 4px;
424    border: 1px solid rgba(255,255,255,0.1);
425    background: rgba(255,255,255,0.03);
426    color: #64748b;
427    cursor: pointer;
428    font-size: 10px;
429    font-weight: 500;
430    text-transform: uppercase;
431    letter-spacing: 0.5px;
432    transition: all 0.15s ease;
433}
434
435.format-btn:hover {
436    background: rgba(255,255,255,0.06);
437    color: #94a3b8;
438}
439
440.format-btn.active {
441    background: rgba(102,126,234,0.2);
442    border-color: rgba(102,126,234,0.4);
443    color: #a5b4fc;
444}
445"#;
446
447#[component]
448fn MilestoneExamples(examples: &'static [crate::ui::pages::roadmap_data::Example]) -> Element {
449    use crate::ui::pages::roadmap_data::Output;
450    let mut active = use_signal(|| 0usize);
451    let mut use_unicode = use_signal(|| false);
452
453    let ex = examples[active()];
454
455    rsx! {
456        div { class: "milestone-examples",
457            div { class: "milestone-tabs",
458                for (i, e) in examples.iter().enumerate() {
459                    button {
460                        key: "{i}",
461                        class: if active() == i { "milestone-tab active" } else { "milestone-tab" },
462                        onclick: move |_| active.set(i),
463                        "{e.label}"
464                    }
465                }
466            }
467            div { class: "milestone-code",
468                {match ex.output {
469                    Output::Fol { .. } => rsx! {
470                        div { class: "milestone-english", "\"{ex.english}\"" }
471                    },
472                    _ => rsx! {
473                        div { class: "milestone-english source", "{ex.english}" }
474                    },
475                }}
476                div { class: "milestone-arrow", "↓" }
477                {match ex.output {
478                    Output::Fol { simple, unicode } => rsx! {
479                        div { class: "format-toggle",
480                            button {
481                                class: if !use_unicode() { "format-btn active" } else { "format-btn" },
482                                onclick: move |_| use_unicode.set(false),
483                                "Simple"
484                            }
485                            button {
486                                class: if use_unicode() { "format-btn active" } else { "format-btn" },
487                                onclick: move |_| use_unicode.set(true),
488                                "Unicode"
489                            }
490                        }
491                        div { class: "milestone-output",
492                            if use_unicode() { "{unicode}" } else { "{simple}" }
493                        }
494                    },
495                    Output::Rust(code) => rsx! {
496                        div { class: "format-toggle",
497                            span { class: "format-btn active", "Rust" }
498                        }
499                        div { class: "milestone-output", "{code}" }
500                    },
501                    Output::Sva(code) => rsx! {
502                        div { class: "format-toggle",
503                            span { class: "format-btn active", "SVA" }
504                        }
505                        div { class: "milestone-output", "{code}" }
506                    },
507                }}
508            }
509        }
510    }
511}
512
513fn milestone_dot_icon(status: Status) -> IconVariant {
514    match status {
515        Status::Complete => IconVariant::Check,
516        Status::InProgress => IconVariant::Clock,
517        Status::Planned => IconVariant::Sparkles,
518    }
519}
520
521#[component]
522fn MilestoneItem(milestone: &'static Milestone) -> Element {
523    let status = milestone.status;
524    let dot_class = format!("milestone-dot {}", status.css_class());
525    let badge_class = format!("milestone-badge {}", status.css_class());
526    let tag_class = match status {
527        Status::Complete => "feature-tag done",
528        _ => "feature-tag",
529    };
530
531    rsx! {
532        div { class: "milestone",
533            div { class: "{dot_class}",
534                Icon { variant: milestone_dot_icon(status), size: IconSize::Small, color: "#fff" }
535            }
536            div { class: "milestone-content",
537                div { class: "milestone-header",
538                    span { class: "milestone-title", "{milestone.title}" }
539                    span { class: "{badge_class}", "{status.label()}" }
540                }
541                p { class: "milestone-desc", "{milestone.description}" }
542                div { class: "milestone-features",
543                    for (i, feature) in milestone.features.iter().enumerate() {
544                        span { key: "{i}", class: "{tag_class}", "{feature}" }
545                    }
546                }
547                if !milestone.examples.is_empty() {
548                    MilestoneExamples { examples: milestone.examples }
549                }
550            }
551        }
552    }
553}
554
555const RELEASE_HISTORY_STYLE: &str = r#"
556.roadmap-section-title {
557    max-width: 700px;
558    margin: 8px auto 18px;
559    padding: 0 20px;
560    font-size: 13px;
561    font-weight: 600;
562    text-transform: uppercase;
563    letter-spacing: 1px;
564    color: rgba(229,231,235,0.5);
565}
566
567.release-history {
568    max-width: 700px;
569    margin: 0 auto;
570    padding: 0 20px 80px;
571}
572
573.release-list {
574    display: flex;
575    flex-direction: column;
576    gap: 2px;
577}
578
579.release-row {
580    display: flex;
581    align-items: baseline;
582    gap: 14px;
583    padding: 10px 14px;
584    border-radius: 10px;
585    border: 1px solid transparent;
586    text-decoration: none;
587    color: inherit;
588    transition: background 0.15s ease, border-color 0.15s ease;
589}
590
591.release-row.linked {
592    cursor: pointer;
593}
594
595.release-row.linked:hover {
596    background: rgba(255,255,255,0.04);
597    border-color: rgba(255,255,255,0.08);
598}
599
600.release-version {
601    font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
602    font-weight: 700;
603    font-size: 13px;
604    color: #a78bfa;
605    min-width: 64px;
606}
607
608.release-date {
609    color: rgba(229,231,235,0.45);
610    font-size: 12px;
611    min-width: 92px;
612    font-variant-numeric: tabular-nums;
613}
614
615.release-title {
616    flex: 1;
617    color: rgba(229,231,235,0.85);
618    font-size: 14px;
619}
620
621.release-unreleased {
622    font-size: 10px;
623    font-weight: 600;
624    text-transform: uppercase;
625    letter-spacing: 0.5px;
626    color: #a78bfa;
627    border: 1px solid rgba(167,139,250,0.3);
628    border-radius: 10px;
629    padding: 2px 8px;
630}
631
632.release-arrow {
633    color: rgba(229,231,235,0.25);
634    font-size: 14px;
635    transition: color 0.15s ease;
636}
637
638.release-row.linked:hover .release-arrow {
639    color: #a78bfa;
640}
641
642.release-row.maintenance {
643    opacity: 0.5;
644}
645
646.release-row.maintenance .release-version {
647    color: rgba(167,139,250,0.6);
648}
649
650.maint-toggle {
651    display: inline-block;
652    margin: 0 0 14px;
653    padding: 6px 12px;
654    border-radius: 8px;
655    border: 1px solid rgba(255,255,255,0.1);
656    background: rgba(255,255,255,0.03);
657    color: rgba(229,231,235,0.6);
658    cursor: pointer;
659    font-size: 12px;
660    font-weight: 500;
661    transition: background 0.15s ease, color 0.15s ease;
662}
663
664.maint-toggle:hover {
665    background: rgba(255,255,255,0.06);
666    color: #e5e7eb;
667}
668
669@media (max-width: 600px) {
670    .release-date { display: none; }
671}
672"#;
673
674#[component]
675fn ReleaseRowBody(release: &'static Release, unreleased: bool) -> Element {
676    rsx! {
677        span { class: "release-version", "v{release.version}" }
678        span { class: "release-date", "{release.date}" }
679        span { class: "release-title", "{release.title}" }
680        if unreleased {
681            span { class: "release-unreleased", "Unreleased" }
682        }
683    }
684}
685
686#[component]
687fn ReleaseRow(release: &'static Release, unreleased: bool) -> Element {
688    let maint = if release.maintenance { " maintenance" } else { "" };
689    match news_slug_for(&release.version) {
690        Some(slug) => rsx! {
691            Link {
692                to: Route::NewsArticle { slug: slug.to_string() },
693                class: "release-row linked{maint}",
694                ReleaseRowBody { release, unreleased }
695                span { class: "release-arrow", "→" }
696            }
697        },
698        None => rsx! {
699            div { class: "release-row{maint}",
700                ReleaseRowBody { release, unreleased }
701            }
702        },
703    }
704}
705
706#[component(lazy)]
707pub fn Roadmap() -> Element {
708    let breadcrumbs = vec![
709        BreadcrumbItem { name: "Home", path: "/" },
710        BreadcrumbItem { name: "Roadmap", path: "/roadmap" },
711    ];
712    let schemas = vec![
713        organization_schema(),
714        roadmap_schema(),
715        breadcrumb_schema(&breadcrumbs),
716    ];
717
718    // Releases are newest-first; the ones above the newest git tag are genuinely
719    // unreleased (prepared, not yet cut). Older untagged releases predate tagging
720    // and were released, so they are not badged.
721    let history = get_history();
722    let first_tagged = history.iter().position(|r| r.tagged).unwrap_or(history.len());
723    let maint_count = history.iter().filter(|r| r.maintenance).count();
724    let mut show_maint = use_signal(|| false);
725
726    rsx! {
727        PageHead {
728            title: seo_pages::ROADMAP.title,
729            description: seo_pages::ROADMAP.description,
730            canonical_path: seo_pages::ROADMAP.canonical_path,
731        }
732        style { "{ROADMAP_STYLE}" }
733        style { "{RELEASE_HISTORY_STYLE}" }
734        JsonLdMultiple { schemas }
735
736        div { class: "roadmap-container",
737            MainNav { active: ActivePage::Roadmap, subtitle: Some("Where we're headed") }
738
739            section { class: "roadmap-hero",
740                h1 { "LOGOS Roadmap" }
741                span { class: "version", "v0.10.0" }
742                p { "From English sentences to a verified compilation stack: first-order logic, a native execution tier, hardware model checking, and a kernel-certified proof core." }
743            }
744
745            h2 { class: "roadmap-section-title", "Featured milestones" }
746            div { class: "timeline",
747                for (i, milestone) in get_milestones().iter().enumerate() {
748                    MilestoneItem { key: "{i}", milestone }
749                }
750            }
751
752            h2 { class: "roadmap-section-title", "Release history" }
753            section { class: "release-history",
754                if maint_count > 0 {
755                    button {
756                        class: "maint-toggle",
757                        onclick: move |_| { let on = show_maint(); show_maint.set(!on); },
758                        if show_maint() {
759                            "Hide maintenance releases"
760                        } else {
761                            "Show {maint_count} maintenance releases (CI, benchmarks, tooling)"
762                        }
763                    }
764                }
765                div { class: "release-list",
766                    for (i, release) in history.iter().enumerate() {
767                        if !release.maintenance || show_maint() {
768                            ReleaseRow { key: "{i}", release, unreleased: i < first_tagged }
769                        }
770                    }
771                }
772            }
773
774            Footer {}
775        }
776    }
777}