<?php
namespace Components\Resources;

use Components\Base;
use Components\Button;
use Components\Dropdown;
use Components\Support\Palette;
use Components\Ui\EmptyState;

/**
 * Generic, reusable list/table presentation for simple CRUD collections.
 * All inline HTML is captured with output buffering and returned as a string.
 *
 * Options:
 * - title: string heading (optional)
 * - items: array of associative arrays (each item at least has id/name; configurable via keys below)
 * - id_key: string key for id in items (default 'id')
 * - label_key: string key for label/title in items (fallback to name_key then 'name')
 * - name_key: string deprecated, use label_key instead
 * - sublabel_key: string key for sublabel (optional, fallback to #id · Active)
 * - status_key: string key for status (optional, used for default sublabel)
 * - outer_attrs: string raw attributes to inject on the outermost container (e.g., id="..." hx-swap-oob="...")
 * - empty_text: string to show when items is empty
 * - panel_type: 'modal' | 'drawer' (default 'modal') — controls default targets for actions
 * - panel_id: element id of the modal/drawer to target (default 'config-modal' for modal, 'config-drawer' for drawer)
 * - panel_target_id: id of the content container inside the panel (default: panel_id + '-body')
 * - new_action: array with:
 *     - label: string button label
 *     - url: string href/hx-get URL (can contain {id} placeholder; unused for new)
 *     - attrs: array of extra attributes [key => value]
 *     - variant: 'primary'|'secondary' (affects classes)
 * - row_actions: array of actions like new_action but {id} in url will be replaced
 *     - For modal/drawer actions, attributes typically include [ 'command' => 'show-modal', 'commandfor' => 'config-modal'|'config-drawer', 'hx-target' => '#config-*-body', 'hx-swap' => 'innerHTML', 'hx-get' => 'URL' ]
 *     - You may also pass 'hx_get' key as shorthand for setting hx-get
 * - menu_model_class: string FQCN of a model that can provide CRUD menu items (optional)
 * - menu_self_route: string router name to include in menu item context (optional)
 * - menu_label: string label for the per-row dropdown trigger (default: UI_ACTIONS/Actions)
 * - show_header: bool whether to show table column headers (default: true if columns present)
 * - name_label: string custom label for the first column (default: Name)
 * - table_id: string unique identifier for this table, used for persistent settings (optional)
 * - columns: array of associative arrays for additional columns:
 *     - key: string key to read from item
 *     - label: string header label
 *     - class: string extra classes for cell and header (e.g. responsive hidden)
 *     - header_class: string extra classes for header only
 *     - render: callable function($val, $item) to customize output
 *     - type: 'text' | 'identity' (default 'text')
 *     - optional: bool whether the column visibility can be toggled by the user (default: false)
 *
 * - refresh_action: array with:
 *     - label: string refresh button label (optional, defaults to icon only)
 *     - url: string refresh URL (hx-get)
 *     - target: string hx-target (optional)
 *     - trigger: string hx-trigger (optional, e.g. "click, refresh-list from:body")
 *     - attrs: array of extra attributes [key => value]
 *
 * - hide_name_column: bool whether to hide the default first identity column (default: false)
 *
 * Optional icon per row (before the name):
 * - icon (array, defaults for all rows) with keys:
 *     - svg: string raw SVG markup (required to show icon)
 *     - shape: 'round'|'square' (default 'round')
 *     - color: palette token (blue|indigo|orange|yellow|green|gray|purple|red|pink|brand) or Tailwind utilities
 *     - variant: 'tint'|'solid' (default 'tint') — tint uses bg-100 + text-600; solid uses bg-500 + text-white
 *     - size: 'sm'|'md' (default 'md')
 * - Alternatively, per-item overrides:
 *     - item['icon_svg'] or item['icon']['svg']
 *     - item['icon_shape'] or item['icon']['shape']
 *     - item['icon_color'] or item['icon']['color']
 *     - item['icon_variant'] or item['icon']['variant']
 *     - item['icon_size'] or item['icon']['size']
 */
class ResourceList
{
    public static function render(array $options): string
    {
        $title = (string)($options['title'] ?? '');
        $items = is_array($options['items'] ?? null) ? $options['items'] : [];
        $idKey = (string)($options['id_key'] ?? 'id');
        $labelKey = (string)($options['label_key'] ?? $options['name_key'] ?? 'name');
        $statusKey = (string)($options['status_key'] ?? 'status');
        $outerAttrs = trim((string)($options['outer_attrs'] ?? ''));
        $emptyText = (string)($options['empty_text'] ?? 'No entries found.');
        $newAction = is_array($options['new_action'] ?? null) ? $options['new_action'] : null;
        $rowActions = is_array($options['row_actions'] ?? null) ? $options['row_actions'] : [];
        $columns = is_array($options['columns'] ?? null) ? $options['columns'] : [];
        $pagination = is_array($options['pagination'] ?? null) ? $options['pagination'] : null;
        $search = is_array($options['search'] ?? null) ? $options['search'] : null;
        $filters = is_array($options['filters'] ?? null) ? $options['filters'] : [];
        $refreshAction = is_array($options['refresh_action'] ?? null) ? $options['refresh_action'] : null;
        $showHeader = (bool)($options['show_header'] ?? (!empty($columns) || !empty($options['name_label'])));
        $tableId = (string)($options['table_id'] ?? $options['name'] ?? '');
        $layout = (string)($options['layout'] ?? 'default');
        $headerPadding = (string)($options['header_padding'] ?? 'py-1.5');
        $rowPadding = (string)($options['row_padding'] ?? 'py-3');
        $firstColPadding = (string)($options['first_col_padding'] ?? ($layout === 'clean' ? 'pl-3' : 'pl-4 sm:pl-0'));
        $lastColPadding = (string)($options['last_col_padding'] ?? ($layout === 'clean' ? 'pr-3' : 'pr-4 sm:pr-0'));

        // Identify optional columns and load their persistent visibility state
        $optionalColumns = [];
        $initialVisibility = [];
        if ($tableId !== '') {
            foreach ($columns as $idx => $col) {
                if (!empty($col['optional'])) {
                    $colKey = (string)($col['key'] ?? $idx);
                    $defaultVisible = (bool)($col['default'] ?? true);
                    $optionalColumns[$colKey] = (string)($col['label'] ?? $colKey);
                    $initialVisibility[$colKey] = $defaultVisible;
                }
            }
            if (!empty($optionalColumns)) {
                $columnStateMap = \Components\ConfigStore::getColumnStateMap();
                foreach ($optionalColumns as $colKey => $label) {
                    $prefKey = 'resource-list:' . $tableId . ':' . $colKey;
                    // Override default with saved state if available
                    if (isset($columnStateMap[$prefKey])) {
                        $initialVisibility[$colKey] = (bool)$columnStateMap[$prefKey];
                    }
                }
            }
        }

        // Optional per-row menu provider (model-based)
        $menuModelClass = (string)($options['menu_model_class'] ?? ($options['model_class'] ?? ''));
        $menuSelfRoute = (string)($options['menu_self_route'] ?? ($options['self_route'] ?? ''));
        $menuLabel = (string)($options['menu_label'] ?? \mh_lng('UI_ACTIONS','Actions'));
        $rowActionsAsMenu = (bool)($options['row_actions_as_menu'] ?? false);
        $menuTrigger = (string)($options['menu_trigger'] ?? 'default');

        // Panel configuration (modal or drawer)
        $panelType = (string)($options['panel_type'] ?? 'modal');
        $panelId = (string)($options['panel_id'] ?? \Components\Support\Htmx::resolvePanelId(null, $panelType === 'drawer'));
        $panelTargetId = (string)($options['panel_target_id'] ?? ($panelId . '-body'));

        ob_start();

        $xDataAttr = '';
        $tableIdClean = preg_replace('/[^a-z0-9_-]/i', '-', $tableId);
        $instanceId = bin2hex(random_bytes(4));
        $toggleEventName = 'toggle-column-' . $tableIdClean . '-' . $instanceId;

        // Pass event name down to headers
        $options['toggle_event_name'] = $toggleEventName;

        if (!empty($optionalColumns)) {
            $xDataValue = '{
                instanceId: ' . json_encode($instanceId) . ',
                visibleColumns: ' . json_encode($initialVisibility, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ',
                toggleColumn(key) {
                    if (this._toggling === key) return;
                    this._toggling = key;
                    setTimeout(() => { if(this._toggling === key) this._toggling = null; }, 100);

                    this.visibleColumns[key] = !this.visibleColumns[key];
                    this.visibleColumns = Object.assign({}, this.visibleColumns);

                    // Sync dropdown icons across potential portals
                    const isVisible = this.visibleColumns[key];
                    document.querySelectorAll(".col-check-" + this.instanceId + "-" + key).forEach(svg => {
                        svg.classList.toggle("opacity-100", isVisible);
                        svg.classList.toggle("opacity-0", !isVisible);
                    });

                    this.persistColumnState(key);
                },
                persistColumnState(key) {
                    const columnMap = {};
                    columnMap["resource-list:" + ' . json_encode($tableId) . ' + ":" + key] = this.visibleColumns[key];
                    if (window.htmx) {
                        htmx.ajax("POST", "' . \Components\Router::url('save_column_state_bulk') . '", {
                            values: { column_map: JSON.stringify(columnMap) },
                            swap: "none"
                        });
                    }
                }
            }';
            $xDataAttr = \Components\Base::attrs(['x-data' => $xDataValue]);
        }

