| """ |
| Progressive disclosure components for better UX |
| Collapsible sections, tooltips, and conditional displays |
| """ |
|
|
| class ProgressiveDisclosure: |
| """Components that reveal information gradually""" |
| |
| @staticmethod |
| def create_collapsible_section(title: str, content: str, initially_open: bool = False, |
| badge_text: str = "", badge_color: str = "#3b82f6"): |
| """Create expandable/collapsible section with optional badge""" |
| section_id = f"section_{hash(title)}" |
| |
| badge_html = "" |
| if badge_text: |
| badge_html = f""" |
| <span style="padding: 3px 10px; background: {badge_color}20; color: {badge_color}; |
| border-radius: 12px; font-size: 11px; font-weight: bold; margin-left: 10px;"> |
| {badge_text} |
| </span> |
| """ |
| |
| return f""" |
| <div class="collapsible-section" style="margin: 1rem 0;"> |
| <button |
| onclick="toggleSection('{section_id}')" |
| class="section-toggle {'open' if initially_open else ''}" |
| aria-expanded="{str(initially_open).lower()}" |
| aria-controls="{section_id}" |
| style="width: 100%; padding: 1rem; background: var(--color-neutral-50); |
| border: 1px solid var(--color-border); border-radius: 0.5rem; |
| text-align: left; font-weight: 600; color: var(--color-text); |
| cursor: pointer; display: flex; justify-content: space-between; |
| align-items: center; transition: background 0.2s;" |
| > |
| <div style="display: flex; align-items: center; gap: 0.5rem;"> |
| <span style="font-size: 1.25rem;">{'▼' if initially_open else '▶'}</span> |
| <span>{title}</span> |
| {badge_html} |
| </div> |
| </button> |
| <div |
| id="{section_id}" |
| class="section-content" |
| style="display: {'block' if initially_open else 'none'}; padding: 1rem; |
| background: white; border: 1px solid var(--color-border); |
| border-top: none; border-radius: 0 0 0.5rem 0.5rem;" |
| aria-hidden="{str(not initially_open).lower()}" |
| > |
| {content} |
| </div> |
| </div> |
| |
| <script> |
| function toggleSection(id) {{ |
| const section = document.getElementById(id); |
| const button = section.previousElementSibling; |
| const isExpanded = section.style.display === 'none'; |
| |
| // Toggle visibility |
| section.style.display = isExpanded ? 'block' : 'none'; |
| section.setAttribute('aria-hidden', !isExpanded); |
| |
| // Update button state |
| button.classList.toggle('open', isExpanded); |
| button.setAttribute('aria-expanded', isExpanded); |
| |
| // Update toggle icon |
| const icon = button.querySelector('span:first-child'); |
| icon.textContent = isExpanded ? '▼' : '▶'; |
| |
| // Smooth scroll if opening |
| if (isExpanded) {{ |
| section.scrollIntoView({{ behavior: 'smooth', block: 'nearest' }}); |
| }} |
| }} |
| </script> |
| """ |
| |
| @staticmethod |
| def create_tooltip(text: str, tooltip_content: str, position: str = "top"): |
| """Create text with a hover tooltip""" |
| position_classes = { |
| "top": "bottom: 125%; left: 50%; margin-left: -100px;", |
| "bottom": "top: 125%; left: 50%; margin-left: -100px;", |
| "left": "top: 50%; right: 125%; margin-top: -15px;", |
| "right": "top: 50%; left: 125%; margin-top: -15px;" |
| } |
| |
| position_css = position_classes.get(position, position_classes["top"]) |
| |
| return f""" |
| <span class="tooltip-container" style="position: relative; display: inline-block;"> |
| {text} |
| <span class="tooltip-text" style=" |
| visibility: hidden; width: 200px; background: var(--color-neutral-800); |
| color: white; text-align: center; padding: 0.5rem; border-radius: 0.375rem; |
| position: absolute; z-index: 1000; {position_css} opacity: 0; |
| transition: opacity 0.3s; font-size: 0.875rem; font-weight: normal; |
| box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); |
| "> |
| {tooltip_content} |
| </span> |
| </span> |
| |
| <style> |
| .tooltip-container:hover .tooltip-text {{ |
| visibility: visible; |
| opacity: 1; |
| }} |
| </style> |
| """ |
| |
| @staticmethod |
| def create_progressive_form(steps: list): |
| """Create a multi-step form with progressive disclosure""" |
| steps_html = "" |
| for i, step in enumerate(steps): |
| step_id = f"step_{i}" |
| steps_html += f""" |
| <div id="{step_id}" class="form-step" |
| style="display: {'block' if i == 0 else 'none'}; margin-bottom: 2rem;"> |
| <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;"> |
| <div style="width: 28px; height: 28px; background: {'var(--color-primary)' if i == 0 else 'var(--color-neutral-300)'}; |
| color: white; border-radius: 50%; display: flex; align-items: center; |
| justify-content: center; font-weight: bold; font-size: 0.875rem;"> |
| {i + 1} |
| </div> |
| <h3 style="margin: 0; font-size: 1.125rem; color: var(--color-text);"> |
| {step['title']} |
| </h3> |
| </div> |
| <div style="background: var(--color-neutral-50); border-radius: 0.5rem; padding: 1.5rem;"> |
| {step['content']} |
| </div> |
| </div> |
| """ |
| |
| |
| nav_html = """ |
| <div style="display: flex; justify-content: space-between; margin-top: 2rem;"> |
| <button onclick="previousStep()" |
| style="padding: 0.5rem 1rem; background: var(--color-neutral-100); |
| border: 1px solid var(--color-border); border-radius: 0.375rem; |
| cursor: pointer; font-weight: 500;" id="prevBtn"> |
| ← Previous |
| </button> |
| <button onclick="nextStep()" |
| style="padding: 0.5rem 1rem; background: var(--color-primary); |
| color: white; border: none; border-radius: 0.375rem; |
| cursor: pointer; font-weight: 500;" id="nextBtn"> |
| Next → |
| </button> |
| </div> |
| """ |
| |
| |
| js = f""" |
| <script> |
| let currentStep = 0; |
| const totalSteps = {len(steps)}; |
| |
| function updateButtons() {{ |
| document.getElementById('prevBtn').style.display = currentStep === 0 ? 'none' : 'block'; |
| document.getElementById('nextBtn').textContent = currentStep === totalSteps - 1 ? 'Finish' : 'Next →'; |
| }} |
| |
| function showStep(stepIndex) {{ |
| // Hide all steps |
| document.querySelectorAll('.form-step').forEach(step => {{ |
| step.style.display = 'none'; |
| }}); |
| |
| // Show current step |
| document.getElementById('step_' + stepIndex).style.display = 'block'; |
| currentStep = stepIndex; |
| updateButtons(); |
| |
| // Smooth scroll to step |
| document.getElementById('step_' + stepIndex).scrollIntoView({{ |
| behavior: 'smooth', |
| block: 'start' |
| }}); |
| }} |
| |
| function nextStep() {{ |
| if (currentStep < totalSteps - 1) {{ |
| showStep(currentStep + 1); |
| }} else {{ |
| // Form completion logic |
| alert('Form completed!'); |
| }} |
| }} |
| |
| function previousStep() {{ |
| if (currentStep > 0) {{ |
| showStep(currentStep - 1); |
| }} |
| }} |
| |
| // Initialize |
| updateButtons(); |
| </script> |
| """ |
| |
| return f""" |
| <div style="max-width: 800px; margin: 0 auto;"> |
| {steps_html} |
| {nav_html} |
| {js} |
| </div> |
| """ |
| |
| @staticmethod |
| def create_accordion(items: list, allow_multiple: bool = False): |
| """Create an accordion with multiple collapsible items""" |
| accordion_id = f"accordion_{hash(str(items))}" |
| |
| items_html = "" |
| for i, item in enumerate(items): |
| item_id = f"{accordion_id}_item_{i}" |
| items_html += f""" |
| <div style="border: 1px solid var(--color-border); border-radius: 0.5rem; |
| margin-bottom: 0.5rem; overflow: hidden;"> |
| <button |
| onclick="toggleAccordionItem('{item_id}', {str(allow_multiple).lower()})" |
| style="width: 100%; padding: 1rem; background: var(--color-neutral-50); |
| border: none; text-align: left; font-weight: 600; color: var(--color-text); |
| cursor: pointer; display: flex; justify-content: space-between; |
| align-items: center; transition: background 0.2s;" |
| > |
| <span>{item['title']}</span> |
| <span style="font-size: 1.25rem; transition: transform 0.2s;" id="{item_id}_icon"> |
| ▼ |
| </span> |
| </button> |
| <div |
| id="{item_id}" |
| style="display: none; padding: 1rem; background: white;" |
| > |
| {item['content']} |
| </div> |
| </div> |
| """ |
| |
| return f""" |
| <div id="{accordion_id}"> |
| {items_html} |
| </div> |
| |
| <script> |
| function toggleAccordionItem(itemId, allowMultiple) {{ |
| const content = document.getElementById(itemId); |
| const icon = document.getElementById(itemId + '_icon'); |
| |
| if (!allowMultiple) {{ |
| // Close all other items in this accordion |
| const accordion = document.getElementById('{accordion_id}'); |
| accordion.querySelectorAll('div[id^="{accordion_id}_item_"]').forEach(item => {{ |
| if (item.id !== itemId) {{ |
| item.style.display = 'none'; |
| const otherIcon = document.getElementById(item.id + '_icon'); |
| if (otherIcon) otherIcon.textContent = '▼'; |
| }} |
| }}); |
| }} |
| |
| // Toggle current item |
| if (content.style.display === 'none') {{ |
| content.style.display = 'block'; |
| icon.textContent = '▲'; |
| }} else {{ |
| content.style.display = 'none'; |
| icon.textContent = '▼'; |
| }} |
| }} |
| </script> |
| """ |