Files
arcum42-ComfyUI_SageUtils/js/components/layout.js
T
2026-05-25 16:36:45 -07:00

644 lines
23 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
*/
import { loadComponentStyles as ensureComponentStyles } from './styleLoader.js';
console.log('[SageUtils] layout.js imported');
function loadComponentStyles() {
ensureComponentStyles('layout.js');
}
/**
* 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',
alignItems = null,
gap = '0',
wrap = false,
padding = '0',
background = 'transparent',
className = '',
style = {},
children = []
} = options;
const effectiveAlign = alignItems || align;
loadComponentStyles();
const container = document.createElement('div');
container.classList.add('sage-flex-container');
if (className) container.classList.add(...className.split(' ').filter(Boolean));
container.style.setProperty('--sage-flex-direction', direction);
container.style.setProperty('--sage-flex-justify', justify);
container.style.setProperty('--sage-flex-align', effectiveAlign);
container.style.setProperty('--sage-flex-gap', gap);
container.style.setProperty('--sage-flex-wrap', wrap ? 'wrap' : 'nowrap');
container.style.setProperty('--sage-flex-padding', padding);
container.style.setProperty('--sage-flex-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;
loadComponentStyles();
const container = document.createElement('div');
container.classList.add('sage-grid-container');
if (className) container.classList.add(...className.split(' ').filter(Boolean));
// Build grid-template-columns
let gridColumns;
if (typeof columns === 'number') {
gridColumns = `repeat(${columns}, 1fr)`;
} else {
gridColumns = columns;
}
container.style.setProperty('--sage-grid-columns', gridColumns);
if (rows !== null) {
if (typeof rows === 'number') {
container.style.setProperty('--sage-grid-rows', `repeat(${rows}, 1fr)`);
} else {
container.style.setProperty('--sage-grid-rows', rows);
}
}
container.style.setProperty('--sage-grid-gap', gap);
if (columnGap) container.style.setProperty('--sage-grid-column-gap', columnGap);
if (rowGap) container.style.setProperty('--sage-grid-row-gap', rowGap);
container.style.setProperty('--sage-grid-padding', padding);
container.style.setProperty('--sage-grid-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;
loadComponentStyles();
const card = document.createElement('div');
card.classList.add('sage-card');
if (className) card.classList.add(...className.split(' ').filter(Boolean));
card.style.setProperty('--sage-card-background', background);
card.style.setProperty('--sage-card-border', `1px solid ${borderColor}`);
card.style.setProperty('--sage-card-border-radius', borderRadius);
// 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.classList.add('sage-card-header');
header.style.setProperty('--sage-card-header-padding', `12px ${padding}`);
header.style.setProperty('--sage-card-header-border-color', borderColor);
if (title) {
const titleElement = document.createElement('h3');
titleElement.textContent = title;
titleElement.classList.add('sage-card-title');
header.appendChild(titleElement);
}
if (actions.length > 0) {
const actionsContainer = document.createElement('div');
actionsContainer.classList.add('sage-card-actions');
actions.forEach(action => {
if (action instanceof HTMLElement) {
actionsContainer.appendChild(action);
}
});
header.appendChild(actionsContainer);
}
card.appendChild(header);
}
// Create content area
if (content) {
const contentArea = document.createElement('div');
contentArea.classList.add('sage-card-content');
contentArea.style.setProperty('--sage-card-content-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;
loadComponentStyles();
const section = document.createElement('div');
section.classList.add('sage-section');
if (className) section.classList.add(...className.split(' ').filter(Boolean));
section.style.setProperty('--sage-section-margin-top', marginTop);
section.style.setProperty('--sage-section-background', background);
// Apply custom style overrides
Object.assign(section.style, style);
// Create title
const titleElement = document.createElement('div');
titleElement.classList.add('sage-section-title');
if (collapsible) {
titleElement.classList.add('sage-section-title--collapsible');
}
titleElement.style.setProperty('--sage-section-title-size', titleSize);
titleElement.style.setProperty('--sage-section-title-color', titleColor);
titleElement.style.setProperty('--sage-section-border-color', borderColor);
if (collapsible) {
const arrow = document.createElement('span');
arrow.textContent = collapsed ? '▶ ' : '▼ ';
arrow.classList.add('sage-section-title-arrow');
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.classList.add('sage-section-content');
contentArea.style.setProperty('--sage-section-content-padding', padding);
if (collapsed) {
contentArea.classList.add('sage-section-content--collapsed');
}
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.classList.toggle('sage-section-content--collapsed');
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;
loadComponentStyles();
const container = document.createElement('div');
container.classList.add('sage-scroll-container');
if (className) container.classList.add(...className.split(' ').filter(Boolean));
container.style.setProperty('--sage-scroll-max-height', maxHeight);
container.style.setProperty('--sage-scroll-max-width', maxWidth);
container.style.setProperty('--sage-scroll-padding', padding);
container.style.setProperty('--sage-scroll-background', background);
if (!showScrollbar) {
container.classList.add('sage-scroll-container--hidden-scrollbar');
}
// 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;
loadComponentStyles();
const container = document.createElement('div');
container.classList.add('sage-split-pane');
if (className) container.classList.add(...className.split(' ').filter(Boolean));
// 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.setProperty('--sage-split-gap', gap);
// Apply custom style overrides
Object.assign(container.style, style);
// Create left pane
const leftPane = document.createElement('div');
leftPane.classList.add('sage-split-pane-left');
leftPane.style.setProperty('--sage-split-left-flex', `0 0 ${leftPercent}%`);
leftPane.style.setProperty('--sage-split-left-min-width', minLeftWidth);
leftPane.appendChild(leftContent);
// Create right pane
const rightPane = document.createElement('div');
rightPane.classList.add('sage-split-pane-right');
rightPane.style.setProperty('--sage-split-right-flex', `0 0 ${rightPercent}%`);
rightPane.style.setProperty('--sage-split-right-min-width', minRightWidth);
rightPane.appendChild(rightContent);
container.appendChild(leftPane);
// Add resize handle if resizable
if (resizable) {
const resizeHandle = document.createElement('div');
resizeHandle.classList.add('sage-split-pane-handle');
resizeHandle.addEventListener('mouseenter', () => {
resizeHandle.classList.add('sage-split-pane-handle-active');
});
resizeHandle.addEventListener('mouseleave', () => {
resizeHandle.classList.remove('sage-split-pane-handle-active');
});
// 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.classList.add('sage-resizing');
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.setProperty('--sage-split-left-flex', `0 0 ${newLeftWidth}px`);
rightPane.style.setProperty('--sage-split-right-flex', `0 0 ${newRightWidth}px`);
}
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.classList.remove('sage-resizing');
}
});
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;
loadComponentStyles();
const container = document.createElement('div');
container.classList.add('sage-centered-container');
if (className) container.classList.add(...className.split(' ').filter(Boolean));
container.style.setProperty('--sage-centered-max-width', maxWidth);
container.style.setProperty('--sage-centered-margin', `${marginTop} auto 0 auto`);
container.style.setProperty('--sage-centered-padding', padding);
container.style.setProperty('--sage-centered-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
});
}