UNPKG

ttabs-svelte

Version:

A flexible layout management system with draggable, resizable tiles and tabs for Svelte applications. Like in VSCode

1,959 lines 78.1 kB
import { generateId } from './utils/tile-utils';
import { DEFAULT_THEME, resolveTheme } from './types/theme-types';
import { parseSizeValue, calculateSizes } from './utils/size-utils';
import { Column, Grid, Panel, Row, Tab } from './ttabsObjects';
import { createValidationMiddleware } from './validation';
/**
 * Ttabs class implementation
 */
export class TTabs {
    tiles = $state({});
    activePanel = $state(null);
    focusedActiveTab = $state(null);
    rootGridId = $state('');
    componentRegistry = $state({});
    theme = $state(DEFAULT_THEME);
    // State change listeners
    stateChangeListeners = [];
    debouncedStateChangeListeners = [];
    pendingNotification = null;
    pendingStateChanges = false;
    defaultComponentIdForEmptyTiles;
    // Validation middleware
    validationMiddleware;
    setupFromScratchCallback;
    /**
     * Find the root grid ID from the current tiles
     * @returns The ID of the root grid
     * @throws Error if no root grid is found
     * @private
     */
    findRootGridId() {
        const rootGridId = Object.values(this.tiles)
            .find(tile => tile.type === 'grid' && !tile.parent)?.id || '';
        if (rootGridId === '') {
            throw new Error('No root grid found');
        }
        return rootGridId;
    }
    constructor(options = {}) {
        // Initialize validation middleware
        this.validationMiddleware = createValidationMiddleware(this, options.validators || [], options.defaultLayoutCreator);
        this.setupFromScratchCallback = options.setupFromScratch;
        // Initialize state
        if (options.tiles) {
            if (Array.isArray(options.tiles)) {
                // Convert array to record
                this.tiles = {};
                // Add each tile to the state
                options.tiles.forEach(tile => {
                    this.tiles[tile.id] = tile;
                });
                // Find the root grid in the initial state
                this.rootGridId = this.findRootGridId();
            }
            else {
                // Record format provided directly
                this.tiles = options.tiles;
                // Find the root grid
                this.rootGridId = this.findRootGridId();
            }
            // Validate initial layout when created with tiles
            this.validateLayout();
        }
        else {
            // Auto-create a root grid if no initial state is provided
            this.rootGridId = this.addGrid();
        }
        // Initialize theme with resolution for inheritance
        if (options.theme) {
            this.theme = resolveTheme(options.theme);
        }
        else {
            this.theme = DEFAULT_THEME;
        }
        // Set focused tab if provided
        if (options.focusedTab && this.getTile(options.focusedTab)) {
            this.focusedActiveTab = options.focusedTab;
        }
        this.defaultComponentIdForEmptyTiles = options.defaultComponentIdForEmptyTiles;
    }
    /**
     * Subscribe to state changes
     * @param callback Function to call when state changes
     * @returns Unsubscribe function
     */
    subscribe(callback) {
        this.stateChangeListeners.push(callback);
        // Call immediately with current state
        callback(this.tiles);
        // Return unsubscribe function
        return () => {
            this.stateChangeListeners = this.stateChangeListeners.filter(cb => cb !== callback);
        };
    }
    /**
     * Subscribe to state changes with debouncing (calls at the end of the frame)
     * @param callback Function to call when state changes
     * @returns Unsubscribe function
     */
    subscribeDebounced(callback) {
        this.debouncedStateChangeListeners.push(callback);
        // Call immediately with current state
        callback(this.tiles);
        // Return unsubscribe function
        return () => {
            this.debouncedStateChangeListeners = this.debouncedStateChangeListeners.filter(cb => cb !== callback);
        };
    }
    /**
     * Notify all subscribers of state change
     */
    notifyStateChange() {
        // Immediately notify regular subscribers
        this.stateChangeListeners.forEach(callback => {
            callback(this.tiles);
        });
        // Mark that we have pending state changes
        this.pendingStateChanges = true;
        // Schedule debounced notifications at the end of the frame
        if (this.debouncedStateChangeListeners.length > 0 && this.pendingNotification === null) {
            this.pendingNotification = requestAnimationFrame(() => {
                // Only process if we have pending changes
                if (this.pendingStateChanges) {
                    this.debouncedStateChangeListeners.forEach(callback => {
                        callback(this.tiles);
                    });
                    // Reset the pending state changes flag
                    this.pendingStateChanges = false;
                }
                this.pendingNotification = null;
            });
        }
    }
    /**
     * Register a component for content rendering
     * @param componentId Unique identifier for the component
     * @param component Svelte component to render
     * @param defaultProps Optional default props for the component
     */
    registerComponent(componentId, component, defaultProps = {}) {
        this.componentRegistry[componentId] = { component, defaultProps };
    }
    /**
     * Get a registered component by ID
     * @param componentId The component identifier
     * @returns The component and its default props, or null if not found
     */
    getContentComponent(componentId) {
        return this.componentRegistry[componentId] || null;
    }
    /**
     * Check if a component is registered
     * @param componentId The component identifier
     * @returns True if the component is registered
     */
    hasContentComponent(componentId) {
        return !!this.componentRegistry[componentId];
    }
    /**
     * Set component to a column or a tab
     * @param parentId ID of the parent column or tab
     * @param componentId ID of the registered component
     * @param props Props to pass to the component
     * @returns ID of the new content with component
     */
    setComponent(parentId, componentId, props = {}) {
        // Verify component exists
        if (!this.hasContentComponent(componentId)) {
            throw new Error(`Component with ID ${componentId} is not registered`);
        }
        // Get parent tile
        const parent = this.getTile(parentId);
        if (!parent) {
            throw new Error(`Parent tile with ID ${parentId} not found`);
        }
        // Handle different parent types
        if (parent.type === 'tab') {
            // Tab content handling
            const tab = parent;
            // Check if tab already has content - update it instead of creating new
            if (tab.content && this.getTile(tab.content)) {
                const existingContent = this.getTile(tab.content);
                if (existingContent && existingContent.type === 'content') {
                    this.updateTile(tab.content, {
                        componentId,
                        data: {
                            ...existingContent.data,
                            componentProps: props
                        }
                    });
                    return tab.content;
                }
            }
            // Create the content
            const contentId = this.addTile({
                type: 'content',
                parent: parentId,
                componentId,
                data: {
                    componentProps: props
                }
            });
            // Update tab's content
            this.updateTile(parentId, {
                content: contentId
            });
            return contentId;
        }
        else if (parent.type === 'column') {
            // Column content handling
            const column = parent;
            // Check if column already has a child
            if (column.child && this.getTile(column.child)) {
                // Remove existing child if it's content type
                const existingChild = this.getTile(column.child);
                if (existingChild && existingChild.type === 'content') {
                    // Update existing content instead of creating new
                    this.updateTile(column.child, {
                        componentId,
                        data: {
                            componentProps: props
                        }
                    });
                    return column.child;
                }
                else {
                    // Remove existing child that's not content type
                    this.removeTile(column.child);
                }
            }
            // Create the content
            const contentId = this.addTile({
                type: 'content',
                parent: parentId,
                componentId,
                data: {
                    componentProps: props
                }
            });
            // Update column's child reference
            this.updateTile(parentId, {
                child: contentId
            });
            return contentId;
        }
        else {
            throw new Error(`Cannot add content to a parent of type ${parent.type}. Content can only be child of a tab or column.`);
        }
    }
    /**
     * Get all tiles
     */
    getTiles() {
        return this.tiles;
    }
    /**
     * Get the active panel ID
     */
    getActivePanel() {
        return this.activePanel;
    }
    /**
     * Get a specific tile by ID with type casting
     */
    getTile(id) {
        return (this.tiles[id] || null);
    }
    /**
     * Get a grid by ID, throwing an error if not found
     * @throws Error if the tile is not found or not a grid
     */
    getGrid(id) {
        const tile = this.tiles[id];
        if (!tile) {
            throw new Error(`Grid not found: ${id}`);
        }
        if (tile.type !== 'grid') {
            throw new Error(`Tile ${id} is not a grid, found ${tile.type}`);
        }
        return tile;
    }
    /**
     * Get a row by ID, throwing an error if not found
     * @throws Error if the tile is not found or not a row
     */
    getRow(id) {
        const tile = this.tiles[id];
        if (!tile) {
            throw new Error(`Row not found: ${id}`);
        }
        if (tile.type !== 'row') {
            throw new Error(`Tile ${id} is not a row, found ${tile.type}`);
        }
        return tile;
    }
    /**
     * Get a column by ID, throwing an error if not found
     * @throws Error if the tile is not found or not a column
     */
    getColumn(id) {
        const tile = this.tiles[id];
        if (!tile) {
            throw new Error(`Column not found: ${id}`);
        }
        if (tile.type !== 'column') {
            throw new Error(`Tile ${id} is not a column, found ${tile.type}`);
        }
        return tile;
    }
    /**
     * Get a panel by ID, throwing an error if not found
     * @throws Error if the tile is not found or not a panel
     */
    getPanel(id) {
        const tile = this.tiles[id];
        if (!tile) {
            throw new Error(`Panel not found: ${id}`);
        }
        if (tile.type !== 'panel') {
            throw new Error(`Tile ${id} is not a panel, found ${tile.type}`);
        }
        return tile;
    }
    /**
     * Get a tab by ID, throwing an error if not found
     * @throws Error if the tile is not found or not a tab
     */
    getTab(id) {
        const tile = this.tiles[id];
        if (!tile) {
            throw new Error(`Tab not found: ${id}`);
        }
        if (tile.type !== 'tab') {
            throw new Error(`Tile ${id} is not a tab, found ${tile.type}`);
        }
        return tile;
    }
    /**
     * Get children of a tile filtered by type
     */
    getChildren(parentId, tileType = null) {
        return Object.values(this.tiles)
            .filter((tile) => Boolean(tile))
            .filter(tile => tile.parent === parentId)
            .filter(tile => !tileType || tile.type === tileType);
    }
    /**
     * Get the currently active panel
     */
    getActivePanelTile() {
        return this.activePanel ? this.getTile(this.activePanel) : null;
    }
    /**
     * Get the active tab of the active panel
     */
    getActivePanelTab() {
        const panel = this.getActivePanelTile();
        if (!panel || !panel.activeTab)
            return null;
        return this.getTile(panel.activeTab);
    }
    /**
     * Get the content associated with a tab
     */
    getTabContent(tabId) {
        const tab = this.getTile(tabId);
        if (!tab)
            return null;
        return this.getTile(tab.content);
    }
    /**
     * Add a new tile to the layout
     * @param tile Tile to add (requires type property)
     * @returns ID of the new tile
     */
    addTile(tile) {
        // Generate ID if not provided
        const id = tile.id || generateId();
        // Create a new tile with the ID
        const newTile = {
            id,
            ...tile
        };
        // Add to state
        this.tiles[id] = newTile;
        // Notify state changes
        this.notifyStateChange();
        return id;
    }
    /**
     * Update a tile with the given changes
     * @param id ID of the tile to update
     * @param updates Changes to apply
     * @returns True if successful
     */
    updateTile(id, updates) {
        const tile = this.getTile(id);
        if (!tile)
            return false;
        // Special handling for grid rows to ensure it's always an array
        if (tile.type === 'grid' && 'rows' in updates) {
            updates = { ...updates, rows: updates.rows || [] };
        }
        // Create the new tile state
        const newTile = { ...tile, ...updates };
        // Check if anything actually changed
        if (JSON.stringify(tile) === JSON.stringify(newTile)) {
            // No changes, return without updating or notifying
            return true;
        }
        // Apply changes
        this.tiles[id] = newTile;
        // Notify state changes
        this.notifyStateChange();
        return true;
    }
    /**
     * Find all tiles that reference the specified tile
     * @param tileId ID of the tile to find references to
     * @returns Array of tiles that reference the specified tile
     */
    findTilesReferencingTile(tileId) {
        return Object.values(this.tiles).filter(tile => {
            if (tile.type === 'grid' && tile.rows.includes(tileId)) {
                return true;
            }
            if (tile.type === 'row' && tile.columns.includes(tileId)) {
                return true;
            }
            if (tile.type === 'column' && tile.child === tileId) {
                return true;
            }
            if (tile.type === 'panel' && tile.tabs.includes(tileId)) {
                return true;
            }
            if (tile.type === 'tab' && tile.content === tileId) {
                return true;
            }
            return false;
        });
    }
    /**
     * Remove a tile from the layout
     * @param id ID of the tile to remove
     * @returns True if successful
     */
    removeTile(id) {
        const tile = this.getTile(id);
        if (!tile)
            return false;
        // Check for references to this tile throughout the layout
        const referencingTiles = this.findTilesReferencingTile(id);
        // Remove references to this tile
        referencingTiles.forEach((referencingTile) => {
            if (referencingTile.type === 'grid' && 'rows' in referencingTile) {
                this.updateTile(referencingTile.id, {
                    rows: referencingTile.rows.filter((rowId) => rowId !== id)
                });
            }
            else if (referencingTile.type === 'row' && 'columns' in referencingTile) {
                this.updateTile(referencingTile.id, {
                    columns: referencingTile.columns.filter((colId) => colId !== id)
                });
            }
            else if (referencingTile.type === 'column' && 'child' in referencingTile) {
                if (referencingTile.child === id) {
                    this.updateTile(referencingTile.id, {
                        child: ''
                    });
                }
            }
            else if (referencingTile.type === 'panel' && 'tabs' in referencingTile) {
                this.updateTile(referencingTile.id, {
                    tabs: referencingTile.tabs.filter((tabId) => tabId !== id),
                    activeTab: referencingTile.activeTab === id ? null : referencingTile.activeTab
                });
            }
            else if (referencingTile.type === 'tab' && 'content' in referencingTile) {
                if (referencingTile.content === id) {
                    this.updateTile(referencingTile.id, {
                        content: ''
                    });
                }
            }
        });
        // Delete the tile itself
        delete this.tiles[id];
        // Notify state changes
        this.notifyStateChange();
        return true;
    }
    /**
     * Set the active panel
     */
    setActivePanel(id) {
        const panel = this.getTile(id);
        if (!panel || panel.type !== 'panel')
            return false;
        this.activePanel = id;
        return true;
    }
    /**
     * Set the active tab
     */
    setActiveTab(tabId) {
        const tab = this.getTile(tabId);
        if (!tab || tab.type !== 'tab')
            return false;
        const panelId = tab.parent;
        if (!panelId)
            return false;
        const panel = this.getTile(panelId);
        if (!panel || panel.type !== 'panel')
            return false;
        this.updateTile(panelId, { activeTab: tabId });
        this.setActivePanel(panelId);
        // Also set as focused tab
        this.focusedActiveTab = tabId;
        return true;
    }
    /**
     * Set the focused active tab
     * @param tabId ID of the tab to focus
     * @returns True if successful
     */
    setFocusedActiveTab(tabId) {
        const tab = this.getTile(tabId);
        if (!tab || tab.type !== 'tab')
            return false;
        // Ensure the tab is active in its panel
        const panelId = tab.parent;
        if (!panelId)
            return false;
        const panel = this.getTile(panelId);
        if (!panel || panel.type !== 'panel')
            return false;
        // If the tab is not the active one in its panel, make it active
        if (panel.activeTab !== tabId) {
            this.updateTile(panelId, { activeTab: tabId });
        }
        // Set the panel as active if it's not already
        if (this.activePanel !== panelId) {
            this.setActivePanel(panelId);
        }
        // Set the focused tab
        this.focusedActiveTab = tabId;
        return true;
    }
    /**
     * Get the focused active tab
     */
    getFocusedActiveTabTile() {
        return this.focusedActiveTab ? this.getTile(this.focusedActiveTab) : null;
    }
    /**
     * Reorder tabs within a panel
     */
    reorderTabs(panelId, oldIndex, newIndex) {
        const panel = this.getTile(panelId);
        if (!panel || panel.type !== 'panel')
            return false;
        // Make sure indices are valid
        if (oldIndex < 0 || oldIndex >= panel.tabs.length ||
            newIndex < 0 || newIndex >= panel.tabs.length) {
            return false;
        }
        // Create a new array with reordered tabs
        const reorderedTabs = [...panel.tabs];
        const [movedTab] = reorderedTabs.splice(oldIndex, 1);
        reorderedTabs.splice(newIndex, 0, movedTab);
        // Update the panel
        this.updateTile(panelId, { tabs: reorderedTabs });
        return true;
    }
    /**
     * Move a tab from one panel to another
     * @param tabId The tab to move
     * @param targetPanelId The panel to move the tab to
     * @param targetIndex Optional index where to insert the tab in the target panel
     * @returns boolean True if the operation was successful
     */
    moveTab(tabId, targetPanelId, targetIndex) {
        try {
            // Get the tab and determine source panel
            const tab = this.getTab(tabId);
            const sourcePanelId = tab.parent;
            if (!sourcePanelId) {
                throw new Error(`Tab ${tabId} has no parent panel`);
            }
            // Get the source and target panels
            const sourcePanel = this.getPanel(sourcePanelId);
            const targetPanel = this.getPanel(targetPanelId);
            // Check if the tab is in the source panel
            if (!sourcePanel.tabs.includes(tabId)) {
                throw new Error(`Tab ${tabId} is not in source panel ${sourcePanelId}`);
            }
            // Create copies of the tab arrays
            const sourceTabs = [...sourcePanel.tabs];
            const targetTabs = [...targetPanel.tabs];
            // Remove the tab from the source panel
            const sourceIndex = sourceTabs.indexOf(tabId);
            sourceTabs.splice(sourceIndex, 1);
            // Find a new active tab for the source panel if needed
            let newSourceActiveTab = sourcePanel.activeTab;
            if (sourcePanel.activeTab === tabId) {
                // Get the next tab, or the previous one if there is no next
                newSourceActiveTab = sourceTabs.length > 0 ? sourceTabs[Math.min(sourceIndex, sourceTabs.length - 1)] : null;
            }
            // Add the tab to the target panel at the specified index or at the end
            if (targetIndex !== undefined && targetIndex >= 0 && targetIndex <= targetTabs.length) {
                targetTabs.splice(targetIndex, 0, tabId);
            }
            else {
                // Append to the end
                targetTabs.push(tabId);
            }
            // Update the tab's parent reference
            this.updateTile(tabId, { parent: targetPanelId });
            // Update the source panel
            this.updateTile(sourcePanelId, {
                tabs: sourceTabs,
                activeTab: newSourceActiveTab
            });
            // Update the target panel and make the moved tab active
            this.updateTile(targetPanelId, {
                tabs: targetTabs,
                activeTab: tabId
            });
            // Set the target panel as active
            this.setActivePanel(targetPanelId);
            // Check if source panel is now empty and should be cleaned up
            this.cleanupContainers(sourcePanelId);
            return true;
        }
        catch (error) {
            console.error('Error in moveTab:', error);
            return false;
        }
    }
    /**
     * Split a panel to create a new layout
     * @param tabId The tab to move to the new panel
     * @param targetPanelId The panel being split
     * @param direction The direction to split ('top', 'right', 'bottom', 'left')
     * @returns boolean True if the split operation was successful
     */
    splitPanel(tabId, targetPanelId, direction) {
        try {
            // Get the tab and determine source panel
            const tab = this.getTab(tabId);
            const sourcePanelId = tab.parent;
            if (!sourcePanelId) {
                throw new Error(`Tab ${tabId} has no parent panel`);
            }
            // Get the source and target panels
            const sourcePanel = this.getPanel(sourcePanelId);
            const targetPanel = this.getPanel(targetPanelId);
            // Check if the tab is in the source panel
            if (!sourcePanel.tabs.includes(tabId)) {
                throw new Error(`Tab ${tabId} is not in source panel ${sourcePanelId}`);
            }
            // Prevent splitting a panel with its only tab
            if (sourcePanelId === targetPanelId && sourcePanel.tabs.length === 1) {
                console.warn('Cannot split a panel with its only tab');
                return false;
            }
            // Find the column containing the target panel
            const targetParentId = targetPanel.parent;
            if (!targetParentId) {
                throw new Error(`Target panel ${targetPanelId} has no parent`);
            }
            const parentColumn = this.getColumn(targetParentId);
            // Different approach based on split direction
            if (direction === 'left' || direction === 'right') {
                // Horizontal split: Find the row containing the column with the target panel
                const rowId = parentColumn.parent;
                if (!rowId) {
                    throw new Error(`Column ${parentColumn.id} has no parent row`);
                }
                const row = this.getRow(rowId);
                // Find the index of the current column in the row
                const columnIndex = row.columns.indexOf(parentColumn.id);
                if (columnIndex === -1) {
                    throw new Error(`Column ${parentColumn.id} not found in row ${rowId}`);
                }
                // Get the target column width
                let targetColumnWidth = parentColumn.width;
                // Calculate new width - half of the current column's width
                const newWidth = {
                    value: targetColumnWidth.value / 2,
                    unit: targetColumnWidth.unit
                };
                // Update existing column width
                this.updateTile(parentColumn.id, { width: newWidth });
                // Create a new column
                const newColumnId = this.addTile({
                    type: 'column',
                    parent: rowId,
                    width: newWidth,
                    child: undefined
                });
                // Create a new panel in the new column
                const newPanelId = this.addTile({
                    type: 'panel',
                    parent: newColumnId,
                    tabs: [],
                    activeTab: null
                });
                // Update new column with the new panel
                this.updateTile(newColumnId, { child: newPanelId });
                // Update row with the new column in the correct position (left or right of existing)
                const newColumns = [...row.columns];
                if (direction === 'right') {
                    // Add after the current column
                    newColumns.splice(columnIndex + 1, 0, newColumnId);
                }
                else {
                    // Add before the current column
                    newColumns.splice(columnIndex, 0, newColumnId);
                }
                this.updateTile(rowId, { columns: newColumns });
                // Update the target panel's parent reference if needed
                if (parentColumn.child === targetPanelId) {
                    this.updateTile(targetPanelId, { parent: parentColumn.id });
                }
                // Move the tab to the new panel
                return this.moveTab(tabId, newPanelId);
            }
            else if (direction === 'top' || direction === 'bottom') {
                // Vertical split
                // Check if the column already contains a grid
                let gridId;
                if (parentColumn.child === targetPanelId) {
                    // Column directly contains the panel, need to create a grid
                    // Create new grid
                    gridId = this.addTile({
                        type: 'grid',
                        parent: parentColumn.id,
                        rows: []
                    });
                    // Create two rows for the existing panel and the new one
                    const rowHeight = { value: 50, unit: '%' }; // 50% each
                    // Create rows in appropriate order based on direction
                    const firstRowId = this.addTile({
                        type: 'row',
                        parent: gridId,
                        height: rowHeight,
                        columns: []
                    });
                    const secondRowId = this.addTile({
                        type: 'row',
                        parent: gridId,
                        height: rowHeight,
                        columns: []
                    });
                    // Create a column for the existing panel
                    const existingPanelColumnId = this.addTile({
                        type: 'column',
                        parent: direction === 'top' ? secondRowId : firstRowId,
                        width: { value: 100, unit: '%' }, // 100% of row width
                        child: targetPanelId
                    });
                    // Create a column for the new panel
                    const newPanelColumnId = this.addTile({
                        type: 'column',
                        parent: direction === 'top' ? firstRowId : secondRowId,
                        width: { value: 100, unit: '%' }, // 100% of row width
                        child: undefined
                    });
                    // Create a new panel
                    const newPanelId = this.addTile({
                        type: 'panel',
                        parent: newPanelColumnId,
                        tabs: [],
                        activeTab: null
                    });
                    // Update column with new panel
                    this.updateTile(newPanelColumnId, { child: newPanelId });
                    // Update rows with their columns
                    this.updateTile(firstRowId, {
                        columns: [direction === 'top' ? newPanelColumnId : existingPanelColumnId]
                    });
                    this.updateTile(secondRowId, {
                        columns: [direction === 'top' ? existingPanelColumnId : newPanelColumnId]
                    });
                    // Update grid with rows
                    this.updateTile(gridId, {
                        rows: [firstRowId, secondRowId]
                    });
                    // Update target panel parent to point to its new column
                    this.updateTile(targetPanelId, { parent: existingPanelColumnId });
                    // Update column to point to the grid instead of directly to the panel
                    this.updateTile(parentColumn.id, { child: gridId });
                    // Move the tab to the new panel
                    return this.moveTab(tabId, newPanelId);
                }
                else {
                    // Column already contains a grid or something else, need to handle differently
                    const existingChild = parentColumn.child;
                    if (!existingChild) {
                        throw new Error(`Column ${parentColumn.id} has no child`);
                    }
                    try {
                        // Try to get the child as a grid
                        const grid = this.getGrid(existingChild);
                        gridId = existingChild;
                        // Find the row containing the target panel
                        let targetRowId = null;
                        let targetRow = null;
                        for (const rowId of grid.rows) {
                            try {
                                const row = this.getRow(rowId);
                                // Check if any column in this row contains the target panel
                                for (const colId of row.columns) {
                                    try {
                                        const col = this.getColumn(colId);
                                        if (col.child === targetPanelId) {
                                            targetRowId = rowId;
                                            targetRow = row;
                                            break;
                                        }
                                    }
                                    catch (error) {
                                        console.error(`Error checking column ${colId}:`, error);
                                        // Continue to next column
                                    }
                                }
                                if (targetRowId)
                                    break;
                            }
                            catch (error) {
                                console.error(`Error checking row ${rowId}:`, error);
                                // Continue to next row
                            }
                        }
                        if (!targetRowId || !targetRow) {
                            throw new Error(`Could not find row containing panel ${targetPanelId}`);
                        }
                        // Get the target row height
                        let targetRowHeight = targetRow.height;
                        // Calculate new height - half of the current row's height
                        const newHeight = {
                            value: targetRowHeight.unit === '%' ? targetRowHeight.value / 2 : 50,
                            unit: targetRowHeight.unit
                        };
                        // Update existing row height
                        this.updateTile(targetRowId, { height: newHeight });
                        // Create a new row
                        const newRowId = this.addTile({
                            type: 'row',
                            parent: gridId,
                            height: newHeight,
                            columns: []
                        });
                        // Create a new column in the row
                        const newColumnId = this.addTile({
                            type: 'column',
                            parent: newRowId,
                            width: { value: 100, unit: '%' }, // 100% of the row
                            child: undefined
                        });
                        // Create a new panel in the column
                        const newPanelId = this.addTile({
                            type: 'panel',
                            parent: newColumnId,
                            tabs: [],
                            activeTab: null
                        });
                        // Update column with panel
                        this.updateTile(newColumnId, { child: newPanelId });
                        // Update row with column
                        this.updateTile(newRowId, { columns: [newColumnId] });
                        // Update grid with new row in the right position
                        const newRows = [...grid.rows];
                        const rowIndex = grid.rows.indexOf(targetRowId);
                        if (direction === 'bottom') {
                            // Add after the current row
                            newRows.splice(rowIndex + 1, 0, newRowId);
                        }
                        else {
                            // Add before the current row
                            newRows.splice(rowIndex, 0, newRowId);
                        }
                        this.updateTile(gridId, { rows: newRows });
                        // Move the tab to the new panel
                        return this.moveTab(tabId, newPanelId);
                    }
                    catch (error) {
                        // The child is not a grid
                        console.error(`Error handling existing child ${existingChild}:`, error);
                        return false;
                    }
                }
            }
            return false;
        }
        catch (error) {
            console.error('Error in splitPanel:', error);
            return false;
        }
    }
    /**
     * Recursively checks and cleans up empty containers
     * Traverses up the hierarchy to remove unnecessary container structures
     */
    cleanupContainers(tileId) {
        const tile = this.getTile(tileId);
        if (!tile)
            return;
        let parentId = tile.parent;
        if (tile.dontClean) {
            if (parentId) {
                // If the tile is marked as dontClean, we don't try to clean it but 
                // go up the hierarchy in case if the parent's parent needs cleaning.
                this.cleanupContainers(parentId);
            }
            return;
        }
        let shouldRemove = false;
        // Check if the tile should be removed based on its type
        switch (tile.type) {
            case 'panel':
                // Remove the panel if it has no tabs
                const panel = tile;
                shouldRemove = panel.tabs.length === 0;
                break;
            case 'column':
                // Remove the column if it has no valid child
                const column = tile;
                shouldRemove = !column.child || !this.tiles[column.child];
                // Redistribute the width to siblings if removing
                if (shouldRemove && parentId) {
                    const row = this.getRow(parentId);
                    const siblingColumns = row.columns.filter(id => id !== tileId);
                    if (siblingColumns.length > 0) {
                        this.redistributeWidths(column);
                    }
                }
                break;
            case 'row':
                // Remove the row if it has no columns
                const row = tile;
                shouldRemove = row.columns.length === 0;
                // Redistribute the height to siblings if removing
                if (shouldRemove && parentId) {
                    const grid = this.getGrid(parentId);
                    const siblingRows = grid.rows.filter(id => id !== tileId);
                    if (siblingRows.length > 0) {
                        this.redistributeHeights(row);
                    }
                }
                break;
            case 'grid':
                // Remove the grid if it has no rows and is not the root
                const grid = tile;
                shouldRemove = grid.rows.length === 0 && !!grid.parent;
                // Check if we can simplify the grid hierarchy (for non root grids)
                if (!shouldRemove && grid.parent) {
                    // In case if we have a grid with a single row and a single column, we can remove 
                    // the grid and replace it with the only child of the column from that grid.
                    if (grid.rows.length === 1) {
                        const row = this.getRow(grid.rows[0]);
                        // Just one row with a single column
                        if (row.columns.length === 1) {
                            const column = this.getColumn(row.columns[0]);
                            const child = column.child;
                            if (child) {
                                const parentColumn = this.getColumn(grid.parent);
                                this.updateTile(parentColumn.id, { child: child });
                                this.updateTile(child, { parent: parentColumn.id });
                                this.updateTile(grid.id, { rows: [] });
                                this.removeTile(grid.id);
                            }
                        }
                    }
                }
                break;
        }
        // Remove the tile if necessary and continue cleanup with parent
        if (shouldRemove) {
            this.removeTile(tileId);
        }
        if (parentId) {
            this.cleanupContainers(parentId);
        }
    }
    /**
     * Redistributes width from a removed column to its sibling columns
     * @param removedColumnId ID of the column being removed
     */
    redistributeWidths(removedColumn) {
        // Get the available width to redistribute
        const availableWidth = removedColumn.width;
        if (availableWidth.value <= 0)
            return;
        // Get the parent row to find siblings
        const parentRowId = removedColumn.parent;
        if (!parentRowId)
            return;
        const parentRow = this.getTile(parentRowId);
        if (!parentRow || parentRow.type !== 'row')
            return;
        // Get sibling columns (excluding the one being removed)
        const siblingColumnIds = parentRow.columns.filter(id => id !== removedColumn.id);
        if (siblingColumnIds.length === 0)
            return;
        // Get the sibling column objects
        const siblingColumns = siblingColumnIds
            .map(id => this.getTile(id))
            .filter(Boolean);
        if (siblingColumns.length === 0)
            return;
        // Check if any siblings have % units
        const percentColumns = siblingColumns.filter(col => col.width.unit === '%');
        // If we have percentage-based columns, only distribute to those
        const targetColumns = percentColumns.length > 0 ? percentColumns : siblingColumns;
        const totalExistingWidth = targetColumns.reduce((sum, col) => sum + col.width.value, 0);
        if (totalExistingWidth <= 0)
            return; // Prevent division by zero
        // Distribute proportionally among target columns
        targetColumns.forEach(column => {
            const proportion = column.width.value / totalExistingWidth;
            const newWidth = column.width.value + (availableWidth.value * proportion);
            this.updateTile(column.id, {
                width: {
                    value: newWidth,
                    unit: column.width.unit
                }
            });
        });
    }
    /**
     * Redistributes height from a removed row to its sibling rows
     * @param removedRow The row being removed
     */
    redistributeHeights(removedRow) {
        // Get the available height to redistribute
        const availableHeight = removedRow.height;
        if (availableHeight.value <= 0)
            return;
        // Get the parent grid to find siblings
        const parentGridId = removedRow.parent;
        if (!parentGridId)
            return;
        const parentGrid = this.getTile(parentGridId);
        if (!parentGrid || parentGrid.type !== 'grid')
            return;
        // Get sibling rows (excluding the one being removed)
        const siblingRowIds = parentGrid.rows.filter(id => id !== removedRow.id);
        if (siblingRowIds.length === 0)
            return;
        // Get the sibling row objects
        const siblingRows = siblingRowIds
            .map(id => this.getTile(id))
            .filter(Boolean);
        if (siblingRows.length === 0)
            return;
        // Check if any siblings have % units
        const percentRows = siblingRows.filter(row => row.height.unit === '%');
        // If we have percentage-based rows, only distribute to those
        const targetRows = percentRows.length > 0 ? percentRows : siblingRows;
        const totalExistingHeight = targetRows.reduce((sum, row) => sum + row.height.value, 0);
        if (totalExistingHeight <= 0)
            return; // Prevent division by zero
        // Distribute proportionally among target rows
        targetRows.forEach(row => {
            const proportion = row.height.value / totalExistingHeight;
            const newHeight = row.height.value + (availableHeight.value * proportion);
            this.updateTile(row.id, {
                height: {
                    value: newHeight,
                    unit: row.height.unit
                }
            });
        });
    }
    /**
     * Reset the layout (but keeping the theme, components, etc.)
     */
    /**
     * Reset the layout (but keeping the theme, components, etc.)
     */
    resetTiles() {
        this.tiles = {};
        this.activePanel = null;
        this.focusedActiveTab = null;
        this.rootGridId = "";
    }
    /**
     * Validate the current layout
     * @returns True if layout is valid, false otherwise
     */
    validateLayout() {
        return this.validationMiddleware.validate(this);
    }
    /**
     * Reset to the default layout
     */
    resetToDefaultLayout() {
        this.validationMiddleware.resetToDefault(this);
    }
    /**
     * Add a custom validator to the validation middleware
     * @param validator The validator to add
     */
    addValidator(validator) {
        this.validationMiddleware.validators.push(validator);
    }
    /**
     * Set the default layout creator function
     * @param creator Function that creates a default layout
     */
    setDefaultLayoutCreator(creator) {
        this.validationMiddleware.defaultLayoutCreator = creator;
    }
    /**
     * Subscribe to layout validation errors
     * @param handler Function to call when validation errors occur
     * @returns Unsubscribe function
     */
    onValidationError(handler) {
        this.validationMiddleware.addErrorHandler(handler);
        // Return unsubscribe function
        return () => {
            this.validationMiddleware.errorHandlers =
                this.validationMiddleware.errorHandlers.filter(h => h !== handler);
        };
    }
    /**
     * Adds a grid to the layout
     * @param parentId Optional parent column ID
     * @returns ID of the new grid
     * @throws Error if parent hierarchy rules are violated
     */
    addGrid(parentId) {
        if (parentId) {
            const parent = this.getTile(parentId);
            if (!parent) {
                throw new Error(`Parent tile with ID ${parentId} not found`);
            }
            // Validate parent hierarchy rules
            if (parent.type === 'column') {
                // A grid can be added to a column
            }
            else {
                throw new Error(`Cannot add a grid to a parent of type ${parent.type}. Grids can only be children of columns.`);
            }
        }
        else {
            if (this.rootGridId) {
                throw new Error("Cannot add a grid to the root grid");
            }
        }
        const gridId = this.addTile({
            type: 'grid',
            parent: parentId || null,
            rows: []
        });
        if (!parentId) {
            this.rootGridId = gridId;
        }
        // If parent is a column, update the column's child reference
        if (parentId) {
            const parent = this.getTile(parentId);
            if (parent && parent.type === 'column') {
                this.updateTile(parentId, {
                    child: gridId
                });
            }
        }
        return gridId;
    }
    /**
     * Adds a row to a grid
     * @param parentId ID of the parent grid
     * @param height Height of the row as a string (e.g., "100%", "260px")
     * @returns ID of the new row
     * @throws Error if parent hierarchy rules are violated
     */
    addRow(parentId, height) {
        const parent = this.getTile(parentId);
        if (!parent) {
            throw new Error(`Parent tile with ID ${parentId} not found`);
        }
        // Validate parent hierarchy rules
        if (parent.type !== 'grid') {
            throw new Error(`Cannot add a row to a parent of type ${parent.type}. Rows can only be children of grids.`);
        }
        const grid = parent;
        let sizeInfo;
        if (!height) {
            if (grid.rows.length === 0) {
                height = '100%';
            }
            else {
                // Calculate default percentage for the new row
                const newRowPercentage = 100 / (grid.rows.length + 1);
                // Get existing percentage-based rows
                const percentageRows = grid.rows
                    .map(id => this.getTile(id))
                    .filter((row) => !!row && row.height.unit === '%');
                if (percentageRows.length > 0) {
                    // Calculate total existing percentage
                    const totalExistingPercentage = percentageRows.reduce((sum, row) => sum + row.height.value, 0);
                    // Calculate scaling factor to redistribute remaining space
                    const remainingPercentage = 100 - newRowPercentage;
                    const scalingFactor = remainingPercentage / totalExistingPercentage;
                    // Update existing rows proportionally
                    percentageRows.forEach(row => {
                        const newPercentage = row.height.value * scalingFactor;
                        this.updateTile(row.id, {
                            height: { value: newPercentage, unit: '%' }
                        });
                    });
                }
                height = `${newRowPercentage}%`;
            }
        }
        // Parse the height value
        sizeInfo = parseSizeValue(height);
        // Create the row
        const rowId = this.addTile({
            type: 'row',
            parent: parentId,
            columns: [],
            height: sizeInfo
        });
        // Update grid's rows
        this.updateTile(parentId, {
            rows: [...grid.rows, rowId]
        });
        //this.recalculateLayout(parentId);
        return rowId;
    }
    /**
     * Adds a column to a row
     * @param parentId ID of the parent row
     * @param width Width of the column as a string (e.g., "100%", "260px")
     * @returns ID of the new column
     * @throws Error if parent hierarchy rules are violated
     */
    addColumn(parentId, width) {
        const parent = this.getTile(parentId);
        if (!parent) {
            throw new Error(`Parent tile with ID ${parentId} not found`);
        }
        // Validate parent hierarchy rules
        if (parent.type !== 'row') {
            throw new Error(`Cannot add a column to a parent of type ${parent.type}. Columns can only be children of rows.`);
        }
        const row = parent;
        // if width is not specified, calculate it based on the existing columns
        // if it's specified - use it as is for px and calculate size for % based. But also run detection of invalid sizes
        if (!width) {
            if (row.columns.length === 0) {
                width = '100%';
            }
            else {
                // Calculate default percentage for the new column
                const newColumnPercentage = 100 / (row.columns.length + 1);
                // Get existing percentage-based columns
                const percentageColumns = row.columns
                    .map(id => this.getTile(id))
                    .filter((col) => !!col && col.width.unit === '%');
                if (percentageColumns.length > 0) {
                    // Calculate total existing percentage
                    const totalExistingPercentage = percentageColumns.reduce((sum, col) => sum + col.width.value, 0);
                    // Calculate scaling factor to redistribute remaining space
                    const remainingPercentage = 100 - newColumnPercentage;
                    const scalingFactor = remainingPercentage / totalExistingPercentage;
                    // Update existing columns proportionally
                    percentageColumns.forEach(col => {
                        const newPercentage = col.width.value * scalingFactor;
                        this.updateTile(col.id, {
                            width: { value: newPercentage, unit: '%' }
                        });
                    });
                }
                width = `${newColumnPercentage}%`;
            }
        }
        else {
            // Validate the width
            try {
                parseSizeValue(width);
            }
            catch (error) {
                throw new Error(`Invalid width format: ${width}. Expected format: "100%" or "260px"`);
            }
        }
        // Parse the width value
        const sizeInfo = parseSizeValue(width);
        // Create the column
        const columnId = this.addTile({
            type: 'column',
            parent: parentId,
            child: '',
            width: sizeInfo
        });
        // Update row's columns
        this.updateTile(parentId, {
            columns: [...row.columns, columnId]
        });
        //this.recalculateLayout(parentId);
        return columnId;
    }
    /**
     * Adds a panel to a column
     * @param parentId ID of the parent column
     * @returns ID of the new panel
     * @throws Error if parent hierarchy rules are violated
     */
    addPanel(parentId) {
        const parent = this.getTile(parentId);
        if (!parent) {
            throw new Error(`Parent tile with ID ${parentId} not found`);
        }
        // Validate parent hierarchy rules
        if (parent.type !== 'column') {
            throw new Error(`Cannot add a panel to a parent of type ${parent.type}. Panels can only be children of columns.`);
        }
        const column = parent;
        // Check if column already has a child
        if (column.child && this.getTile(column.child)) {
            throw new Error(`Column ${parentId} already has a child. Remove it first.`);
        }
        // Create the panel
        const panelId = this.addTile({
            type: 'panel',
            parent: parentId,
            tabs: [],
            activeTab: null
        });
        // Update column's child
        this.updateTile(parentId, {
            child: panelId
        });
        return panelId;
    }
    /**
     * Adds a tab to a panel
     * @param panelId ID of the parent panel
     * @param name Name of the tab
     * @param setActive Whether to set this tab as active
     * @param isLazy Whether to add the tab as lazy
     * @returns ID of the new tab
     * @throws Error if parent hierarchy rules are violated
     */
    addTabToPanel(panel, name, setActive = true, isLazy = false) {
        // Create the tab
        const tabId = this.addTile({
            type: 'tab',
            parent: panel.id,
            name,
            content: '',
            isLazy
        });
        // Create content for the tab
        const contentId = this.addTile({
            type: 'content',
            parent: tabId,
        });
        // Update tab with its content
        this.updateTile(tabId, {
            content: contentId
        });
        // Update panel's tabs
        const updatedTabs = [...panel.tabs, tabId];
        this.updateTile(panel.id, {
            tabs: updatedTabs,
            ...(setActive ? { activeTab: tabId } : {})
        });
        return tabId;
    }
    /**
     * Adds a tab to a panel. If the parent is a grid, it will be added to the first row and column of the grid.
     * @param parentId ID of the parent container (grid, column, or panel)
     * @param name Name of the tab
     * @param setActive Whether to set this tab as active
     * @param isLazy Whether to add the tab as lazy
     * @returns ID of the new tab
     */
    addTab(parentId, name, setActive = true, isLazy = false) {
        const parent = this.getTile(parentId);
        if (!parent) {
            throw new Error(`Parent tile with ID ${parentId} not found`);
        }
        switch (parent.type) {
            case 'panel':
                // Panel is the direct container for tabs, add directly
                return this.addTabToPanel(parent, name, setActive, isLazy);
            case 'column':
                // Need to check if column already has a child
                const column = parent;
                let panelId = '';
                if (column.child) {
                    // If column already has a child, it might be a panel or a grid
                    const child = this.getTile(column.child);
                    if (!child) {
                        // Child reference exists but tile doesn't, create a new panel
                        panelId = this.addPanel(column.id);
                    }
                    else if (child.type === 'panel') {
                        // Column already has a panel, use it
                        panelId = child.id;
                    }
                    else if (child.type === 'grid') {
                        // Column has a grid, find or create a panel in the grid
                        panelId = this.findOrCreatePanelInGrid(child.id);
                    }
                    else {
                        // Column has some other child (should be a content), replace with a panel
                        this.removeTile(child.id);
                        panelId = this.addPanel(column.id);
                    }
                }
                else {
                    // Column has no child, add a panel
                    panelId = this.addPanel(column.id);
                }
                return this.addTabToPanel(this.getPanel(panelId), name, setActive, isLazy);
            case 'row':
                // For rows, we need to find or create a column to contain our panel
                const row = parent;
                let columnId;
                if (row.columns.length > 0) {
                    // Use the last column in the row
                    columnId = row.columns[row.columns.length - 1];
                }
                else {
                    // Create a new column
                    columnId = this.addColumn(row.id);
                }
                // Recursively call addTab with the column ID
                return this.addTab(columnId, name, setActive, isLazy);
            case 'grid':
                // Find or create a panel in the grid
                const panelInGridId = this.findOrCreatePanelInGrid(parentId);
                return this.addTabToPanel(this.getPanel(panelInGridId), name, setActive, isLazy);
            default:
                throw new Error(`Cannot add a tab to a parent of type ${parent.type}. Use a panel, column, row, or grid.`);
        }
    }
    /**
     * Finds an existing panel in a grid or creates a new one
     * @param gridId ID of the grid
     * @returns ID of a panel in the grid
     */
    findOrCreatePanelInGrid(gridId) {
        const grid = this.getGrid(gridId);
        // Try to find an existing panel
        for (const rowId of grid.rows) {
            const row = this.getRow(rowId);
            for (const columnId of row.columns) {
                try {
                    const col = this.getColumn(columnId);
                    if (col.child) {
                        const child = this.getTile(col.child);
                        if (child && child.type === 'panel') {
                            return child.id;
                        }
                    }
                }
                catch (error) {
                    console.error(`Error processing column ${columnId}:`, error);
                }
            }
        }
        // No panel found, create a new row, column, and panel
        let rowId;
        if (grid.rows.length === 0) {
            // No rows exist, create one
            rowId = this.addRow(gridId);
        }
        else {
            // Use the last row
            rowId = grid.rows[grid.rows.length - 1];
        }
        // Create a column in the row
        const columnId = this.addColumn(rowId);
        // Create a panel in the column
        return this.addPanel(columnId);
    }
    /**
     * Ads a new tab in the active panel
     * @param name Name of the tab
     * @returns ID of the new tab, or null if no active panel exists
     */
    addTabInActivePanel(name, setActive = true, isLazy = false) {
        const activePanel = this.getActivePanelTile();
        if (!activePanel)
            return null;
        const tabId = this.addTabToPanel(activePanel, name, setActive, isLazy);
        return tabId;
    }
    /**
     * Serialize the layout to JSON
     */
    serializeLayout() {
        // Get all tiles as an array
        const tilesArray = Object.values(this.tiles);
        // Add metadata for focused tab
        const metadata = {
            activePanel: this.activePanel,
            focusedActiveTab: this.focusedActiveTab
        };
        return JSON.stringify({
            tiles: tilesArray,
            metadata
        });
    }
    /**
     * Setup the ttabs instance with the given tiles
     * This will validate the layout and reset to default if invalid
     * @param tiles The tiles to set up
     * @param options Optional settings for active panel and focused tab
     */
    setup(tiles, { activePanel, focusedActiveTab } = {}) {
        this.resetTiles();
        let didResetToDefaultLayout = false;
        let success = true;
        try {
            const rootGrids = tiles.filter(tile => tile.type === 'grid' && !tile.parent);
            if (rootGrids.length !== 1) {
                throw new Error('Invalid number of root grids. There has to be a single root grid.');
            }
            tiles.forEach((tile) => {
                this.tiles[tile.id] = tile;
            });
            this.rootGridId = rootGrids[0].id;
            // Validate the layout after setting up tiles
            this.validateLayout();
            if (activePanel) {
                const panel = this.getTile(activePanel);
                if (panel) {
                    this.activePanel = activePanel;
                }
            }
            if (focusedActiveTab) {
                const focusedTab = this.getTile(focusedActiveTab);
                if (focusedTab) {
                    this.focusedActiveTab = focusedActiveTab;
                }
                else {
                    this.findAndSetDefaultFocusedTab();
                }
            }
            this.notifyStateChange();
        }
        catch (error) {
            console.error('Failed to setup ttabs:', error);
            // If setup fails, reset to default layout
            this.resetToDefaultLayout();
            didResetToDefaultLayout = true;
            success = false;
        }
        // Call the callback with the result if provided
        this.setupFromScratchCallback?.({
            didResetToDefaultLayout
        });
    }
    /**
     * Setup the ttabs instance with the given tiles as a record
     * @param tiles The tiles to set up as a record
     * @param callback Optional callback that will be called when setup is complete with information about the result
     */
    setupWithRecord(tiles) {
        this.resetTiles();
        this.tiles = tiles;
        try {
            this.rootGridId = this.findRootGridId();
            // Validate the layout after setting up tiles
            this.validateLayout();
            this.notifyStateChange();
            this.setupFromScratchCallback?.({
                didResetToDefaultLayout: false
            });
        }
        catch (error) {
            console.error('Failed to setup ttabs with record:', error);
            // If setup fails, reset to default layout
            this.resetToDefaultLayout();
            this.setupFromScratchCallback?.({
                didResetToDefaultLayout: true
            });
        }
    }
    /**
     * Deserialize a layout from JSON
     * @param json The JSON string to deserialize
     * @param callback Optional callback that will be called when setup is complete with information about the result
     * @returns True if deserialization was successful, false otherwise
     */
    deserializeLayout(json) {
        try {
            const parsed = JSON.parse(json);
            if (parsed && typeof parsed === 'object' && 'tiles' in parsed && Array.isArray(parsed.tiles)) {
                this.setup(parsed.tiles, {
                    activePanel: parsed.metadata?.activePanel,
                    focusedActiveTab: parsed.metadata?.focusedActiveTab
                });
                return true;
            }
            return false;
        }
        catch (e) {
            console.error('Failed to deserialize layout:', e);
            // If we couldn't even parse the JSON, reset to default layout and call callback
            this.resetToDefaultLayout();
            this.setupFromScratchCallback?.({
                didResetToDefaultLayout: true
            });
            return false;
        }
    }
    /**
     * Find and set a default focused tab when none is specified
     * Used after deserializing older layouts
     */
    findAndSetDefaultFocusedTab() {
        // If active panel exists, try to use its active tab
        if (this.activePanel) {
            const panel = this.getTile(this.activePanel);
            if (panel && panel.activeTab) {
                this.focusedActiveTab = panel.activeTab;
                return;
            }
        }
        // Otherwise find the first panel with an active tab
        const panelsWithActiveTabs = Object.values(this.tiles)
            .filter((tile) => tile.type === 'panel' && tile.activeTab !== null && !!this.getTile(tile.activeTab));
        if (panelsWithActiveTabs.length > 0 && panelsWithActiveTabs[0].activeTab) {
            this.focusedActiveTab = panelsWithActiveTabs[0].activeTab;
            this.activePanel = panelsWithActiveTabs[0].id;
        }
    }
    /**
     * Close a tab and remove it
     * @param tabId The ID of the tab to close
     * @returns True if successful
     */
    closeTab(tabId) {
        try {
            const tab = this.getTile(tabId);
            if (!tab || tab.type !== 'tab')
                return false;
            const panelId = tab.parent;
            if (!panelId)
                return false;
            const panel = this.getTile(panelId);
            if (!panel || panel.type !== 'panel')
                return false;
            // Find index of tab to remove
            const tabIndex = panel.tabs.indexOf(tabId);
            if (tabIndex === -1)
                return false;
            // If we're closing the focused tab, we need to find a new tab to focus
            let newFocusedTab = null;
            if (this.focusedActiveTab === tabId) {
                // First try to find another tab in the same panel
                if (panel.tabs.length > 1) {
                    // Use the same logic as for activeTab - get next or previous tab
                    const newTabIndex = Math.min(tabIndex, panel.tabs.length - 2);
                    newFocusedTab = panel.tabs[newTabIndex];
                    if (newFocusedTab === tabId) {
                        // Extra safety check
                        newFocusedTab = panel.tabs.find(t => t !== tabId) || null;
                    }
                }
                else {
                    // Look for a tab in another panel
                    const otherPanels = Object.values(this.tiles)
                        .filter((tile) => tile.type === 'panel' && tile.id !== panelId && tile.tabs.length > 0);
                    // Find the first panel with tabs and use its active tab
                    const otherPanel = otherPanels[0];
                    if (otherPanel && otherPanel.activeTab) {
                        newFocusedTab = otherPanel.activeTab;
                    }
                }
            }
            // Remove tab
            this.removeTile(tabId);
            // Update panel's tabs array
            const newTabs = [...panel.tabs];
            newTabs.splice(tabIndex, 1);
            // If closed tab was active, activate another tab
            let newActiveTab = panel.activeTab;
            if (panel.activeTab === tabId) {
                if (newTabs.length > 0) {
                    // Activate either the tab at the same index, or the last tab
                    const newActiveIndex = Math.min(tabIndex, newTabs.length - 1);
                    newActiveTab = newTabs[newActiveIndex];
                }
                else {
                    newActiveTab = null;
                }
            }
            // Update panel
            this.updateTile(panelId, {
                tabs: newTabs,
                activeTab: newActiveTab
            });
            // Update the focused tab if needed
            if (newFocusedTab) {
                this.focusedActiveTab = newFocusedTab;
            }
            // Clean up empty containers
            this.cleanupContainers(panelId);
            return true;
        }
        catch (error) {
            console.error('Error closing tab:', error);
            return false;
        }
    }
    /**
     * Set or update the theme
     */
    setTheme(theme) {
        console.log('Setting theme:', theme.name);
        // Resolve the theme to handle inheritance
        const resolvedTheme = resolveTheme(theme);
        // Ensure we're making a full replacement to trigger reactivity
        this.theme = { ...resolvedTheme };
        console.log('Theme updated:', this.theme.name);
    }
    // @TODO: consider making a universal search function for all properties of a tab (including its content)
    /**
     * Get all lazy tabs in a panel, grid, or across the entire layout
     * @param containerId Optional ID of a panel or grid to search within. If not provided, searches all panels.
     * @returns Array of lazy tab tiles
     */
    getLazyTabs(containerId) {
        if (containerId) {
            const container = this.getTile(containerId);
            if (!container)
                return [];
            if (container.type === 'panel') {
                // Get lazy tabs directly from the panel
                const panel = container;
                return panel.tabs
                    .map(tabId => this.getTile(tabId))
                    .filter((tab) => !!tab && tab.isLazy === true);
            }
            else if (container.type === 'grid') {
                // Find all panels within the grid and get their lazy tabs
                const lazyTabs = [];
                // Recursively find all panels within the grid
                const findPanelsInGrid = (gridId) => {
                    const grid = this.getGrid(gridId);
                    const panels = [];
                    for (const rowId of grid.rows) {
                        try {
                            const row = this.getRow(rowId);
                            for (const colId of row.columns) {
                                try {
                                    const col = this.getColumn(colId);
                                    if (col.child) {
                                        const child = this.getTile(col.child);
                                        if (child?.type === 'panel') {
                                            panels.push(child);
                                        }
                                        else if (child?.type === 'grid') {
                                            // Recursive case - grid within grid
                                            panels.push(...findPanelsInGrid(child.id));
                                        }
                                    }
                                }
                                catch (error) {
                                    console.error(`Error processing column ${colId}:`, error);
                                }
                            }
                        }
                        catch (error) {
                            console.error(`Error processing row ${rowId}:`, error);
                        }
                    }
                    return panels;
                };
                const panels = findPanelsInGrid(container.id);
                // Get lazy tabs from each panel
                for (const panel of panels) {
                    const panelLazyTabs = panel.tabs
                        .map(tabId => this.getTile(tabId))
                        .filter((tab) => !!tab && tab.isLazy === true);
                    lazyTabs.push(...panelLazyTabs);
                }
                return lazyTabs;
            }
            return [];
        }
        else {
            // Search all panels for lazy tabs
            return Object.values(this.tiles)
                .filter((tile) => tile.type === 'tab' && tile.isLazy === true);
        }
    }
    // @TODO: do we need it?
    /**
     * Recalculates the layout for a container and its children
     * @param containerId ID of the container to recalculate
     */
    recalculateLayout(containerId) {
        const container = this.getTile(containerId);
        if (!container)
            return;
        // Depending on container type, recalculate children
        if (container.type === 'row') {
            const row = container;
            const containerEl = document.querySelector(`[data-tile-id="${containerId}"]`);
            if (!containerEl)
                return;
            const containerWidth = containerEl.offsetWidth;
            const columns = row.columns.map(id => {
                const col = this.getTile(id);
                if (!col)
                    return null;
                return { id, size: col.width };
            }).filter(Boolean);
            const calculatedSizes = calculateSizes(containerWidth, columns);
            // Apply the calculated sizes
            calculatedSizes.forEach(({ id, computedSize, scaledDown }) => {
                const column = this.getTile(id);
                if (!column)
                    return;
                // Store computed size for reference during resize operations
                this.updateTile(id, { computedSize });
                // Update tiles based on their unit type
                if (column.width.unit === 'px' && scaledDown) {
                    // If original px size was scaled down, update the rendered size
                    this.updateTile(id, {
                        computedSize,
                        originalWidth: column.width.value
                    });
                }
                else if (column.width.unit === '%') {
                    // Update percentage based on computed size
                    this.updateTile(id, {
                        width: {
                            value: (computedSize / containerWidth) * 100,
                            unit: '%'
                        }
                    });
                }
            });
        }
        else if (container.type === 'grid') {
            const grid = container;
            const containerEl = document.querySelector(`[data-tile-id="${containerId}"]`);
            if (!containerEl)
                return;
            const containerHeight = containerEl.offsetHeight;
            const rows = grid.rows.map(id => {
                const row = this.getTile(id);
                if (!row)
                    return null;
                return { id, size: row.height };
            }).filter(Boolean);
            const calculatedSizes = calculateSizes(containerHeight, rows);
            // Apply the calculated sizes
            calculatedSizes.forEach(({ id, computedSize, scaledDown }) => {
                const row = this.getTile(id);
                if (!row)
                    return;
                // Store computed size for reference during resize operations
                this.updateTile(id, { computedSize });
                // Update tiles based on their unit type
                if (row.height.unit === 'px' && scaledDown) {
                    // If original px size was scaled down, update the rendered size
                    this.updateTile(id, {
                        computedSize,
                        originalHeight: row.height.value
                    });
                }
                else if (row.height.unit === '%') {
                    // Update percentage based on computed size
                    this.updateTile(id, {
                        height: {
                            value: (computedSize / containerHeight) * 100,
                            unit: '%'
                        }
                    });
                }
            });
        }
    }
    /**
     * Gets the parent column of a tile
     * @param tileId ID of the tile to find the parent column for
     * @returns The parent column if found, or null if not found
     */
    getParentColumn(tileId) {
        const tile = this.getTile(tileId);
        if (!tile)
            return null;
        // If parent is a column, return it
        if (tile.parent) {
            const parent = this.getTile(tile.parent);
            if (parent && parent.type === 'column') {
                return parent;
            }
        }
        return null;
    }
    /**
     * Create a new grid or get the existing root grid as an object
     * @returns A Grid object for the root grid
     */
    newGrid() {
        // If there's already a root grid, use it
        if (this.rootGridId) {
            return new Grid(this, this.rootGridId);
        }
        // Otherwise create a new grid
        const gridId = this.addGrid();
        return new Grid(this, gridId);
    }
    /**
     * Get a grid object for an existing grid
     * @param id ID of the grid
     * @returns A Grid object
     */
    getGridObject(id) {
        return new Grid(this, id);
    }
    /**
     * Get a row object for an existing row
     * @param id ID of the row
     * @returns A Row object
     */
    getRowObject(id) {
        return new Row(this, id);
    }
    /**
     * Get a column object for an existing column
     * @param id ID of the column
     * @returns A Column object
     */
    getColumnObject(id) {
        return new Column(this, id);
    }
    /**
     * Get a panel object for an existing panel
     * @param id ID of the panel
     * @returns A TtabsPanel object
     */
    getPanelObject(id) {
        return new Panel(this, id);
    }
    /**
     * Get a tab object for an existing tab
     * @param id ID of the tab
     * @returns A Tab object
     */
    getTabObject(id) {
        return new Tab(this, id);
    }
}