mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-15 13:22:19 +01:00
feat: add modal overlay for type definitions
Implement a modal popup system for viewing type definitions without leaving the current context, with browser-like back/forward navigation. Modal UI Components: - Full-screen overlay with centered modal dialog - Header with title, close button, and navigation controls - Scrollable content area for type definition - Back (←) and forward (→) navigation buttons - Visual disabled state for navigation at history bounds Navigation Features: - Click any type reference in root schema to open in modal - Click type references within modal to navigate deeper - Keyboard shortcuts: Alt+← (back), Alt+→ (forward), Escape (close) - History tracking with array and index pointer - History resets on modal close JavaScript Implementation: - `openTypeModal(typeName, addToHistory)`: Open/navigate to type - `showType(typeName)`: Clone and display type definition content - `navigateHistory(direction)`: Handle back/forward navigation - `closeModal()`: Close modal and reset state - `updateNavButtons()`: Enable/disable nav buttons based on position CSS Styling (102 lines added): - `.modal-overlay`: Fixed positioning with semi-transparent background - `.modal`: Centered container with max dimensions and shadow - `.modal-header`: Flex layout for nav, title, and close button - `.modal-nav-btn`: Styled navigation buttons with disabled state - `.modal-content`: Scrollable content with padding Identifier Updates: - Add modal-related IDs and classes to identifiers.rs - New ID: `modal-overlay` - New classes: modal, modal-active, modal-header, modal-title, modal-close, modal-content, modal-nav, modal-nav-btn, disabled
This commit is contained in:
1 parent
f32d94d929
commit
c6835ddbb2
7 files changed
+312
-8
No files matched your search
@@ -1686,4 +1686,138 @@ mod tests {
|
||||
broken.iter().take(20).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
// Test: Modal has navigation buttons
|
||||
#[test]
|
||||
fn test_modal_has_navigation_buttons() {
|
||||
let generator = Generator::new().unwrap();
|
||||
let schema = make_schema(json!({
|
||||
"title": "Test",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": { "type": "string" }
|
||||
}
|
||||
}));
|
||||
let options = Options::default();
|
||||
let html = generator.generate(&schema, &options).unwrap();
|
||||
|
||||
// Should have modal navigation container
|
||||
assert!(
|
||||
html.contains("modal-nav"),
|
||||
"Should have modal-nav container"
|
||||
);
|
||||
// Should have back and forward buttons
|
||||
assert!(
|
||||
html.contains("data-dir=\"back\""),
|
||||
"Should have back button"
|
||||
);
|
||||
assert!(
|
||||
html.contains("data-dir=\"forward\""),
|
||||
"Should have forward button"
|
||||
);
|
||||
// Should have navigation button class
|
||||
assert!(
|
||||
html.contains("modal-nav-btn"),
|
||||
"Should have nav button class"
|
||||
);
|
||||
}
|
||||
|
||||
// Test: Modal nav buttons start disabled
|
||||
#[test]
|
||||
fn test_modal_nav_buttons_start_disabled() {
|
||||
let generator = Generator::new().unwrap();
|
||||
let schema = make_schema(json!({
|
||||
"title": "Test",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": { "type": "string" }
|
||||
}
|
||||
}));
|
||||
let options = Options::default();
|
||||
let html = generator.generate(&schema, &options).unwrap();
|
||||
|
||||
// Both nav buttons should start with disabled class
|
||||
assert!(
|
||||
html.contains("modal-nav-btn disabled"),
|
||||
"Nav buttons should start disabled"
|
||||
);
|
||||
}
|
||||
|
||||
// Test: Modal has keyboard navigation
|
||||
#[test]
|
||||
fn test_modal_has_keyboard_navigation() {
|
||||
let generator = Generator::new().unwrap();
|
||||
let schema = make_schema(json!({
|
||||
"title": "Test",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": { "type": "string" }
|
||||
}
|
||||
}));
|
||||
let options = Options::default();
|
||||
let html = generator.generate(&schema, &options).unwrap();
|
||||
|
||||
// Should have Alt+Arrow keyboard handling
|
||||
assert!(html.contains("altKey"), "Should handle Alt key modifier");
|
||||
assert!(
|
||||
html.contains("ArrowLeft"),
|
||||
"Should handle ArrowLeft for back"
|
||||
);
|
||||
assert!(
|
||||
html.contains("ArrowRight"),
|
||||
"Should handle ArrowRight for forward"
|
||||
);
|
||||
}
|
||||
|
||||
// Test: Modal has history management
|
||||
#[test]
|
||||
fn test_modal_has_history_management() {
|
||||
let generator = Generator::new().unwrap();
|
||||
let schema = make_schema(json!({
|
||||
"title": "Test",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": { "type": "string" }
|
||||
}
|
||||
}));
|
||||
let options = Options::default();
|
||||
let html = generator.generate(&schema, &options).unwrap();
|
||||
|
||||
// Should have history array and index
|
||||
assert!(
|
||||
html.contains("history=[]"),
|
||||
"Should initialize history array"
|
||||
);
|
||||
assert!(
|
||||
html.contains("historyIndex=-1"),
|
||||
"Should initialize history index"
|
||||
);
|
||||
// Should have navigate function
|
||||
assert!(
|
||||
html.contains("navigateHistory"),
|
||||
"Should have navigateHistory function"
|
||||
);
|
||||
}
|
||||
|
||||
// Test: Modal history cleared on close
|
||||
#[test]
|
||||
fn test_modal_history_cleared_on_close() {
|
||||
let generator = Generator::new().unwrap();
|
||||
let schema = make_schema(json!({
|
||||
"title": "Test",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": { "type": "string" }
|
||||
}
|
||||
}));
|
||||
let options = Options::default();
|
||||
let html = generator.generate(&schema, &options).unwrap();
|
||||
|
||||
// closeModal function should reset history
|
||||
// The function contains: history=[];historyIndex=-1;
|
||||
assert!(
|
||||
html.contains("history=[]") && html.contains("historyIndex=-1"),
|
||||
"closeModal should reset history state"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ pub struct HtmlIds {
|
||||
pub sidebar_toggle: &'static str,
|
||||
pub root_schema: &'static str,
|
||||
pub definitions: &'static str,
|
||||
pub modal_overlay: &'static str,
|
||||
}
|
||||
|
||||
/// All static HTML IDs
|
||||
@@ -25,6 +26,7 @@ pub const IDS: HtmlIds = HtmlIds {
|
||||
sidebar_toggle: "sidebar-toggle",
|
||||
root_schema: "root-schema",
|
||||
definitions: "definitions",
|
||||
modal_overlay: "modal-overlay",
|
||||
};
|
||||
|
||||
/// CSS class names organized by category
|
||||
@@ -123,6 +125,18 @@ pub struct CssClasses {
|
||||
pub usages: &'static str,
|
||||
pub usage_section: &'static str,
|
||||
pub usage_list: &'static str,
|
||||
|
||||
// Modal
|
||||
pub modal_overlay: &'static str,
|
||||
pub modal: &'static str,
|
||||
pub modal_active: &'static str,
|
||||
pub modal_header: &'static str,
|
||||
pub modal_title: &'static str,
|
||||
pub modal_close: &'static str,
|
||||
pub modal_content: &'static str,
|
||||
pub modal_nav: &'static str,
|
||||
pub modal_nav_btn: &'static str,
|
||||
pub modal_nav_disabled: &'static str,
|
||||
}
|
||||
|
||||
/// All CSS class names
|
||||
@@ -220,6 +234,18 @@ pub const CSS: CssClasses = CssClasses {
|
||||
usages: "usages",
|
||||
usage_section: "usage-section",
|
||||
usage_list: "usage-list",
|
||||
|
||||
// Modal
|
||||
modal_overlay: "modal-overlay",
|
||||
modal: "modal",
|
||||
modal_active: "modal-active",
|
||||
modal_header: "modal-header",
|
||||
modal_title: "modal-title",
|
||||
modal_close: "modal-close",
|
||||
modal_content: "modal-content",
|
||||
modal_nav: "modal-nav",
|
||||
modal_nav_btn: "modal-nav-btn",
|
||||
modal_nav_disabled: "disabled",
|
||||
};
|
||||
|
||||
/// Type-specific badge class suffixes (used with type-badge prefix)
|
||||
@@ -281,4 +307,12 @@ mod tests {
|
||||
assert!(TYPE_CLASSES.integer.starts_with("type-"));
|
||||
assert!(TYPE_CLASSES.array.starts_with("type-"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modal_nav_classes_are_valid() {
|
||||
// Modal navigation CSS classes should not contain spaces
|
||||
assert!(!CSS.modal_nav.contains(' '));
|
||||
assert!(!CSS.modal_nav_btn.contains(' '));
|
||||
assert!(!CSS.modal_nav_disabled.contains(' '));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
//! # schemars-docgen
|
||||
//!
|
||||
//! Generate HTML documentation from `schemars::Schema` objects.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
use clap::Parser;
|
||||
use schemars_docgen::Options;
|
||||
use schemars_docgen::generate_html;
|
||||
|
||||
+7
-7
@@ -39,13 +39,13 @@ fn add_wrapper_display_names(schema: &mut Value) {
|
||||
let display_name = name.trim_end_matches(|c: char| c.is_ascii_digit());
|
||||
|
||||
// Only add _display_name if it differs from the original name
|
||||
if display_name != name {
|
||||
if let Some(obj) = def.as_object_mut() {
|
||||
obj.insert(
|
||||
"_display_name".to_string(),
|
||||
Value::String(display_name.to_string()),
|
||||
);
|
||||
}
|
||||
if display_name != name
|
||||
&& let Some(obj) = def.as_object_mut()
|
||||
{
|
||||
obj.insert(
|
||||
"_display_name".to_string(),
|
||||
Value::String(display_name.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-1
@@ -151,8 +151,38 @@
|
||||
{%- endif -%}
|
||||
</main>
|
||||
</div>
|
||||
<div class="{{ css.modal_overlay }}" id="{{ ids.modal_overlay }}">
|
||||
<div class="{{ css.modal }}">
|
||||
<div class="{{ css.modal_header }}">
|
||||
<div class="{{ css.modal_nav }}">
|
||||
<button
|
||||
class="{{ css.modal_nav_btn }} {{ css.modal_nav_disabled }}"
|
||||
aria-label="Go back"
|
||||
data-dir="back"
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<button
|
||||
class="{{ css.modal_nav_btn }} {{ css.modal_nav_disabled }}"
|
||||
aria-label="Go forward"
|
||||
data-dir="forward"
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="{{ css.modal_title }}"></h3>
|
||||
<button
|
||||
class="{{ css.modal_close }}"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="{{ css.modal_content }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){const searchInput=document.getElementById('{{ ids.search }}');const searchResults=document.getElementById('{{ ids.search_results }}');const searchIndex={{ search_index | json_encode() | safe }};searchInput.addEventListener('input',function(e){const query=e.target.value.toLowerCase().trim();if(!query){searchResults.innerHTML='';searchResults.classList.remove('{{ css.search_results_active }}');return;}const matches=searchIndex.filter(item=>item.name.toLowerCase().includes(query)||item.path.toLowerCase().includes(query)).slice(0,10);if(matches.length===0){searchResults.innerHTML='<div class="{{ css.search_result_item }} {{ css.no_results }}">No results found</div>';}else{searchResults.innerHTML=matches.map(item=>`<a href="#${item.anchor}" class="{{ css.search_result_item }}"><span class="{{ css.result_name }}">${item.name}</span><span class="{{ css.result_path }}">${item.path}</span><span class="{{ css.result_type }}">${item.entry_type}</span></a>`).join('');}searchResults.classList.add('{{ css.search_results_active }}');});document.addEventListener('click',function(e){if(!searchInput.contains(e.target)&&!searchResults.contains(e.target)){searchResults.classList.remove('{{ css.search_results_active }}');}});searchInput.addEventListener('keydown',function(e){if(e.key==='Escape'){searchResults.classList.remove('{{ css.search_results_active }}');searchInput.blur();}});const sidebarToggle=document.getElementById('{{ ids.sidebar_toggle }}');const sidebar=document.getElementById('{{ ids.sidebar }}');if(sidebarToggle&&sidebar){sidebarToggle.addEventListener('click',function(){sidebar.classList.toggle('{{ css.sidebar_open }}');});}document.querySelectorAll('a[href^="#"]').forEach(anchor=>{anchor.addEventListener('click',function(e){const targetId=this.getAttribute('href').slice(1);const target=document.getElementById(targetId);if(target){target.classList.add('{{ css.highlight }}');setTimeout(()=>target.classList.remove('{{ css.highlight }}'),2000);}if(sidebar){sidebar.classList.remove('{{ css.sidebar_open }}');}searchResults.classList.remove('{{ css.search_results_active }}');});});})();
|
||||
(function(){const searchInput=document.getElementById('{{ ids.search }}');const searchResults=document.getElementById('{{ ids.search_results }}');const searchIndex={{ search_index | json_encode() | safe }};searchInput.addEventListener('input',function(e){const query=e.target.value.toLowerCase().trim();if(!query){searchResults.innerHTML='';searchResults.classList.remove('{{ css.search_results_active }}');return;}const matches=searchIndex.filter(item=>item.name.toLowerCase().includes(query)||item.path.toLowerCase().includes(query)).slice(0,10);if(matches.length===0){searchResults.innerHTML='<div class="{{ css.search_result_item }} {{ css.no_results }}">No results found</div>';}else{searchResults.innerHTML=matches.map(item=>`<a href="#${item.anchor}" class="{{ css.search_result_item }}"><span class="{{ css.result_name }}">${item.name}</span><span class="{{ css.result_path }}">${item.path}</span><span class="{{ css.result_type }}">${item.entry_type}</span></a>`).join('');}searchResults.classList.add('{{ css.search_results_active }}');});document.addEventListener('click',function(e){if(!searchInput.contains(e.target)&&!searchResults.contains(e.target)){searchResults.classList.remove('{{ css.search_results_active }}');}});searchInput.addEventListener('keydown',function(e){if(e.key==='Escape'){searchResults.classList.remove('{{ css.search_results_active }}');searchInput.blur();}});const sidebarToggle=document.getElementById('{{ ids.sidebar_toggle }}');const sidebar=document.getElementById('{{ ids.sidebar }}');if(sidebarToggle&&sidebar){sidebarToggle.addEventListener('click',function(){sidebar.classList.toggle('{{ css.sidebar_open }}');});}document.querySelectorAll('a[href^="#"]').forEach(anchor=>{anchor.addEventListener('click',function(e){const targetId=this.getAttribute('href').slice(1);const target=document.getElementById(targetId);if(target){target.classList.add('{{ css.highlight }}');setTimeout(()=>target.classList.remove('{{ css.highlight }}'),2000);}if(sidebar){sidebar.classList.remove('{{ css.sidebar_open }}');}searchResults.classList.remove('{{ css.search_results_active }}');});});const modalOverlay=document.getElementById('{{ ids.modal_overlay }}');const modalTitle=modalOverlay.querySelector('.{{ css.modal_title }}');const modalContent=modalOverlay.querySelector('.{{ css.modal_content }}');const modalClose=modalOverlay.querySelector('.{{ css.modal_close }}');const modalNavBtns=modalOverlay.querySelectorAll('.{{ css.modal_nav_btn }}');const configSection=document.getElementById('{{ ids.root_schema }}');let history=[];let historyIndex=-1;function updateNavButtons(){modalNavBtns.forEach(btn=>{const dir=btn.dataset.dir;if(dir==='back'){btn.classList.toggle('{{ css.modal_nav_disabled }}',historyIndex<=0);}else if(dir==='forward'){btn.classList.toggle('{{ css.modal_nav_disabled }}',historyIndex>=history.length-1);}});}function showType(typeName){const typeId=typeName.toLowerCase();const typeDef=document.getElementById(typeId);if(!typeDef)return false;const clone=typeDef.cloneNode(true);clone.removeAttribute('id');const usages=clone.querySelector('.{{ css.usages }}');if(usages)usages.remove();const titleEl=clone.querySelector('.{{ css.definition_name }}');modalTitle.textContent=titleEl?titleEl.textContent:typeName;if(titleEl)titleEl.remove();modalContent.innerHTML='';modalContent.appendChild(clone);return true;}function openTypeModal(typeName,addToHistory=true){if(!showType(typeName))return;if(addToHistory){history=history.slice(0,historyIndex+1);history.push(typeName);historyIndex=history.length-1;}updateNavButtons();modalOverlay.classList.add('{{ css.modal_active }}');document.body.style.overflow='hidden';}function navigateHistory(direction){if(direction==='back'&&historyIndex>0){historyIndex--;showType(history[historyIndex]);updateNavButtons();}else if(direction==='forward'&&historyIndex<history.length-1){historyIndex++;showType(history[historyIndex]);updateNavButtons();}}function closeModal(){modalOverlay.classList.remove('{{ css.modal_active }}');document.body.style.overflow='';history=[];historyIndex=-1;updateNavButtons();}modalNavBtns.forEach(btn=>{btn.addEventListener('click',function(){if(!this.classList.contains('{{ css.modal_nav_disabled }}')){navigateHistory(this.dataset.dir);}});});configSection.addEventListener('click',function(e){const typeRef=e.target.closest('.{{ css.type_ref }}');if(typeRef){e.preventDefault();const href=typeRef.getAttribute('href');const typeName=href.slice(1);openTypeModal(typeName);}});modalContent.addEventListener('click',function(e){const typeRef=e.target.closest('.{{ css.type_ref }}');if(typeRef){e.preventDefault();const href=typeRef.getAttribute('href');const typeName=href.slice(1);openTypeModal(typeName);}});modalOverlay.addEventListener('click',function(e){if(e.target===modalOverlay)closeModal();});modalClose.addEventListener('click',closeModal);document.addEventListener('keydown',function(e){if(!modalOverlay.classList.contains('{{ css.modal_active }}'))rLine truncated
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -943,6 +943,108 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal overlay */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.modal-overlay.modal-active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.modal-nav {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.modal-nav-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
transition:
|
||||
background-color 0.15s,
|
||||
opacity 0.15s;
|
||||
}
|
||||
|
||||
.modal-nav-btn:hover:not(.disabled) {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.modal-nav-btn.disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.25rem 0.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Dark mode modal */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.modal-overlay {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.header {
|
||||
|
||||
Reference in new issue
Block a user