Agentic-Reliability-Framework-API / ui /enhancements /progressive_disclosure.py
petter2025's picture
Create enhancements/progressive_disclosure.py
1084c42 verified
raw
history blame
11.5 kB
"""
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>
"""
# Navigation buttons HTML
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>
"""
# JavaScript for step navigation
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>
"""