Skip to main content

logicaffeine_web/ui/
responsive.rs

1//! Responsive design system and mobile styling utilities.
2//!
3//! Centralizes all mobile/responsive concerns to ensure consistent behavior
4//! across device sizes. Provides breakpoints, touch target standards, and
5//! reusable CSS for mobile-first components.
6//!
7//! # Breakpoint Strategy
8//!
9//! Mobile-first approach with these breakpoints:
10//! - `XS` (480px): Small phones
11//! - `SM` (640px): Large phones, small tablets
12//! - `MD` (768px): Tablets (primary mobile breakpoint)
13//! - `LG` (1024px): Small laptops
14//! - `XL` (1280px): Desktops
15//!
16//! # Touch Target Standards
17//!
18//! Follows WCAG 2.5 guidelines:
19//! - Minimum: 44x44px
20//! - Comfortable: 48x48px
21//! - Large (primary actions): 56x56px
22//!
23//! # Usage
24//!
25//! Include base styles in your root component:
26//!
27//! ```no_run
28//! # use dioxus::prelude::*;
29//! use logicaffeine_web::ui::responsive::{MOBILE_BASE_STYLES, all_mobile_styles};
30//!
31//! # fn Example() -> Element {
32//! rsx! {
33//!     // Option 1: Just the base utilities
34//!     style { "{MOBILE_BASE_STYLES}" }
35//!
36//!     // Option 2: All mobile styles including tabs, panels, buttons
37//!     style { "{all_mobile_styles()}" }
38//! }
39//! # }
40//! ```
41//!
42//! # CSS Classes
43//!
44//! | Class | Description |
45//! |-------|-------------|
46//! | `.desktop-only` | Hidden on mobile (≤768px) |
47//! | `.mobile-only` | Hidden on desktop (>768px) |
48//! | `.touch-target` | Minimum 44x44px touch area |
49//! | `.safe-top` | Respects notch/status bar |
50//! | `.safe-bottom` | Respects home indicator |
51//! | `.scroll-smooth` | iOS momentum scrolling |
52
53// =============================================================================
54// BREAKPOINTS
55// =============================================================================
56
57/// Standard breakpoint values used across the application
58pub mod breakpoints {
59    /// Extra small devices (phones in portrait)
60    pub const XS: &str = "480px";
61    /// Small devices (phones in landscape, small tablets)
62    pub const SM: &str = "640px";
63    /// Medium devices (tablets)
64    pub const MD: &str = "768px";
65    /// Large devices (small laptops)
66    pub const LG: &str = "1024px";
67    /// Extra large devices (desktops)
68    pub const XL: &str = "1280px";
69}
70
71/// Media query helpers - use these in your CSS strings
72pub mod media {
73    pub const MOBILE: &str = "@media (max-width: 768px)";
74    pub const TABLET: &str = "@media (min-width: 769px) and (max-width: 1024px)";
75    pub const DESKTOP: &str = "@media (min-width: 1025px)";
76    pub const MOBILE_LANDSCAPE: &str = "@media (max-height: 500px) and (orientation: landscape)";
77    pub const TOUCH_DEVICE: &str = "@media (hover: none) and (pointer: coarse)";
78    pub const REDUCED_MOTION: &str = "@media (prefers-reduced-motion: reduce)";
79}
80
81// =============================================================================
82// TOUCH TARGETS
83// =============================================================================
84
85/// WCAG 2.5 compliant touch target sizes
86pub mod touch {
87    /// Minimum touch target size (44x44px per WCAG 2.5)
88    pub const MIN_TARGET: &str = "44px";
89    /// Comfortable touch target size
90    pub const COMFORTABLE_TARGET: &str = "48px";
91    /// Large touch target for primary actions
92    pub const LARGE_TARGET: &str = "56px";
93}
94
95// =============================================================================
96// BASE MOBILE STYLES
97// =============================================================================
98
99/// Include this in your root component (app.rs) for global mobile utilities
100pub const MOBILE_BASE_STYLES: &str = r#"
101/* ============================================ */
102/* MOBILE CSS VARIABLES                         */
103/* ============================================ */
104:root {
105    /* Touch targets */
106    --touch-min: 44px;
107    --touch-comfortable: 48px;
108    --touch-large: 56px;
109
110    /* Mobile spacing */
111    --mobile-padding: 12px;
112    --mobile-gap: 8px;
113
114    /* Safe area insets for notched devices */
115    --safe-top: env(safe-area-inset-top, 0px);
116    --safe-bottom: env(safe-area-inset-bottom, 0px);
117    --safe-left: env(safe-area-inset-left, 0px);
118    --safe-right: env(safe-area-inset-right, 0px);
119
120    /* Layout heights - standardized across app */
121    --header-height: 72px;
122    --footer-height-desktop: 280px;
123    --footer-height-tablet: 360px;
124    --footer-height-mobile: auto;
125
126    /* Standardized breakpoints (as CSS custom properties for reference) */
127    --breakpoint-mobile: 768px;
128    --breakpoint-tablet: 1024px;
129}
130
131/* ============================================ */
132/* MOBILE UTILITY CLASSES                       */
133/* ============================================ */
134
135/* Hide on mobile, show on desktop */
136.desktop-only {
137    display: block;
138}
139
140/* Show on mobile, hide on desktop */
141.mobile-only {
142    display: none;
143}
144
145@media (max-width: 768px) {
146    .desktop-only {
147        display: none !important;
148    }
149    .mobile-only {
150        display: block !important;
151    }
152    .mobile-only-flex {
153        display: flex !important;
154    }
155}
156
157/* Touch-friendly button base */
158.touch-target {
159    min-width: var(--touch-min);
160    min-height: var(--touch-min);
161    display: flex;
162    align-items: center;
163    justify-content: center;
164    cursor: pointer;
165    -webkit-tap-highlight-color: transparent;
166    touch-action: manipulation;
167}
168
169/* Safe area padding utilities */
170.safe-top {
171    padding-top: max(var(--mobile-padding), var(--safe-top));
172}
173
174.safe-bottom {
175    padding-bottom: max(var(--mobile-padding), var(--safe-bottom));
176}
177
178.safe-horizontal {
179    padding-left: max(var(--mobile-padding), var(--safe-left));
180    padding-right: max(var(--mobile-padding), var(--safe-right));
181}
182
183/* Smooth scrolling with momentum on iOS */
184.scroll-smooth {
185    -webkit-overflow-scrolling: touch;
186    scroll-behavior: smooth;
187}
188
189/* Prevent text selection on interactive elements */
190.no-select {
191    -webkit-user-select: none;
192    user-select: none;
193}
194
195/* Reduced motion support */
196@media (prefers-reduced-motion: reduce) {
197    *,
198    *::before,
199    *::after {
200        animation-duration: 0.01ms !important;
201        animation-iteration-count: 1 !important;
202        transition-duration: 0.01ms !important;
203    }
204}
205
206/* ============================================ */
207/* BODY SCROLL LOCK                            */
208/* ============================================ */
209
210/* Applied to body when mobile menu is open */
211body.menu-open {
212    overflow: hidden;
213    position: fixed;
214    width: 100%;
215    height: 100%;
216}
217
218/* ============================================ */
219/* HAMBURGER MENU & OVERLAY                    */
220/* ============================================ */
221
222/* Hamburger button */
223.hamburger-btn {
224    display: none;
225    width: var(--touch-comfortable);
226    height: var(--touch-comfortable);
227    padding: 0;
228    border: none;
229    background: transparent;
230    cursor: pointer;
231    -webkit-tap-highlight-color: transparent;
232    position: relative;
233    z-index: 1001;
234}
235
236.hamburger-icon {
237    width: 24px;
238    height: 18px;
239    position: relative;
240    display: flex;
241    flex-direction: column;
242    justify-content: space-between;
243}
244
245.hamburger-line {
246    width: 100%;
247    height: 2px;
248    background: var(--text-primary, #f0f0f0);
249    border-radius: 1px;
250    transition: all 0.3s ease;
251    transform-origin: center;
252}
253
254/* Hamburger to X animation */
255.hamburger-btn.open .hamburger-line:nth-child(1) {
256    transform: translateY(8px) rotate(45deg);
257}
258
259.hamburger-btn.open .hamburger-line:nth-child(2) {
260    opacity: 0;
261    transform: scaleX(0);
262}
263
264.hamburger-btn.open .hamburger-line:nth-child(3) {
265    transform: translateY(-8px) rotate(-45deg);
266}
267
268/* Mobile menu overlay */
269.mobile-menu-overlay {
270    position: fixed;
271    top: 0;
272    left: 0;
273    right: 0;
274    bottom: 0;
275    background: rgba(6, 8, 20, 0.98);
276    backdrop-filter: blur(20px);
277    -webkit-backdrop-filter: blur(20px);
278    z-index: 1000;
279    opacity: 0;
280    visibility: hidden;
281    transition: opacity 0.3s ease, visibility 0.3s ease;
282    display: flex;
283    flex-direction: column;
284    padding: calc(var(--header-height) + 20px) var(--mobile-padding) var(--mobile-padding);
285    overflow-y: auto;
286}
287
288.mobile-menu-overlay.open {
289    opacity: 1;
290    visibility: visible;
291}
292
293/* Mobile menu navigation links */
294.mobile-menu-nav {
295    display: flex;
296    flex-direction: column;
297    gap: 8px;
298    margin-bottom: 32px;
299}
300
301.mobile-menu-link {
302    display: flex;
303    align-items: center;
304    gap: 12px;
305    padding: 16px 20px;
306    border-radius: 12px;
307    background: rgba(255, 255, 255, 0.03);
308    color: var(--text-secondary, #b0b0b0);
309    text-decoration: none;
310    font-size: 18px;
311    font-weight: 500;
312    transition: all 0.2s ease;
313    border: 1px solid transparent;
314}
315
316.mobile-menu-link:active {
317    transform: scale(0.98);
318    background: rgba(255, 255, 255, 0.08);
319}
320
321.mobile-menu-link.active {
322    background: rgba(102, 126, 234, 0.15);
323    color: var(--text-primary, #f0f0f0);
324    border-color: rgba(102, 126, 234, 0.3);
325}
326
327.mobile-menu-link-icon {
328    font-size: 20px;
329    width: 24px;
330    text-align: center;
331}
332
333/* Mobile menu CTAs */
334.mobile-menu-ctas {
335    display: flex;
336    flex-direction: column;
337    gap: 12px;
338    margin-top: auto;
339    padding-top: 24px;
340    border-top: 1px solid rgba(255, 255, 255, 0.08);
341}
342
343.mobile-menu-cta {
344    display: flex;
345    align-items: center;
346    justify-content: center;
347    padding: 16px 24px;
348    border-radius: 12px;
349    font-size: 16px;
350    font-weight: 600;
351    text-decoration: none;
352    transition: all 0.2s ease;
353}
354
355.mobile-menu-cta.primary {
356    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
357    color: white;
358}
359
360.mobile-menu-cta.secondary {
361    background: rgba(255, 255, 255, 0.05);
362    border: 1px solid rgba(255, 255, 255, 0.1);
363    color: var(--text-primary, #f0f0f0);
364}
365
366/* Show hamburger on mobile/tablet */
367@media (max-width: 1024px) {
368    .hamburger-btn {
369        display: flex;
370        align-items: center;
371        justify-content: center;
372    }
373
374    .nav-links-desktop {
375        display: none !important;
376    }
377
378    .nav-ctas-desktop {
379        display: none !important;
380    }
381}
382
383/* ============================================ */
384/* TABLET-SPECIFIC ADJUSTMENTS                 */
385/* ============================================ */
386
387@media (min-width: 769px) and (max-width: 1024px) {
388    .mobile-menu-overlay {
389        padding: calc(var(--header-height) + 40px) 40px 40px;
390    }
391
392    .mobile-menu-link {
393        padding: 18px 24px;
394        font-size: 20px;
395    }
396
397    .mobile-menu-nav {
398        max-width: 400px;
399        margin: 0 auto 40px;
400    }
401
402    .mobile-menu-ctas {
403        max-width: 400px;
404        margin: auto auto 0;
405    }
406}
407"#;
408
409// =============================================================================
410// MOBILE TAB BAR COMPONENT STYLES
411// =============================================================================
412
413/// Reusable mobile tab bar styles - use for any tabbed interface on mobile
414pub const MOBILE_TAB_BAR_STYLES: &str = r#"
415/* Mobile Tab Bar Container */
416.mobile-tabs {
417    display: none;
418}
419
420@media (max-width: 768px) {
421    .mobile-tabs {
422        display: flex;
423        gap: 4px;
424        padding: 8px var(--mobile-padding, 12px);
425        background: rgba(0, 0, 0, 0.4);
426        border-bottom: 1px solid rgba(255, 255, 255, 0.08);
427        overflow-x: auto;
428        -webkit-overflow-scrolling: touch;
429        flex-shrink: 0;
430        /* Hide scrollbar but keep functionality */
431        scrollbar-width: none;
432        -ms-overflow-style: none;
433    }
434
435    .mobile-tabs::-webkit-scrollbar {
436        display: none;
437    }
438
439    /* Individual Tab Button */
440    .mobile-tab {
441        flex: 1;
442        min-width: 0;
443        padding: 10px 8px;
444        border: none;
445        border-radius: 10px;
446        background: rgba(255, 255, 255, 0.05);
447        color: #888;
448        font-size: 13px;
449        font-weight: 500;
450        cursor: pointer;
451        transition: all 0.2s ease;
452        display: flex;
453        flex-direction: column;
454        align-items: center;
455        gap: 4px;
456        min-height: var(--touch-min, 44px);
457        -webkit-tap-highlight-color: transparent;
458    }
459
460    .mobile-tab-icon {
461        font-size: 18px;
462        line-height: 1;
463    }
464
465    .mobile-tab-label {
466        font-size: 11px;
467        white-space: nowrap;
468        overflow: hidden;
469        text-overflow: ellipsis;
470        max-width: 100%;
471    }
472
473    .mobile-tab:active {
474        background: rgba(255, 255, 255, 0.15);
475        transform: scale(0.97);
476    }
477
478    .mobile-tab.active {
479        background: rgba(102, 126, 234, 0.25);
480        color: #e8e8e8;
481        border: 1px solid rgba(102, 126, 234, 0.4);
482    }
483
484    /* Tab indicator dots (optional, for swipe hint) */
485    .mobile-tab-indicator {
486        display: flex;
487        justify-content: center;
488        gap: 6px;
489        padding: 6px;
490        background: rgba(0, 0, 0, 0.2);
491    }
492
493    .mobile-tab-dot {
494        width: 6px;
495        height: 6px;
496        border-radius: 50%;
497        background: rgba(255, 255, 255, 0.2);
498        transition: all 0.2s ease;
499    }
500
501    .mobile-tab-dot.active {
502        background: #667eea;
503        width: 18px;
504        border-radius: 3px;
505    }
506}
507
508/* Landscape mobile - horizontal tab layout */
509@media (max-height: 500px) and (orientation: landscape) {
510    .mobile-tabs {
511        padding: 4px 8px;
512    }
513
514    .mobile-tab {
515        padding: 6px 12px;
516        flex-direction: row;
517        gap: 6px;
518        min-height: 36px;
519    }
520
521    .mobile-tab-icon {
522        font-size: 16px;
523    }
524}
525"#;
526
527// =============================================================================
528// MOBILE PANEL STYLES
529// =============================================================================
530
531/// Styles for switchable panel content (used with mobile tabs)
532pub const MOBILE_PANEL_STYLES: &str = r#"
533/* Desktop: show all panels side by side */
534.panel-container {
535    display: flex;
536    flex: 1;
537    overflow: hidden;
538}
539
540.panel {
541    display: flex;
542    flex-direction: column;
543    overflow: hidden;
544    background: rgba(0, 0, 0, 0.3);
545}
546
547.panel-header {
548    padding: 12px 16px;
549    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
550    font-size: 12px;
551    font-weight: 600;
552    text-transform: uppercase;
553    letter-spacing: 0.5px;
554    color: #888;
555    display: flex;
556    justify-content: space-between;
557    align-items: center;
558    flex-shrink: 0;
559}
560
561.panel-content {
562    flex: 1;
563    overflow: auto;
564    -webkit-overflow-scrolling: touch;
565}
566
567@media (max-width: 768px) {
568    .panel-container {
569        flex-direction: column;
570        position: relative;
571    }
572
573    /* On mobile, panels stack and only active one shows */
574    .panel {
575        position: absolute;
576        top: 0;
577        left: 0;
578        right: 0;
579        bottom: 0;
580        opacity: 0;
581        pointer-events: none;
582        transition: opacity 0.15s ease;
583        min-width: unset;
584    }
585
586    .panel.panel-active {
587        position: relative;
588        flex: 1;
589        opacity: 1;
590        pointer-events: auto;
591    }
592
593    /* Panel headers hidden on mobile (tabs replace them) */
594    .panel .panel-header {
595        display: none;
596    }
597
598    /* But show header for active panel if it has controls */
599    .panel.panel-active .panel-header.has-controls {
600        display: flex;
601        padding: 8px 12px;
602        background: rgba(0, 0, 0, 0.2);
603    }
604}
605"#;
606
607// =============================================================================
608// MOBILE BUTTON STYLES
609// =============================================================================
610
611/// Mobile-optimized button styles with proper touch targets
612pub const MOBILE_BUTTON_STYLES: &str = r#"
613@media (max-width: 768px) {
614    /* Ensure all buttons meet touch target requirements */
615    button,
616    .btn,
617    [role="button"] {
618        min-height: var(--touch-min, 44px);
619        min-width: var(--touch-min, 44px);
620        padding: 10px 16px;
621        font-size: 14px;
622    }
623
624    /* Toggle button groups */
625    .toggle-group {
626        gap: 6px;
627        padding: 4px;
628        border-radius: 8px;
629    }
630
631    .toggle-btn {
632        padding: 10px 16px;
633        font-size: 14px;
634        border-radius: 6px;
635        min-height: var(--touch-min, 44px);
636        min-width: var(--touch-min, 44px);
637        display: flex;
638        align-items: center;
639        justify-content: center;
640    }
641
642    /* Small icon buttons */
643    .icon-btn {
644        width: var(--touch-min, 44px);
645        height: var(--touch-min, 44px);
646        padding: 0;
647        display: flex;
648        align-items: center;
649        justify-content: center;
650    }
651
652    .icon-btn svg,
653    .icon-btn .icon {
654        width: 20px;
655        height: 20px;
656    }
657}
658"#;
659
660// =============================================================================
661// MOBILE INPUT STYLES
662// =============================================================================
663
664/// Mobile-optimized form input styles
665pub const MOBILE_INPUT_STYLES: &str = r#"
666@media (max-width: 768px) {
667    /* Text inputs and textareas */
668    input[type="text"],
669    input[type="email"],
670    input[type="password"],
671    input[type="search"],
672    textarea {
673        font-size: 16px; /* Prevents iOS zoom on focus */
674        min-height: var(--touch-min, 44px);
675        padding: 12px 16px;
676        border-radius: 10px;
677    }
678
679    textarea {
680        min-height: 120px;
681        resize: vertical;
682    }
683
684    /* Labels above inputs */
685    label {
686        font-size: 14px;
687        margin-bottom: 6px;
688    }
689
690    /* Form groups */
691    .form-group {
692        margin-bottom: 16px;
693    }
694}
695"#;
696
697// =============================================================================
698// MOBILE RESIZER ALTERNATIVE
699// =============================================================================
700
701/// On mobile, hide desktop resizers entirely
702pub const MOBILE_RESIZER_STYLES: &str = r#"
703.panel-resizer {
704    width: 6px;
705    background: rgba(255, 255, 255, 0.05);
706    cursor: col-resize;
707    transition: background 0.2s ease;
708    flex-shrink: 0;
709}
710
711.panel-resizer:hover,
712.panel-resizer.active {
713    background: rgba(102, 126, 234, 0.5);
714}
715
716@media (max-width: 768px) {
717    .panel-resizer {
718        display: none;
719    }
720}
721"#;
722
723// =============================================================================
724// COMBINED MOBILE STYLES
725// =============================================================================
726
727/// All mobile styles combined - include this for a complete mobile solution
728pub fn all_mobile_styles() -> String {
729    format!(
730        "{}\n{}\n{}\n{}\n{}\n{}",
731        MOBILE_BASE_STYLES,
732        MOBILE_TAB_BAR_STYLES,
733        MOBILE_PANEL_STYLES,
734        MOBILE_BUTTON_STYLES,
735        MOBILE_INPUT_STYLES,
736        MOBILE_RESIZER_STYLES,
737    )
738}
739
740/// Generate a complete mobile-ready style block for a page
741/// This combines the base mobile utilities with any page-specific styles
742pub fn with_mobile_styles(page_styles: &str) -> String {
743    format!("{}\n{}", MOBILE_BASE_STYLES, page_styles)
744}