        $containerClass = 'ResourceList rounded-2xl border border-divider bg-white p-3 dark:border-white/10 dark:bg-slate-900';
        if ($layout === 'clean') {
//            $containerClass = 'bg-white dark:bg-slate-900';
        }
        ?>
        <div  x-cloak <?= $outerAttrs; ?> <?= $xDataAttr; ?> @<?= \Components\Base::h($toggleEventName) ?>.window="toggleColumn($event.detail.key)" class="<?= $containerClass ?>">
            <?= self::renderHeader($title, $newAction, $panelId, $panelTargetId, $search, $filters, $tableId, $refreshAction); ?>
            <?php if (empty($items)): ?>
                <?php
                $emptyState = is_array($options['empty_state'] ?? null) ? $options['empty_state'] : [];
                // Prefer explicit empty_title, else provided in empty_state, else fallback
                $emptyTitle = (string)($options['empty_title'] ?? ($emptyState['title'] ?? \mh_lng('UI_NO_RESULTS','No result found')));
                if (!isset($emptyState['title'])) { $emptyState['title'] = $emptyTitle; }
                if (!isset($emptyState['text'])) { $emptyState['text'] = $emptyText; }
                if (!isset($emptyState['icon']) && ($search || !empty($filters))) { $emptyState['icon'] = \Components\Base::SVG_SEARCH; }

                $hasSearchValue = $search && !empty($search['value']);
                $hasFilterValue = false;
                foreach ($filters as $f) {
                    if (!empty($f['value'])) { $hasFilterValue = true; break; }
                }

                if (!isset($emptyState['action']) && ($hasSearchValue || $hasFilterValue)) {
                    $resetUrl = $search['url'] ?? null;
                    $resetTarget = $search['target'] ?? null;
                    if (!$resetUrl || !$resetTarget) {
                        foreach ($filters as $f) {
                            if (isset($f['url'])) { $resetUrl = $f['url']; }
                            if (isset($f['target'])) { $resetTarget = $f['target']; }
                            if ($resetUrl && $resetTarget) break;
                        }
                    }

                    if ($resetUrl && $resetTarget) {
                        $resetParams = [];
                        $clearKeys = [];
                        if ($search) {
                            // Search is always cleared to an empty string.
                            $resetParams['search'] = '';
                            $clearKeys[] = 'search';
                        }
                        foreach ($filters as $fKey => $f) {
                            $fValue = $f['value'] ?? '';
                            // For numeric filters (like IDs), reset to '0' to avoid 'int' validation errors in MailBeez.
                            // For others, use an empty string.
                            $resetParams[(string)$fKey] = (is_numeric($fValue) && $fValue !== '') ? '0' : '';
                            $clearKeys[] = (string)$fKey;
                        }
                        if ($pagination) {
                            $resetParams['page'] = '1';
                        }
                        if (!empty($clearKeys)) {
                            $resetParams['_clear_filters'] = implode(',', $clearKeys);
                        }

                        $emptyState['action'] = [
                            'label' => \mh_lng('UI_CLEAR_FILTERS', 'Clear search and filters'),
                            'variant' => 'secondary',
                            'attrs' => [
                                'hx-get' => $resetUrl,
                                'hx-target' => $resetTarget,
                                'hx-swap' => 'outerHTML',
                                'hx-vals' => json_encode($resetParams),
                            ]
                        ];
                    }
                }

