resizablePanels function

BloomNode resizablePanels({
  1. required BloomNode first,
  2. required BloomNode second,
  3. bool vertical = false,
  4. double initialSplit = 0.5,
  5. double minSplit = 0.1,
  6. double maxSplit = 0.9,
  7. bool withHandle = true,
  8. String extraClassName = '',
})

Resizable split-pane layout component.

Supports horizontal and vertical split orientations with interactive drag resizing.

Drag tracking resolves the container's own bounding rect via a conditionally imported browser-only helper (_resizable_dom_web.dart, falling back to a no-op stub off the browser so this file — part of the pure-Dart core library re-exported from bloom_js_native.dart — stays VM-testable and does not pull package:web into server-rendering builds). BloomEvent only carries pointer-relative coordinates (clientX/clientY), not the container's size, so the container needs a stable id to resolve a drag position into a ratio.

Implementation

BloomNode resizablePanels({
  required BloomNode first,
  required BloomNode second,
  bool vertical = false,
  double initialSplit = 0.5,
  double minSplit = 0.1,
  double maxSplit = 0.9,
  bool withHandle = true,
  String extraClassName = '',
}) {
  final split = signal<double>(initialSplit.clamp(minSplit, maxSplit));
  final isDragging = signal<bool>(false);
  final containerId = 'bloom-resizable-${_resizableInstanceCounter++}';

  void updateFromPointer(BloomEvent e) {
    if (!isDragging.value) return;
    final clientX = e.clientX;
    final clientY = e.clientY;
    if (clientX == null || clientY == null) return;
    final ratio = resolveContainerRatio(
      containerId: containerId,
      clientX: clientX,
      clientY: clientY,
      vertical: vertical,
    );
    if (ratio == null) return;
    split.value = ratio.clamp(minSplit, maxSplit);
  }

  return Live(() {
    final currentSplit = split.value;
    final firstPct = (currentSplit * 100).toStringAsFixed(2);

    return Div(
      attrs: {
        'id': containerId,
        'data-slot': 'resizable-panel-group',
        'aria-orientation': vertical ? 'vertical' : 'horizontal',
      },
      className: cn([
        'relative flex w-full h-full overflow-hidden select-none',
        vertical ? 'flex-col' : 'flex-row',
        extraClassName,
      ]),
      on: {
        'pointermove': updateFromPointer,
        'pointerup': (_) => isDragging.value = false,
        'pointerleave': (_) => isDragging.value = false,
      },
      children: [
        // First Panel
        Div(
          attrs: const {'data-slot': 'resizable-panel'},
          style: 'flex: 0 0 $firstPct%; overflow: auto;',
          className: 'min-w-0 min-h-0',
          children: [first],
        ),
        // Draggable Separator Handle
        El(
          'div',
          attrs: {
            'role': 'separator',
            'tabindex': '0',
            'aria-orientation': vertical ? 'horizontal' : 'vertical',
            'aria-valuenow': firstPct,
            'data-slot': 'resizable-handle',
          },
          className: cn([
            'relative flex items-center justify-center bg-[var(--border)] transition-colors hover:bg-[var(--primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]',
            vertical
                ? 'h-1.5 w-full cursor-row-resize'
                : 'w-1.5 h-full cursor-col-resize',
          ]),
          on: {
            'pointerdown': (BloomEvent e) {
              e.stopPropagation();
              isDragging.value = true;
            },
          },
          children: [
            if (withHandle)
              Div(
                className: cn([
                  'z-10 flex items-center justify-center rounded-sm bg-[var(--card)] border border-[var(--border)] text-[var(--text-muted)] shadow-xs',
                  vertical ? 'h-3 w-6' : 'h-6 w-3',
                ]),
                children: [
                  uiIcon(
                    vertical ? 'grip-horizontal' : 'grip-vertical',
                    className: 'w-2.5 h-2.5',
                  ),
                ],
              ),
          ],
        ),
        // Second Panel
        Div(
          attrs: const {'data-slot': 'resizable-panel'},
          style: 'flex: 1 1 0%; overflow: auto;',
          className: 'min-w-0 min-h-0',
          children: [second],
        ),
      ],
    );
  });
}