Files
arcum42-ComfyUI_SageUtils/js/components/layout.js
T
2025-10-12 11:08:20 -07:00

681 lines
22 KiB
JavaScript

/**
* @fileoverview Layout Component Library
* Provides reusable layout utilities for consistent UI structure:
* - Cards with titles and actions
* - Grid and flex containers
* - Sections with headers
* - Split panes
* - Scrollable containers
*
* @module components/layout
*/
/**
* Create a flex container with common configurations
* @param {Object} options - Configuration options
* @param {string} options.direction - Flex direction: 'row' | 'column' | 'row-reverse' | 'column-reverse' (default: 'row')
* @param {string} options.justify - Justify content: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly' (default: 'flex-start')
* @param {string} options.align - Align items: 'stretch' | 'flex-start' | 'flex-end' | 'center' | 'baseline' (default: 'stretch')
* @param {string} options.gap - Gap between items (e.g., '8px', '1rem') (default: '0')
* @param {boolean} options.wrap - Enable flex wrap (default: false)
* @param {string} options.padding - Container padding (default: '0')
* @param {string} options.background - Background color (default: 'transparent')
* @param {string} options.className - Additional CSS class
* @param {Object} options.style - Custom style overrides
* @param {HTMLElement[]} options.children - Child elements to append
* @returns {HTMLElement} Configured flex container
*/
export function createFlexContainer(options = {}) {
const {
direction = 'row',
justify = 'flex-start',
align = 'stretch',
gap = '0',
wrap = false,
padding = '0',
background = 'transparent',
className = '',
style = {},
children = []
} = options;
const container = document.createElement('div');
if (className) container.className = className;
container.style.cssText = `
display: flex;
flex-direction: ${direction};
justify-content: ${justify};
align-items: ${align};
gap: ${gap};
flex-wrap: ${wrap ? 'wrap' : 'nowrap'};
padding: ${padding};
background: ${background};
`;
// Apply custom style overrides
Object.assign(container.style, style);
// Append children
children.forEach(child => {
if (child instanceof HTMLElement) {
container.appendChild(child);
}
});
return container;
}
/**
* Create a grid container with common configurations
* @param {Object} options - Configuration options
* @param {string|number} options.columns - Column definition: number (e.g., 3) or CSS value (e.g., '1fr 2fr 1fr', 'repeat(auto-fill, minmax(200px, 1fr))')
* @param {string|number} options.rows - Row definition (optional): number or CSS value
* @param {string} options.gap - Gap between items (default: '16px')
* @param {string} options.columnGap - Gap between columns (overrides gap)
* @param {string} options.rowGap - Gap between rows (overrides gap)
* @param {string} options.padding - Container padding (default: '0')
* @param {string} options.background - Background color (default: 'transparent')
* @param {string} options.className - Additional CSS class
* @param {Object} options.style - Custom style overrides
* @param {HTMLElement[]} options.children - Child elements to append
* @returns {HTMLElement} Configured grid container
*/
export function createGrid(options = {}) {
const {
columns = 1,
rows = null,
gap = '16px',
columnGap = null,
rowGap = null,
padding = '0',
background = 'transparent',
className = '',
style = {},
children = []
} = options;
const container = document.createElement('div');
if (className) container.className = className;
// Build grid-template-columns
let gridColumns;
if (typeof columns === 'number') {
gridColumns = `repeat(${columns}, 1fr)`;
} else {
gridColumns = columns;
}
// Build grid-template-rows
let gridRows = '';
if (rows !== null) {
if (typeof rows === 'number') {
gridRows = `grid-template-rows: repeat(${rows}, 1fr);`;
} else {
gridRows = `grid-template-rows: ${rows};`;
}
}
container.style.cssText = `
display: grid;
grid-template-columns: ${gridColumns};
${gridRows}
gap: ${gap};
${columnGap ? `column-gap: ${columnGap};` : ''}
${rowGap ? `row-gap: ${rowGap};` : ''}
padding: ${padding};
background: ${background};
`;
// Apply custom style overrides
Object.assign(container.style, style);
// Append children
children.forEach(child => {
if (child instanceof HTMLElement) {
container.appendChild(child);
}
});
return container;
}
/**
* Create a card container with optional title and actions
* @param {Object} options - Configuration options
* @param {string} options.title - Card title (optional)
* @param {HTMLElement|string} options.content - Card content (HTMLElement or HTML string)
* @param {HTMLElement[]} options.actions - Action buttons to display in header (optional)
* @param {string} options.padding - Content padding (default: '16px')
* @param {string} options.background - Background color (default: '#2a2a2a')
* @param {string} options.borderColor - Border color (default: '#444')
* @param {string} options.borderRadius - Border radius (default: '8px')
* @param {string} options.className - Additional CSS class
* @param {Object} options.style - Custom style overrides
* @returns {HTMLElement} Card container
*/
export function createCard(options = {}) {
const {
title = '',
content = null,
actions = [],
padding = '16px',
background = '#2a2a2a',
borderColor = '#444',
borderRadius = '8px',
className = '',
style = {}
} = options;
const card = document.createElement('div');
if (className) card.className = className;
card.style.cssText = `
background: ${background};
border: 1px solid ${borderColor};
border-radius: ${borderRadius};
overflow: hidden;
`;
// Apply custom style overrides
Object.assign(card.style, style);
// Create header if title or actions are provided
if (title || actions.length > 0) {
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px ${padding};
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid ${borderColor};
`;
if (title) {
const titleElement = document.createElement('h3');
titleElement.textContent = title;
titleElement.style.cssText = `
margin: 0;
color: #ccc;
font-size: 14px;
font-weight: bold;
`;
header.appendChild(titleElement);
}
if (actions.length > 0) {
const actionsContainer = document.createElement('div');
actionsContainer.style.cssText = `
display: flex;
gap: 8px;
align-items: center;
`;
actions.forEach(action => {
if (action instanceof HTMLElement) {
// Remove margin-top from buttons in header
if (action.style.marginTop) {
action.style.marginTop = '0';
}
actionsContainer.appendChild(action);
}
});
header.appendChild(actionsContainer);
}
card.appendChild(header);
}
// Create content area
if (content) {
const contentArea = document.createElement('div');
contentArea.style.cssText = `
padding: ${padding};
`;
if (typeof content === 'string') {
contentArea.innerHTML = content;
} else if (content instanceof HTMLElement) {
contentArea.appendChild(content);
}
card.appendChild(contentArea);
}
return card;
}
/**
* Create a section with title and content
* @param {string} title - Section title
* @param {HTMLElement|string} content - Section content
* @param {Object} options - Configuration options
* @param {string} options.titleColor - Title color (default: '#569cd6')
* @param {string} options.titleSize - Title font size (default: '16px')
* @param {string} options.borderColor - Border color (default: '#4CAF50')
* @param {string} options.padding - Content padding (default: '16px')
* @param {string} options.marginTop - Section margin top (default: '16px')
* @param {string} options.background - Background color (default: 'transparent')
* @param {boolean} options.collapsible - Make section collapsible (default: false)
* @param {boolean} options.collapsed - Initial collapsed state (default: false)
* @param {string} options.className - Additional CSS class
* @param {Object} options.style - Custom style overrides
* @returns {HTMLElement} Section container
*/
export function createSection(title, content, options = {}) {
const {
titleColor = '#569cd6',
titleSize = '16px',
borderColor = '#4CAF50',
padding = '16px',
marginTop = '16px',
background = 'transparent',
collapsible = false,
collapsed = false,
className = '',
style = {}
} = options;
const section = document.createElement('div');
if (className) section.className = className;
section.style.cssText = `
margin-top: ${marginTop};
background: ${background};
`;
// Apply custom style overrides
Object.assign(section.style, style);
// Create title
const titleElement = document.createElement('div');
titleElement.style.cssText = `
font-size: ${titleSize};
font-weight: bold;
color: ${titleColor};
padding-bottom: 8px;
border-bottom: 2px solid ${borderColor};
margin-bottom: 12px;
${collapsible ? 'cursor: pointer; user-select: none;' : ''}
`;
if (collapsible) {
const arrow = document.createElement('span');
arrow.textContent = collapsed ? '▶ ' : '▼ ';
arrow.style.cssText = `
display: inline-block;
transition: transform 0.2s;
`;
titleElement.appendChild(arrow);
}
const titleText = document.createElement('span');
titleText.textContent = title;
titleElement.appendChild(titleText);
section.appendChild(titleElement);
// Create content area
const contentArea = document.createElement('div');
contentArea.style.cssText = `
padding: ${padding};
${collapsed ? 'display: none;' : ''}
`;
if (typeof content === 'string') {
contentArea.innerHTML = content;
} else if (content instanceof HTMLElement) {
contentArea.appendChild(content);
}
section.appendChild(contentArea);
// Add collapsible functionality
if (collapsible) {
titleElement.addEventListener('click', () => {
const isCollapsed = contentArea.style.display === 'none';
contentArea.style.display = isCollapsed ? '' : 'none';
const arrow = titleElement.querySelector('span');
arrow.textContent = isCollapsed ? '▼ ' : '▶ ';
});
}
return section;
}
/**
* Create a scrollable container
* @param {HTMLElement|string} content - Content to make scrollable
* @param {Object} options - Configuration options
* @param {string} options.maxHeight - Maximum height before scrolling (default: '400px')
* @param {string} options.maxWidth - Maximum width before scrolling (default: '100%')
* @param {string} options.padding - Container padding (default: '0')
* @param {string} options.background - Background color (default: 'transparent')
* @param {boolean} options.showScrollbar - Show scrollbar (default: true)
* @param {string} options.className - Additional CSS class
* @param {Object} options.style - Custom style overrides
* @returns {HTMLElement} Scrollable container
*/
export function createScrollContainer(content, options = {}) {
const {
maxHeight = '400px',
maxWidth = '100%',
padding = '0',
background = 'transparent',
showScrollbar = true,
className = '',
style = {}
} = options;
const container = document.createElement('div');
if (className) container.className = className;
container.style.cssText = `
max-height: ${maxHeight};
max-width: ${maxWidth};
overflow-y: auto;
overflow-x: auto;
padding: ${padding};
background: ${background};
${!showScrollbar ? `
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
` : ''}
`;
// Hide scrollbar for WebKit browsers if requested
if (!showScrollbar) {
const style = document.createElement('style');
const uniqueClass = `scroll-container-${Date.now()}`;
container.classList.add(uniqueClass);
style.textContent = `.${uniqueClass}::-webkit-scrollbar { display: none; }`;
document.head.appendChild(style);
}
// Apply custom style overrides
Object.assign(container.style, style);
// Add content
if (typeof content === 'string') {
container.innerHTML = content;
} else if (content instanceof HTMLElement) {
container.appendChild(content);
}
return container;
}
/**
* Create a split pane layout (two-column layout with optional resizing)
* @param {HTMLElement} leftContent - Left pane content
* @param {HTMLElement} rightContent - Right pane content
* @param {Object} options - Configuration options
* @param {string} options.splitRatio - Split ratio: '30-70', '50-50', '40-60', etc. (default: '50-50')
* @param {string} options.gap - Gap between panes (default: '8px')
* @param {string} options.minLeftWidth - Minimum left pane width (default: '200px')
* @param {string} options.minRightWidth - Minimum right pane width (default: '200px')
* @param {boolean} options.resizable - Enable resize handle (default: false)
* @param {string} options.className - Additional CSS class
* @param {Object} options.style - Custom style overrides
* @returns {HTMLElement} Split pane container
*/
export function createSplitPane(leftContent, rightContent, options = {}) {
const {
splitRatio = '50-50',
gap = '8px',
minLeftWidth = '200px',
minRightWidth = '200px',
resizable = false,
className = '',
style = {}
} = options;
const container = document.createElement('div');
if (className) container.className = className;
// Parse split ratio
const [leftRatio, rightRatio] = splitRatio.split('-').map(Number);
const totalRatio = leftRatio + rightRatio;
const leftPercent = (leftRatio / totalRatio) * 100;
const rightPercent = (rightRatio / totalRatio) * 100;
container.style.cssText = `
display: flex;
gap: ${gap};
width: 100%;
`;
// Apply custom style overrides
Object.assign(container.style, style);
// Create left pane
const leftPane = document.createElement('div');
leftPane.style.cssText = `
flex: 0 0 ${leftPercent}%;
min-width: ${minLeftWidth};
overflow: auto;
`;
leftPane.appendChild(leftContent);
// Create right pane
const rightPane = document.createElement('div');
rightPane.style.cssText = `
flex: 0 0 ${rightPercent}%;
min-width: ${minRightWidth};
overflow: auto;
`;
rightPane.appendChild(rightContent);
container.appendChild(leftPane);
// Add resize handle if resizable
if (resizable) {
const resizeHandle = document.createElement('div');
resizeHandle.style.cssText = `
width: 4px;
background: #444;
cursor: col-resize;
flex-shrink: 0;
transition: background 0.2s;
`;
resizeHandle.addEventListener('mouseenter', () => {
resizeHandle.style.background = '#4CAF50';
});
resizeHandle.addEventListener('mouseleave', () => {
resizeHandle.style.background = '#444';
});
// Implement resize functionality
let isResizing = false;
let startX = 0;
let startLeftWidth = 0;
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
startLeftWidth = leftPane.offsetWidth;
document.body.style.cursor = 'col-resize';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaX = e.clientX - startX;
const newLeftWidth = startLeftWidth + deltaX;
const containerWidth = container.offsetWidth;
const newRightWidth = containerWidth - newLeftWidth - resizeHandle.offsetWidth - (parseInt(gap) * 2);
// Check minimum widths
if (newLeftWidth >= parseInt(minLeftWidth) && newRightWidth >= parseInt(minRightWidth)) {
leftPane.style.flex = `0 0 ${newLeftWidth}px`;
rightPane.style.flex = `0 0 ${newRightWidth}px`;
}
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
}
});
container.appendChild(resizeHandle);
}
container.appendChild(rightPane);
return container;
}
/**
* Create a responsive grid that auto-fits items
* @param {Object} options - Configuration options
* @param {number} options.minItemWidth - Minimum width per grid item in pixels (default: 200)
* @param {number} options.maxItemWidth - Maximum width per grid item (default: '1fr')
* @param {string} options.gap - Gap between items (default: '16px')
* @param {string} options.padding - Container padding (default: '0')
* @param {string} options.background - Background color (default: 'transparent')
* @param {string} options.className - Additional CSS class
* @param {Object} options.style - Custom style overrides
* @param {HTMLElement[]} options.children - Child elements to append
* @returns {HTMLElement} Responsive grid container
*/
export function createResponsiveGrid(options = {}) {
const {
minItemWidth = 200,
maxItemWidth = '1fr',
gap = '16px',
padding = '0',
background = 'transparent',
className = '',
style = {},
children = []
} = options;
return createGrid({
columns: `repeat(auto-fill, minmax(${minItemWidth}px, ${maxItemWidth}))`,
gap,
padding,
background,
className,
style,
children
});
}
/**
* Create a centered container
* @param {HTMLElement|string} content - Content to center
* @param {Object} options - Configuration options
* @param {string} options.maxWidth - Maximum width of centered content (default: '800px')
* @param {string} options.padding - Container padding (default: '16px')
* @param {string} options.marginTop - Top margin (default: '0')
* @param {string} options.background - Background color (default: 'transparent')
* @param {string} options.className - Additional CSS class
* @param {Object} options.style - Custom style overrides
* @returns {HTMLElement} Centered container
*/
export function createCenteredContainer(content, options = {}) {
const {
maxWidth = '800px',
padding = '16px',
marginTop = '0',
background = 'transparent',
className = '',
style = {}
} = options;
const container = document.createElement('div');
if (className) container.className = className;
container.style.cssText = `
max-width: ${maxWidth};
margin: ${marginTop} auto 0 auto;
padding: ${padding};
background: ${background};
`;
// Apply custom style overrides
Object.assign(container.style, style);
// Add content
if (typeof content === 'string') {
container.innerHTML = content;
} else if (content instanceof HTMLElement) {
container.appendChild(content);
}
return container;
}
/**
* Create a stack layout (vertical list with consistent spacing)
* @param {HTMLElement[]} items - Items to stack
* @param {Object} options - Configuration options
* @param {string} options.gap - Gap between items (default: '16px')
* @param {string} options.padding - Container padding (default: '0')
* @param {string} options.background - Background color (default: 'transparent')
* @param {string} options.className - Additional CSS class
* @param {Object} options.style - Custom style overrides
* @returns {HTMLElement} Stack container
*/
export function createStack(items, options = {}) {
const {
gap = '16px',
padding = '0',
background = 'transparent',
className = '',
style = {}
} = options;
return createFlexContainer({
direction: 'column',
gap,
padding,
background,
className,
style,
children: items
});
}
/**
* Create an inline layout (horizontal list with consistent spacing)
* @param {HTMLElement[]} items - Items to arrange inline
* @param {Object} options - Configuration options
* @param {string} options.gap - Gap between items (default: '8px')
* @param {string} options.align - Vertical alignment (default: 'center')
* @param {boolean} options.wrap - Allow wrapping (default: true)
* @param {string} options.padding - Container padding (default: '0')
* @param {string} options.background - Background color (default: 'transparent')
* @param {string} options.className - Additional CSS class
* @param {Object} options.style - Custom style overrides
* @returns {HTMLElement} Inline container
*/
export function createInline(items, options = {}) {
const {
gap = '8px',
align = 'center',
wrap = true,
padding = '0',
background = 'transparent',
className = '',
style = {}
} = options;
return createFlexContainer({
direction: 'row',
align,
gap,
wrap,
padding,
background,
className,
style,
children: items
});
}