                echo EmptyState::render($emptyState);
                ?>
            <?php else: ?>
                <div class="<?= ($layout === 'clean1' ? '' : 'mt-4 ') ?>flow-root overflow-hidden">
                    <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
                        <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
                            <?php
                            $tableClass = "relative min-w-full";
                            if ($layout === 'clean') {
                                $tableClass .= " border-separate border-spacing-0";
                            } else {
                                $tableClass .= " divide-y divide-divider-strong dark:divide-white/10";
                            }
                            ?>
                            <table class="<?= $tableClass ?>">
                                <?= self::renderColumnsHeader($options, $columns, !empty($options['icon']) || !empty($items[0]['icon']), $showHeader, $optionalColumns, $tableId, $headerPadding, $firstColPadding, $lastColPadding, $instanceId, $initialVisibility); ?>
                                <tbody class="<?= ($layout === 'clean' ? '' : 'divide-y divide-divider dark:divide-white/10') ?>">
                                    <?php foreach ($items as $it): ?>
                                        <?= self::renderItemRow(
                                            $it,
                                            $options,
                                            $idKey,
                                            $labelKey,
                                            $statusKey,
                                            $panelId,
                                            $panelTargetId,
                                            $menuModelClass,
                                            $menuSelfRoute,
                                            $menuLabel,
                                            $rowActions,
                                            $columns,
                                            $rowActionsAsMenu,
                                            $menuTrigger,
                                            $rowPadding,
                                            $firstColPadding,
                                            $lastColPadding
                                        ); ?>
                                    <?php endforeach; ?>
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
                <?= self::renderPagination($pagination, $layout, $tableId); ?>
            <?php endif; ?>
        </div>
        <?php
        return ob_get_clean();
    }

    private static function btnClasses(string $variant): string
    {
        if ($variant === 'primary') {
            return 'inline-flex items-center rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-white hover:bg-primary/90';
        }
        return 'inline-flex items-center rounded-md border border-gray-200 bg-white px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-100 dark:border-white/10 dark:bg-slate-800 dark:text-gray-200';
    }

    private static function renderAttrs(array $attrs): string
    {
        $out = '';
        foreach ($attrs as $k => $v) {
            if ($v === null || $v === false) { continue; }
            if ($k === 'hx_get') { $k = 'hx-get'; }
            $out .= ' ' . htmlspecialchars((string)$k, ENT_QUOTES, 'UTF-8') . '="' . htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8') . '"';
        }
        return $out;
    }

    private static function addDrawerParam(string $url, string $panelId): string
    {
        // Append drawer=1 when targeting a drawer panel and param is absent
        $isDrawer = (substr($panelId, -6) === 'drawer');
        if (!$isDrawer) { return $url; }
        if ($url === '' || stripos($url, 'drawer=') !== false) { return $url; }
        $sep = (strpos($url, '?') === false) ? '?' : '&';
        return $url . $sep . 'drawer=1';
    }

    private static function normalizePanelAttrs(array $attrs, string $panelId, string $panelTargetId): array
    {
        // Only normalize if we have some htmx action or if command is already set
        $hasHtmx = !empty($attrs['hx-get']) || !empty($attrs['hx-post']) || !empty($attrs['hx_get']);
        if (!$hasHtmx && !isset($attrs['command'])) {
            return $attrs;
        }

        if (!isset($attrs['command'])) { $attrs['command'] = 'show-modal'; }
        if (!isset($attrs['commandfor'])) { $attrs['commandfor'] = $panelId; }
        if (!isset($attrs['hx-target'])) { $attrs['hx-target'] = '#' . $panelTargetId; }
        if (!isset($attrs['hx-swap'])) { $attrs['hx-swap'] = 'innerHTML'; }
        if (!isset($attrs['hx-disinherit'])) { $attrs['hx-disinherit'] = '*'; }
        // If hx-get already present and panel is a drawer, ensure drawer=1 is included
        if (isset($attrs['hx-get'])) {
            $attrs['hx-get'] = self::addDrawerParam((string)$attrs['hx-get'], $panelId);
        }
        return $attrs;
    }

    private static function renderHeader(string $title, ?array $newAction, string $panelId, string $panelTargetId, ?array $search = null, array $filters = [], string $tableId = '', ?array $refreshAction = null): string
    {
        ob_start();
        ?>
        <div class="flex items-center justify-between mb-3 gap-1">
            <div class="flex items-center gap-4 flex-1 min-w-0">
<!--                <div class="text-sm font-semibold text-gray-900 dark:text-gray-100 shrink-0">--><?php //echo \Components\Base::h($title); ?><!--</div>-->
                <div class="flex items-center gap-2 flex-1">
                    <?php if ($search):
                        $searchValue = (string)($search['value'] ?? '');
                        $searchPlaceholder = (string)($search['placeholder'] ?? \mh_lng('UI_SEARCH', 'Search...'));
                        $searchAttrs = is_array($search['attrs'] ?? null) ? $search['attrs'] : [];
                        if (!isset($searchAttrs['hx-trigger'])) { $searchAttrs['hx-trigger'] = 'keyup changed delay:500ms, search'; }
                        if (!isset($searchAttrs['hx-target']) && isset($search['target'])) { $searchAttrs['hx-target'] = $search['target']; }
                        if (!isset($searchAttrs['hx-get']) && isset($search['url'])) { $searchAttrs['hx-get'] = self::cleanUrl((string)$search['url']); }
                        if (!isset($searchAttrs['hx-push-url'])) { $searchAttrs['hx-push-url'] = 'false'; }
                        if (!isset($searchAttrs['hx-include'])) { $searchAttrs['hx-include'] = 'closest div'; }

                        // Help HTMX focus preservation with a stable ID if one is provided or can be derived
                        $searchId = (string)($search['id'] ?? $searchAttrs['id'] ?? '');
                        unset($searchAttrs['id']);

                        if ($searchId === '' && $tableId !== '') {
                            $searchId = $tableId . '-search';
                        }
                        if ($searchId === '' && isset($search['target'])) {
                            $targetId = ltrim((string)$search['target'], '#');
                            if ($targetId !== '') {
                                $searchId = $targetId . '-search';
                            }
                        }

                        echo \Components\Form\Input::render('search', 'Search', $searchValue, [
                            'id' => $searchId,
                            'no_label' => true,
                            'size' => 'md',
                            'container_class' => 'flex-1 max-w-xs min-w-[120px]',
                            'prefix_icon_html' => \Components\Base::SVG_SEARCH,
                            'show_reset' => true,
                            'input_type' => 'search',
                            'input_attrs' => 'placeholder="' . \Components\Base::h($searchPlaceholder) . '" ' . self::renderAttrs($searchAttrs),
                        ]);
                        ?>
                    <?php endif; ?>

                    <?php if (!empty($filters)): ?>
                        <div class="flex items-center gap-0 ml-auto shrink-0 text-secondary/70 hover:text-secondary">
                            <?php foreach ($filters as $fKey => $f):
                                $fValue = (string)($f['value'] ?? '');
                                $fOptions = is_array($f['options'] ?? null) ? $f['options'] : [];
                                $fAttrs = is_array($f['attrs'] ?? null) ? $f['attrs'] : [];
                                if (!isset($fAttrs['hx-trigger'])) { $fAttrs['hx-trigger'] = 'change delay:50ms'; }
                                if (!isset($fAttrs['hx-target']) && isset($f['target'])) { $fAttrs['hx-target'] = $f['target']; }
                                if (!isset($fAttrs['hx-get']) && isset($f['url'])) { $fAttrs['hx-get'] = self::cleanUrl((string)$f['url']); }
                                if (!isset($fAttrs['hx-push-url'])) { $fAttrs['hx-push-url'] = 'false'; }
                                if (!isset($fAttrs['hx-include'])) { $fAttrs['hx-include'] = 'closest div'; }

                                $component = (string)($f['component'] ?? ($f['rich'] ?? false ? 'rich_select' : ''));

                                if ($component === 'rich_select') {
                                    // Enrich options with CollectionIdentity when identity/dot_class/icon is provided
                                    $hasIdentity = false;
                                    foreach ($fOptions as $opt) {
                                        if (is_array($opt) && (isset($opt['identity']) || isset($opt['dot_class']) || isset($opt['icon']))) { $hasIdentity = true; break; }
                                    }
                                    $rich = [];
                                    foreach ($fOptions as $val => $opt) {
                                        if (is_array($opt) && array_key_exists('subheader', $opt)) {
                                            $rich[$val] = ['subheader' => (string)$opt['subheader']];
                                            continue;
                                        }
                                        $rawLabel = is_array($opt) ? (string)($opt['label'] ?? $opt['text'] ?? $val) : (string)$opt;
                                        $isEmpty = ($val === '' || $val === '0' || $val === 0);

                                        // Requirement: if no value just show a filter name e.g. "Templates", no dot
                                        if ($isEmpty && isset($f['label'])) {
                                            $rawLabel = $f['label'];
                                        }

                                        $labelHtml = is_array($opt) ? ($opt['label_html'] ?? null) : null;
                                        if (!$labelHtml && is_array($opt) && isset($opt['badge'])) {
                                            $badge = is_array($opt['badge']) ? $opt['badge'] : ['token' => (string)$opt['badge']];
                                            $badgeLabel = (string)($badge['label'] ?? $rawLabel);
                                            $badgeType = (string)($badge['type'] ?? 'default');
                                            $badgeOpts = $badge;
                                            unset($badgeOpts['label'], $badgeOpts['type']);
                                            $labelHtml = \Components\Ui\Badge::render($badgeType, $badgeLabel, $badgeOpts);
                                        }

                                        if (!$labelHtml && $hasIdentity && is_array($opt) && !$isEmpty && (isset($opt['identity']) || isset($opt['dot_class']) || isset($opt['icon']))) {
                                            $identity = $opt['identity'] ?? (string)($opt['dot_class'] ?? 'bg-gray-400');
                                            $identityOpts = [];
                                            if (isset($opt['icon'])) { $identityOpts['icon'] = $opt['icon']; }
                                            if (isset($opt['icon_class'])) { $identityOpts['icon_class'] = $opt['icon_class']; }
                                            if (isset($opt['size'])) { $identityOpts['size'] = $opt['size']; }
                                            if (isset($opt['dot_size_class'])) { $identityOpts['dot_size_class'] = $opt['dot_size_class']; }
                                            $labelHtml = \Components\Ui\CollectionIdentity::render(\Components\Base::h($rawLabel), $identity, $identityOpts);
                                        }

                                        if ($labelHtml) {
                                            $rich[$val] = [ 'label' => $rawLabel, 'label_html' => $labelHtml, 'description' => (string)($opt['description'] ?? $opt['desc'] ?? '') ];
                                        } else {
                                            $rich[$val] = ['label' => $rawLabel];
                                            if (is_array($opt) && isset($opt['description'])) { $rich[$val]['description'] = $opt['description']; }
                                        }
                                    }
                                    $fOptions = $rich;

                                    echo \Components\Form\RichSelect::render($fKey, $fKey, $fOptions, $fValue, [
                                        'no_label' => true,
                                        'variant' => 'ghost',
                                        'size' => 'md',
                                        'container_class' => 'shrink-0',
                                        'input_attrs' => self::renderAttrs($fAttrs),
                                    ]);
                                } else {
                                    // Handle empty label override for standard Select
                                    if (isset($f['label'])) {
                                        foreach ($fOptions as $val => $opt) {
                                            $isEmpty = ($val === '' || $val === '0' || $val === 0);
                                            if ($isEmpty) {
                                                if (is_array($fOptions[$val])) {
                                                    $fOptions[$val]['label'] = $f['label'];
                                                } else {
                                                    $fOptions[$val] = $f['label'];
                                                }
                                            }
                                        }
                                    }
                                    echo \Components\Form\Select::render($fKey, $fKey, $fOptions, $fValue, [
                                        'no_label' => true,
                                        'variant' => 'ghost',
                                        'size' => 'md',
                                        'container_class' => 'shrink-0',
                                        'include_empty' => false,
                                        'input_attrs' => self::renderAttrs($fAttrs),
                                    ]);
                                }
                                ?>
                            <?php endforeach; ?>
                        </div>
                    <?php endif; ?>
                </div>
            </div>
            <div class="flex items-center gap-2">
                <?php if ($refreshAction):
                    $refreshAttrs = $refreshAction['attrs'] ?? [];
                    if (isset($refreshAction['url'])) { $refreshAttrs['hx-get'] = (string)$refreshAction['url']; }
                    if (!isset($refreshAttrs['hx-trigger'])) { $refreshAttrs['hx-trigger'] = $refreshAction['trigger'] ?? 'click'; }
                    if (!isset($refreshAttrs['hx-target']) && isset($refreshAction['target'])) { $refreshAttrs['hx-target'] = $refreshAction['target']; }
                    if (!isset($refreshAttrs['hx-target'])) { $refreshAttrs['hx-target'] = 'closest .ResourceList'; }
                    if (!isset($refreshAttrs['hx-swap'])) { $refreshAttrs['hx-swap'] = 'outerHTML'; }

                    echo \Components\Button::render((string)($refreshAction['label'] ?? ''), [
                        'variant' => 'ghost',
                        'size' => 'md',
                        'icon' => \Components\Base::SVG_REFRESH,
                        'attrs' => $refreshAttrs,
                    ]);
                endif; ?>

                <?php if ($newAction):
                    $naAttrs = $newAction['attrs'] ?? [];
                    if (isset($newAction['url'])) { $naAttrs['hx-get'] = self::addDrawerParam((string)$newAction['url'], $panelId); }
                    $naAttrs = self::normalizePanelAttrs($naAttrs, $panelId, $panelTargetId);
                    $naAttrs['data-no-global-loader'] = '1';
                    $naAttrs['data-no-local-loader'] = '1';

                    echo \Components\Button::render((string)($newAction['label'] ?? \mh_lng('UI_NEW', 'New')), [
                        'variant' => (string)($newAction['variant'] ?? 'primary'),
                        'size' => 'md',
                        'attrs' => $naAttrs,
                    ]);
                endif; ?>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

    public static function computeIconHtml(array $options, array $it): string
    {
        // Icon config (global defaults + per-item overrides)
        $iconOpt = $options['icon'] ?? null;
        $iconDefaults = is_array($iconOpt) ? $iconOpt : (is_string($iconOpt) ? ['svg' => $iconOpt] : []);
        $itIconOpt = $it['icon'] ?? null;
        $itIcon = is_array($itIconOpt) ? $itIconOpt : (is_string($itIconOpt) ? ['svg' => $itIconOpt] : []);
        $iconSvg = (string)($it['icon_svg'] ?? ($itIcon['svg'] ?? ($iconDefaults['svg'] ?? '')));
        $iconShape = strtolower((string)($it['icon_shape'] ?? ($itIcon['shape'] ?? ($iconDefaults['shape'] ?? 'round'))));
        $iconVariant = strtolower((string)($it['icon_variant'] ?? ($itIcon['variant'] ?? ($iconDefaults['variant'] ?? 'tint'))));
        $iconColor = (string)($it['icon_color'] ?? ($itIcon['color'] ?? ($iconDefaults['color'] ?? '')));
        $iconSize = strtolower((string)($it['icon_size'] ?? ($itIcon['size'] ?? ($iconDefaults['size'] ?? 'md'))));
        // Compute size classes
        $wrapSz = ($iconSize === 'sm') ? 'size-8 p-1.5' : 'size-10 p-2';
        $svgSz = ($iconSize === 'sm') ? 'size-6' : 'size-6';
        $shapeCls = ($iconShape === 'square') ? 'rounded-lg' : 'rounded-full';
        $baseWrapCls = (($iconShape === 'round') ? 'relative ' : '') . 'flex items-center justify-center shrink-0 ' . $shapeCls . ' ' . $wrapSz;
        if ($iconShape === 'square') { $baseWrapCls .= ' shadow-sm'; }
        // Resolve color classes
        $iconColor = trim($iconColor);
        $isUtility = $iconColor !== '' && (preg_match('/\b(bg|text|from|to|via|fill|stroke|ring|border)-/i', $iconColor) || strpos($iconColor, ' ') !== false);
        if ($isUtility) {
            $colorCls = $iconColor;
        } else {
            if ($iconVariant === 'solid') {
                $bgDot = Palette::dotBg($iconColor !== '' ? $iconColor : 'gray', 'gray');
                $colorCls = trim($bgDot . ' text-white');
            } else {
                $colorCls = Palette::classes($iconColor !== '' ? $iconColor : 'gray', 'gray');
            }
        }
        $iconWrapCls = trim($baseWrapCls . ' ' . $colorCls);
        // Size the SVG using CSS if applicable
        if ($iconSvg !== '' && strpos($iconSvg, '<svg') !== false) {
            $iconWrapCls .= ' [&_svg]:' . $svgSz;
        }

        if ($iconSvg === '') { return ''; }
        return '<div class="' . htmlspecialchars($iconWrapCls, ENT_QUOTES, 'UTF-8') . '">' . $iconSvg . '</div>';
    }

    private static function resolveMenuItems(string $menuModelClass, string $menuSelfRoute, int $id, array $it, string $panelId, string $panelTargetId): array
    {
        $menuItems = [];
        if ($menuModelClass !== '') {
            try {
                $ctx = [
                    'mode'      => 'list',
                    'id'        => $id,
                    'values'    => $it,
                    'selfRoute' => $menuSelfRoute,
                ];
                if (class_exists($menuModelClass)) {
                    if (method_exists($menuModelClass, 'getCrudMenuItems')) {
                        /** @phpstan-ignore-next-line */
                        $menu = $menuModelClass::getCrudMenuItems($ctx);
                        if (is_array($menu)) { $menuItems = $menu; }
                    } elseif (method_exists($menuModelClass, 'crudMenuItems')) {
                        /** @phpstan-ignore-next-line */
                        $menu = $menuModelClass::crudMenuItems($ctx);
                        if (is_array($menu)) { $menuItems = $menu; }
                    } else {
                        // Try instance method fallback if the model supports it
                        try {
                            $inst = new $menuModelClass($it);
                            if (method_exists($inst, 'getCrudMenuItems')) {
                                $menu = $inst->getCrudMenuItems($ctx);
                                if (is_array($menu)) { $menuItems = $menu; }
                            } else {
                                // Static property fallback
                                $rc = new \ReflectionClass($menuModelClass);
                                $defaults = $rc->getDefaultProperties();
                                $prop = $defaults['crudMenu'] ?? ($defaults['menu'] ?? null);
                                if (is_array($prop)) { $menuItems = $prop; }
                            }
                        } catch (\Throwable $e) { /* ignore */ }
                    }
                }
            } catch (\Throwable $e) { /* ignore */ }
        }

        if (!empty($menuItems)) {
            foreach ($menuItems as $miIdx => $mi) {
                if (!is_array($mi)) continue;
                $hrefI = isset($mi['href']) ? (string)$mi['href'] : '';
                $attrsI = is_array($mi['attrs'] ?? null) ? $mi['attrs'] : [];
                if ($hrefI === '') {
                    $attrsI = self::normalizePanelAttrs($attrsI, $panelId, $panelTargetId);
                }
                if (isset($attrsI['hx_get']) && !isset($attrsI['hx-get'])) {
                    $attrsI['hx-get'] = $attrsI['hx_get'];
                    unset($attrsI['hx_get']);
                }
                $menuItems[$miIdx]['attrs'] = $attrsI;
            }
        }
        return $menuItems;
    }

    private static function renderRowActions(array $rowActions, int $id, string $panelId, string $panelTargetId, bool $stopClickPropagation = false): string
    {
        ob_start();
        foreach ($rowActions as $action):
            $aAttrs = $action['attrs'] ?? [];
            if ($stopClickPropagation) {
                $aAttrs['onmousedown'] = 'event.stopPropagation();';
                // Stop bubbling so parent containers (cards/rows) don't also handle the click.
                $aAttrs['onclick'] = 'event.preventDefault(); event.stopPropagation();';
            }
            if (isset($action['url'])) {
                $url = (string)$action['url'];
                $repl = (string)$id;
                $url = str_replace('{id}', $repl, $url);
                $url = str_ireplace('%7Bid%7D', $repl, $url);
                if ($url !== '') {
                    if (($aAttrs['tag'] ?? '') === 'a') {
                        $aAttrs['href'] = $url;
                    } else {
                        $aAttrs['hx-get'] = self::addDrawerParam($url, $panelId);
                    }
                }
            }
            foreach ($aAttrs as $k => $v) {
                if (is_string($v)) {
                    $v = str_replace('{id}', (string)$id, $v);
                    $aAttrs[$k] = str_ireplace('%7Bid%7D', (string)$id, $v);
                }
            }
            $aAttrs = self::normalizePanelAttrs($aAttrs, $panelId, $panelTargetId);
            $variant = (string)($action['variant'] ?? 'secondary');
            $tag = (string)($aAttrs['tag'] ?? 'button');
            unset($aAttrs['tag']);
            if ($tag === 'a' && !isset($aAttrs['href'])) { $tag = 'button'; }
            $typeAttr = ($tag === 'button' && !isset($aAttrs['type'])) ? ' type="button"' : '';
            ?>
            <<?= $tag ?><?= $typeAttr ?> class="<?php echo self::btnClasses($variant); ?>"<?= self::renderAttrs($aAttrs); ?>>
                <?php echo \Components\Base::h((string)($action['label'] ?? 'Action')); ?>
            </<?= $tag ?>>
            <?php
        endforeach;
        return ob_get_clean();
    }

    private static function renderColumnsHeader(array $options, array $columns, bool $hasIcon, bool $showHeader = true, array $optionalColumns = [], string $tableId = 'resource-list', string $headerPadding = 'py-3.5', string $firstColPadding = 'pl-4 sm:pl-0', string $lastColPadding = 'pr-4 sm:pr-0', string $instanceId = '', array $initialVisibility = []): string
    {
        if (!$showHeader) { return ''; }
        ob_start();
        $nameLabel = (string)($options['name_label'] ?? \mh_lng('UI_NAME', 'Name'));
        $hideNameColumn = (bool)($options['hide_name_column'] ?? false);
        $layout = (string)($options['layout'] ?? 'clean');
        $getThClass = function($isFirst, $isLast) use ($layout) {
            if ($layout === 'clean') {
                $cls = "bg-divider/20 border-y border-divider/20";
                if ($isFirst) $cls .= " border-l rounded-l-lg";
                if ($isLast) $cls .= " border-r rounded-r-lg";
                return $cls;
            }
            return 'bg-elevated backdrop-blur-sm backdrop-filter dark:bg-slate-900/75';
        };
        $namePaddingClass = "$headerPadding pr-3 $firstColPadding";
        $isFirst = true;
        ?>
        <thead>
            <tr>
                <?php if (!$hideNameColumn): ?>
                    <?php $thBgClass = $getThClass($isFirst, false); $isFirst = false; ?>
                    <th scope="col" class="sticky top-0 z-10 <?= $thBgClass ?> <?= $namePaddingClass ?> text-left text-sm font-semibold text-gray-900 dark:text-gray-100">
                        <?php echo \Components\Base::h($nameLabel); ?>
                    </th>
                <?php endif; ?>
                <?php foreach ($columns as $idx => $col):
                    $colClass = (string)($col['header_class'] ?? $col['class'] ?? '');
                    $align = (string)($col['align'] ?? 'left');
                    $alignClass = ($align === 'right') ? 'text-right' : (($align === 'center') ? 'text-center' : 'text-left');
                    $paddingClass = ($isFirst) ? "$headerPadding pr-3 $firstColPadding" : "px-3 $headerPadding";

                    $thBgClass = $getThClass($isFirst, false); $isFirst = false;
                    $thClass = "sticky top-0 z-10 $thBgClass $paddingClass $alignClass text-sm font-semibold text-gray-900 dark:text-gray-100";
                    $isOptional = !empty($col['optional']);
                    $colKey = (string)($col['key'] ?? $idx);
                ?>
                    <th scope="col" class="<?php echo trim($thClass . ' ' . $colClass); ?>" <?php if ($isOptional): ?> x-show="visibleColumns['<?= $colKey ?>']" <?php endif; ?>>
                        <?php echo \Components\Base::h($col['label'] ?? ''); ?>
                    </th>
                <?php endforeach; ?>
                <?php
                $actionsPaddingClass = "$headerPadding $lastColPadding pl-3";
                $thBgClass = $getThClass($isFirst, true);
                ?>
                <th scope="col" class="font-normal sticky top-0 z-10 <?= $thBgClass ?> <?= $actionsPaddingClass ?>">
                    <div class="flex items-center justify-end gap-2">
                        <span class="sr-only"><?php echo \mh_lng('UI_ACTIONS', 'Actions'); ?></span>
                        <?php if (!empty($optionalColumns)):
                            $ddItems = [];
                            foreach ($optionalColumns as $colKey => $label) {
                                $isVisible = $initialVisibility[$colKey] ?? true;
                                $ddItems[] = [
                                    'label' => $label,
                                    'icon' => str_replace('<svg', '<svg class="col-check-' . $instanceId . '-' . $colKey . ' ' . ($isVisible ? 'opacity-100' : 'opacity-0') . '"', \Components\Base::SVG_MICRO_CHECK),
                                    'attrs' => [
                                        '@click' => "window.dispatchEvent(new CustomEvent('" . \Components\Base::h($options['toggle_event_name'] ?? ('toggle-column-' . $tableId)) . "', { detail: { key: '" . \Components\Base::h($colKey) . "' } }))",
                                    ]
                                ];
                            }
                            $triggerButton = \Components\Button::render('', [
                                'variant' => 'ghost',
                                'size' => 'md',
                                'icon' => \Components\Base::SVG_COLUMNS_2,
                                'attrs' => [
                                    'aria-label' => \mh_lng('UI_COLUMNS', 'Columns'),
                                ],
                            ]);
                            echo \Components\Dropdown::render([
                                'trigger_tag' => 'div',
                                'trigger_html' => $triggerButton,
                                'trigger_attrs' => [
                                    'onclick' => 'event.stopPropagation();',
                                    'onmousedown' => 'event.stopPropagation();',
                                    'data-no-modal' => 'true',
                                    'class' => 'shrink-0',
                                ],
                                'anchor' => 'bottom end',
                                'items' => $ddItems,
                            ]);
                        endif; ?>
                    </div>
                </th>
            </tr>
        </thead>
        <?php
        return ob_get_clean();
    }

    private static function renderPagination(?array $pagination, string $layout = 'clean', string $listId = ''): string
    {
        if (!$pagination) { return ''; }
        $baseUrl = (string)($pagination['url'] ?? '');
        $baseUrl = self::cleanUrl($baseUrl);
        $total = (int)($pagination['total'] ?? 0);
        $perPage = max(1, (int)($pagination['per_page'] ?? 10));
        $currentPage = (int)($pagination['current_page'] ?? 1);
        $totalPages = (int)ceil($total / $perPage);
        if ($totalPages < 1) { return ''; }

        $target = (string)($pagination['target'] ?? '');
        $attrs = is_array($pagination['attrs'] ?? null) ? $pagination['attrs'] : [];
        if ($target !== '' && !isset($attrs['hx-target'])) { $attrs['hx-target'] = $target; }
        if (!isset($attrs['hx-push-url'])) { $attrs['hx-push-url'] = 'false'; }

        $renderItem = function($page, $label, $active = false, $disabled = false, $type = 'page') use ($baseUrl, $attrs) {
            $pageUrl = $baseUrl;
            if (strpos($pageUrl, '{page}') !== false) {
                $pageUrl = str_replace('{page}', (string)$page, $pageUrl);
            } else {
                $sep = (strpos($pageUrl, '?') === false) ? '?' : '&';
                $pageUrl .= $sep . 'page=' . $page;
            }

            $itemAttrs = $attrs;
            if (!isset($itemAttrs['hx-push-url'])) { $itemAttrs['hx-push-url'] = 'false'; }
            $itemAttrs['data-slot'] = 'pagination-link';
            if ($active) {
                $itemAttrs['aria-current'] = 'page';
                $itemAttrs['data-active'] = 'true';
            } elseif ($type === 'page') {
                $itemAttrs['data-active'] = 'false';
            }

            if ($type === 'prev') {
                $itemAttrs['aria-label'] = \mh_lng('UI_PAGINATION_GO_TO_PREVIOUS_PAGE', 'Go to previous page');
            } elseif ($type === 'next') {
                $itemAttrs['aria-label'] = \mh_lng('UI_PAGINATION_GO_TO_NEXT_PAGE', 'Go to next page');
            }

            if ($disabled) {
                $itemAttrs['aria-disabled'] = 'true';
                $itemAttrs['role'] = 'link';
            } else {
                $itemAttrs['hx-get'] = $pageUrl;
            }

            $btnOpts = [
                'variant' => $active ? 'outline' : 'ghost',
                'size' => 'md',
                'attrs' => $itemAttrs,
                'class' => 'disabled:opacity-50 disabled:pointer-events-none'
            ];

            if ($type === 'page') {
                $btnOpts['class'] .= ' size-9! p-0! justify-center';
            } else {
                $btnOpts['icon'] = ($type === 'prev' ? Base::SVG_CHEVRON_LEFT : Base::SVG_CHEVRON_RIGHT);
                $btnOpts['iconPosition'] = ($type === 'prev' ? 'front' : 'back');
                $btnOpts['class'] .= ' [&>span]:hidden sm:[&>span]:block';
                if ($type === 'prev') {
                    $btnOpts['class'] .= ' sm:pl-2.5';
                } else {
                    $btnOpts['class'] .= ' sm:pr-2.5';
                }
            }

            return '<li data-slot="pagination-item">' . Button::render($label, $btnOpts) . '</li>';
        };

        $renderEllipsis = function() {
            return '
            <li data-slot="pagination-item">
                <span aria-hidden="true" data-slot="pagination-ellipsis" class="flex size-9 items-center justify-center">
' . Base::SVG_ELLIPSIS_HORIZONTAL .'                    
<span class="sr-only">' . \mh_lng('UI_PAGINATION_MORE_PAGES', 'More pages') . '</span>
                </span>
            </li>';
        };

        $renderPerPageDropdown = function() use ($perPage, $baseUrl, $attrs, $listId, $target) {
            $options = [5, 10, 25, 50, 100];
            if (!in_array($perPage, $options)) {
                $options[] = $perPage;
                sort($options);
            }

            $items = [];
            foreach ($options as $opt) {
                $itemAttrs = $attrs;
                $url = $baseUrl;

                // Reset page to 1
                if (strpos($url, '{page}') !== false) {
                    $url = str_replace('{page}', '1', $url);
                } else {
                    if (preg_match('/([?&])page=[^&]*/', $url)) {
                        $url = preg_replace('/([?&])page=[^&]*/', '$1page=1', $url);
                    } else {
                        $sep = (strpos($url, '?') === false) ? '?' : '&';
                        $url .= $sep . 'page=1';
                    }
                }

                if ($listId !== '') {
                    $itemAttrs['hx-post'] = \Components\Router::url('save_ui_list_per_page');
                    // Reset page to 1 and use HX-Location for targeted refresh
                    $itemAttrs['hx-vals'] = json_encode([
                        'list_id' => $listId,
                        'per_page' => $opt,
//                        'redirect_url' => $url,
//                        'redirect_target' => $target
                    ]);
                    if ($target !== '') {
                        $itemAttrs['hx-target'] = $target;
                        $itemAttrs['hx-swap'] = 'outerHTML';
                    } else {
                        $itemAttrs['hx-swap'] = 'none';
                    }
                } else {
                    // Set per_page
                    if (strpos($url, '{per_page}') !== false) {
                        $url = str_replace('{per_page}', (string)$opt, $url);
                    } else {
                        if (preg_match('/([?&])per_page=[^&]*/', $url)) {
                            $url = preg_replace('/([?&])per_page=[^&]*/', '$1per_page=' . $opt, $url);
                        } else {
                            $sep = (strpos($url, '?') === false) ? '?' : '&';
                            $url .= $sep . 'per_page=' . $opt;
                        }
                    }
                    $itemAttrs['hx-get'] = $url;
                }

                $items[] = [
                    'label' => (string)$opt,
                    'attrs' => $itemAttrs,
                    'icon' => ($opt === $perPage) ? Base::SVG_MICRO_CHECK : '',
                ];
            }

            $triggerHtml = Button::render($perPage . ' / ' . \mh_lng('UI_PAGE', 'page'), [
                'variant' => 'secondary',
                'size' => 'md',
                'icon' => Base::SVG_CHEVRON_DOWN,
                'iconPosition' => 'back',
                'class' => 'font-normal!',
            ]);

            return Dropdown::render([
                'trigger_tag' => 'div',
                'trigger_html' => $triggerHtml,
                'items' => $items,
                'anchor' => 'bottom start'
            ]);
        };

        ob_start();

        $containerClass = 'mt-4 flex items-center justify-between px-4 py-3 ';
        if ($layout === 'clean') {
            $containerClass .= 'bg-divider/20 border border-divider/20 rounded-lg';
        } else {
            $containerClass .= 'bg-elevated backdrop-blur-sm backdrop-filter dark:bg-slate-900/75 rounded-lg border border-divider/10';
        }

        $prevLabel = \mh_lng('UI_PREVIOUS', 'Previous');
        $nextLabel = \mh_lng('UI_NEXT', 'Next');
        ?>
        <div class="<?= $containerClass ?>">
            <div class="flex flex-1 items-center justify-between gap-4">
                <div class="hidden sm:flex items-center gap-4">
                    <p class="text-sm text-gray-700 dark:text-gray-400">
                        <?php echo sprintf(\mh_lng('UI_PAGINATION_SHOWING_X_TO_Y_OF_Z', 'Showing <span class="font-medium">%d</span> to <span class="font-medium">%d</span> of <span class="font-medium">%d</span> results'),
                            ($currentPage - 1) * $perPage + 1,
                            min($currentPage * $perPage, $total),
                            $total
                        ); ?>
                    </p>
                    <?= $renderPerPageDropdown(); ?>
                </div>
                <nav aria-label="Pagination">
                    <ul data-slot="pagination-content" class="flex flex-row items-center gap-1">
                        <?= $renderItem($currentPage - 1, $prevLabel, false, $currentPage <= 1, 'prev'); ?>

                        <?php
                        $range = 2;
                        $dotsBefore = false;
                        $dotsAfter = false;
                        for ($i = 1; $i <= $totalPages; $i++):
                            if ($i == 1 || $i == $totalPages || ($i >= $currentPage - $range && $i <= $currentPage + $range)):
                                echo $renderItem($i, (string)$i, $i == $currentPage);
                            elseif ($i < $currentPage - $range && !$dotsBefore):
                                echo $renderEllipsis();
                                $dotsBefore = true;
                            elseif ($i > $currentPage + $range && !$dotsAfter):
                                echo $renderEllipsis();
                                $dotsAfter = true;
                            endif;
                        endfor;
                        ?>

                        <?= $renderItem($currentPage + 1, $nextLabel, false, $currentPage >= $totalPages, 'next'); ?>
                    </ul>
                </nav>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Clean a URL by removing redundant partial parameters
     * This ensures that pushed URLs are reloadable as full pages
     * while htmx still gets fragments via the HX-Request header.
     *
     * @param string $url
     * @return string
     */
    private static function cleanUrl(string $url): string
    {
        // Remove partial=1 or partial=0
        $url = preg_replace('/([?&])partial=[^&]*/', '', $url);
        // Normalize separators
        $url = str_replace(['?&', '&&'], ['?', '&'], $url);
        // Trim any trailing ? or &
        $url = rtrim($url, '?&');
        return $url;
    }

    /**
     * Renders the identity block (icon + label + sublabel).
     *
     * @param array $it Item data
     * @param array $options ResourceList options (for id_key, label_key, sublabel_key, status_key, icon, dot_color_key)
     * @return string
     */
    public static function renderIdentity(array $it, array $options): string
    {
        // Resolve label
        $labelKey = $options['label_key'] ?? $options['name_key'] ?? 'name';
        $label = \Components\Base::h((string)($it[$labelKey] ?? ''), false);

        // Resolve sublabel
        $sublabelKey = $options['sublabel_key'] ?? null;
        $sublabel = null;
        if ($sublabelKey) {
            $sublabel = \Components\Base::h((string)($it[$sublabelKey] ?? ''), false);
        }

        // Resolve dot color
        $dotColorKey = $options['dot_color_key'] ?? 'dot_color';
        $dotToken = $it[$dotColorKey] ?? $options['dot_color'] ?? null;
        $dotHtml = '';
        if ($dotToken) {
            $dotBg = \Components\Support\Palette::dotBg((string)$dotToken);
            $dotHtml = '<span class="h-1.5 w-1.5 rounded-full shrink-0 ' . \Components\Base::h($dotBg) . '"></span>';
        }

        $iconHtml = self::computeIconHtml($options, $it);

        ob_start();
        ?>
        <div class="flex items-center gap-3">
            <?php echo $iconHtml; ?>
            <div class="min-w-0 flex-1">
                <div class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate flex items-center gap-2">
                    <?php echo $dotHtml; ?>
                    <span class="truncate"><?php echo $label; ?></span>
                </div>
                <div class="text-xs text-gray-500 dark:text-gray-400"><?php echo $sublabel; ?></div>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

    private static function renderItemRow(
        array $it,
        array $options,
        string $idKey,
        string $labelKey,
        string $statusKey,
        string $panelId,
        string $panelTargetId,
        string $menuModelClass,
        string $menuSelfRoute,
        string $menuLabel,
        array $rowActions,
        array $columns = [],
        bool $rowActionsAsMenu = false,
        string $menuTrigger = 'default',
        string $rowPadding = 'py-4',
        string $firstColPadding = 'pl-4 sm:pl-0',
        string $lastColPadding = 'pr-4 sm:pr-0'
    ): string {
        $id = (int)($it[$idKey] ?? 0);

        // Find primary action if defined
        $primaryActionKey = (string)($options['primary_action'] ?? '');
        $primaryAction = null;
        if ($primaryActionKey !== '') {
            foreach ($rowActions as $action) {
                if (($action['key'] ?? '') === $primaryActionKey || ($action['label'] ?? '') === $primaryActionKey) {
                    $primaryAction = $action;
                    break;
                }
            }
        }

        // Resolve model-defined CRUD menu items for this row
        $menuItems = self::resolveMenuItems($menuModelClass, $menuSelfRoute, $id, $it, $panelId, $panelTargetId);

        if ($rowActionsAsMenu && !empty($rowActions)) {
            if (!empty($menuItems)) {
                $menuItems[] = ['divider' => true];
            }
            foreach ($rowActions as $action) {
                $aAttrs = $action['attrs'] ?? [];
                if (isset($action['url'])) {
                    $url = (string)$action['url'];
                    $repl = (string)$id;
                    $url = str_replace('{id}', $repl, $url);
                    $url = str_ireplace('%7Bid%7D', $repl, $url);
                    if ($url !== '') {
                        if (($aAttrs['tag'] ?? '') === 'a') {
                            $action['href'] = $url;
                        } else {
                            $aAttrs['hx-get'] = self::addDrawerParam($url, $panelId);
                        }
                    }
                }
                foreach ($aAttrs as $k => $v) {
                    if (is_string($v)) {
                        $v = str_replace('{id}', (string)$id, $v);
                        $aAttrs[$k] = str_ireplace('%7Bid%7D', (string)$id, $v);
                    }
                }
                $aAttrs = self::normalizePanelAttrs($aAttrs, $panelId, $panelTargetId);

                $menuItems[] = [
                    'label' => (string)($action['label'] ?? 'Action'),
                    'href' => $action['href'] ?? null,
                    'attrs' => $aAttrs,
                    'icon' => $action['icon'] ?? null,
                    'end_icon' => $action['end_icon'] ?? null,
                ];
            }
        }

        ob_start();
        $hideNameColumn = (bool)($options['hide_name_column'] ?? false);
        $layout = (string)($options['layout'] ?? 'default');
        $trClass = ($layout === 'clean' || $primaryAction) ? 'group' : '';
        $getTdHoverClass = function($isFirst, $isLast) use ($layout, $primaryAction) {
            if ($layout === 'clean') {
                $cls = "border-y border-transparent group-hover:bg-divider/20 dark:group-hover:bg-white/5 transition-colors group-hover:border-divider/10";
                if ($isFirst) $cls .= " border-l border-transparent rounded-l-lg";
                if ($isLast) $cls .= " border-r border-transparent rounded-r-lg";
                return $cls;
            }
            if ($primaryAction) {
                return 'group-hover:bg-gray-50 dark:group-hover:bg-white/5 transition-colors';
            }
            return '';
        };
        $isFirst = true;
        ?>
        <?php
        $trAttrs = [];
        if ($primaryAction) {
            $url = (string)($primaryAction['url'] ?? '');
            $repl = (string)$id;
            $url = str_replace('{id}', $repl, $url);
            $url = str_ireplace('%7Bid%7D', $repl, $url);
            $trAttrs = $primaryAction['attrs'] ?? [];
            foreach ($trAttrs as $k => $v) {
                if (is_string($v)) {
                    $v = str_replace('{id}', $repl, $v);
                    $trAttrs[$k] = str_ireplace('%7Bid%7D', $repl, $v);
                }
            }
            if ($url !== '') {
                $trAttrs['hx-get'] = self::addDrawerParam($url, $panelId);
            }
            $trAttrs = self::normalizePanelAttrs($trAttrs, $panelId, $panelTargetId);
            $trClass .= ' cursor-pointer';
        }
        ?>
        <tr class="<?= trim($trClass) ?>" <?= self::renderAttrs($trAttrs) ?>>
            <?php if (!$hideNameColumn):
                $namePaddingClass = "$rowPadding pr-3 $firstColPadding";
                $tdHoverClass = $getTdHoverClass($isFirst, false); $isFirst = false;
                ?>
                <td class="<?= $tdHoverClass ?> <?= $namePaddingClass ?> text-sm font-medium whitespace-nowrap text-gray-900 dark:text-gray-100">
                    <?php echo self::renderIdentity($it, $options); ?>
                </td>
            <?php endif; ?>

            <?php foreach ($columns as $idx => $col):
                $type = $col['type'] ?? 'text';
                $colClass = (string)($col['class'] ?? '');
                $paddingClass = ($isFirst) ? "$rowPadding pr-3 $firstColPadding" : "px-3 $rowPadding";
                $tdHoverClass = $getTdHoverClass($isFirst, false); $isFirst = false;
                $isOptional = !empty($col['optional']);
                $colKey = (string)($col['key'] ?? $idx);
                $align = (string)($col['align'] ?? 'left');
                $alignClass = ($align === 'right') ? 'text-right' : (($align === 'center') ? 'text-center' : 'text-left');

                if ($type === 'identity') {
                    $val = self::renderIdentity($it, $options);
                } else {
                    $val = $it[$col['key'] ?? ''] ?? '';
                    $render = $col['render'] ?? null;
                    if (is_callable($render)) {
                        $val = (string)call_user_func($render, $val, $it);
                    } else {
                        $val = \Components\Base::h((string)$val);
                    }
                }

                $tdClass = "$tdHoverClass $paddingClass text-sm whitespace-nowrap $alignClass " . ($type === 'identity' ? 'text-gray-900 dark:text-gray-100 font-medium' : 'text-gray-500 dark:text-gray-400');
                ?>
                <td class="<?php echo trim($tdClass . ' ' . $colClass); ?>" <?php if ($isOptional): ?> x-show="visibleColumns['<?= $colKey ?>']" <?php endif; ?>>
                    <?php echo $val; ?>
                </td>
            <?php endforeach; ?>

            <?php $tdHoverClass = $getTdHoverClass($isFirst, true); ?>
            <td class="<?= "$tdHoverClass $rowPadding $lastColPadding pl-3" ?> text-right text-sm font-medium whitespace-nowrap">
                <div class="flex items-center justify-end gap-2">
                    <?php if ($primaryAction && ($primaryAction['icon'] ?? '')): ?>
                        <div class="opacity-0 group-hover:opacity-100 transition-opacity flex items-center shrink-0 text-gray-400 dark:text-gray-500 [&_svg]:size-4 px-2">
                            <?= $primaryAction['icon'] ?>
                        </div>
                    <?php endif; ?>
                    <?php if (!empty($menuItems)) {
                        if ($menuTrigger === 'ellipsis') {
                            echo \Components\Dropdown::render([
                                'trigger_tag' => 'div',
                                'trigger_html' => \Components\Button::render('', [
                                    'variant' => 'ghost',
                                    'size' => 'md',
                                    'icon' => \Components\Base::SVG_ELLIPSIS_HORIZONTAL,
                                    'attrs' => [
                                        'aria-label' => $menuLabel ?: \mh_lng('UI_OPTIONS', 'Options'),
                                    ],
                                ]),
                                'trigger_attrs' => [
                                    'class' => 'flex items-center',
                                    'onclick' => $primaryAction ? 'event.stopPropagation();' : null,
                                    'onmousedown' => $primaryAction ? 'event.stopPropagation();' : null,
                                    'data-no-modal' => $primaryAction ? 'true' : null,
                                ],
                                'items' => $menuItems,
                                'anchor' => 'bottom end',
                            ]);
                        } else {
                            $triggerAttrs = [
                                'class' => 'inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20'
                            ];
                            if ($primaryAction) {
                                $triggerAttrs['onclick'] = 'event.stopPropagation();';
                                $triggerAttrs['onmousedown'] = 'event.stopPropagation();';
                                $triggerAttrs['data-no-modal'] = 'true';
                            }
                            echo \Components\Dropdown::render([
                                'label' => $menuLabel,
                                'items' => $menuItems,
                                'trigger_attrs' => $triggerAttrs,
                            ]);
                        }
                    } ?>
                    <?php if (!$rowActionsAsMenu) {
                        echo self::renderRowActions($rowActions, $id, $panelId, $panelTargetId, $primaryAction !== null);
                    } ?>
                </div>
            </td>
        </tr>
        <?php
        return ob_get_clean();
    }
}
