commandViewport function

BloomNode commandViewport()

Root viewport component for rendering the global command palette modal overlay.

Mount this once near the root of the application.

Implementation

BloomNode commandViewport() {
  final query = signal<String>('');
  final activeIndex = signal<int>(0);

  return Live(() {
    final config = activeCommandPalette.value;
    if (config == null) return const Fragment(children: []);

    final search = query.value.trim().toLowerCase();
    final filtered = config.items.where((item) {
      if (search.isEmpty) return true;
      return item.label.toLowerCase().contains(search) ||
          (item.group != null && item.group!.toLowerCase().contains(search));
    }).toList();

    // Clamp active index
    if (activeIndex.value >= filtered.length && filtered.isNotEmpty) {
      activeIndex.value = filtered.length - 1;
    } else if (filtered.isEmpty) {
      activeIndex.value = 0;
    }

    // Group items
    final groups = <String, List<CommandItemConfig>>{};
    for (final item in filtered) {
      final grp = item.group ?? '';
      groups.putIfAbsent(grp, () => []).add(item);
    }

    return Div(
      className:
          'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-start justify-center p-4 pt-[15vh]',
      onClick: (_) => closeCommandPalette(),
      children: [
        Div(
          attrs: const {
            'role': 'dialog',
            'aria-modal': 'true',
            'aria-label': 'Command palette',
          },
          className:
              'relative bg-[var(--card)] border border-[var(--border)] rounded-[var(--radius-lg)] '
              'shadow-[var(--shadow-overlay)] max-w-xl w-full flex flex-col overflow-hidden',
          onClick: (BloomEvent e) => e.stopPropagation(),
          children: [
            // Search Input Header
            Div(
              className:
                  'flex items-center gap-2.5 px-3 py-3 border-b border-[var(--border)]',
              children: [
                uiIcon('search', className: 'w-4 h-4 text-[var(--text-muted)] shrink-0'),
                El(
                  'input',
                  attrs: {
                    'type': 'text',
                    'placeholder': config.placeholder,
                    'value': query.value,
                    'autofocus': 'autofocus',
                    'aria-autocomplete': 'list',
                  },
                  className:
                      'w-full bg-transparent text-xs sm:text-sm text-[var(--text)] placeholder-[var(--text-muted)] '
                      'focus:outline-none border-none p-0',
                  on: {
                    'input': (BloomEvent e) {
                      query.value = e.value ?? '';
                      activeIndex.value = 0;
                    },
                    'keydown': (BloomEvent e) {
                      if (e.key == 'Escape') {
                        e.preventDefault();
                        closeCommandPalette();
                      } else if (e.key == 'ArrowDown') {
                        e.preventDefault();
                        if (activeIndex.value < filtered.length - 1) {
                          activeIndex.value++;
                        }
                      } else if (e.key == 'ArrowUp') {
                        e.preventDefault();
                        if (activeIndex.value > 0) {
                          activeIndex.value--;
                        }
                      } else if (e.key == 'Enter') {
                        e.preventDefault();
                        if (filtered.isNotEmpty &&
                            activeIndex.value < filtered.length) {
                          final selectedItem = filtered[activeIndex.value];
                          if (!selectedItem.disabled) {
                            closeCommandPalette();
                            selectedItem.onSelect();
                          }
                        }
                      }
                    },
                  },
                ),
                El(
                  'button',
                  attrs: const {'type': 'button', 'aria-label': 'Close'},
                  className:
                      'text-[var(--text-muted)] hover:text-[var(--text)] p-1 rounded-[var(--radius-sm)] cursor-pointer',
                  onClick: (_) => closeCommandPalette(),
                  children: [uiIcon('x', className: 'w-3.5 h-3.5')],
                ),
              ],
            ),
            // Results list
            Div(
              className: 'max-h-72 overflow-y-auto p-1.5 flex flex-col gap-1',
              children: [
                if (filtered.isEmpty)
                  Div(
                    className:
                        'py-8 text-center text-xs text-[var(--text-muted)]',
                    text: 'No commands found.',
                  )
                else
                  ...groups.entries.expand((entry) {
                    final groupName = entry.key;
                    final groupItems = entry.value;

                    return [
                      if (groupName.isNotEmpty)
                        Div(
                          className:
                              'px-2 pt-2 pb-1 text-[10px] font-semibold tracking-wider uppercase text-[var(--text-muted)]',
                          text: groupName,
                        ),
                      ...groupItems.map((item) {
                        final overallIndex = filtered.indexOf(item);
                        final isSelected = overallIndex == activeIndex.value;

                        return El(
                          'button',
                          attrs: {
                            'type': 'button',
                            'role': 'option',
                            'aria-selected': isSelected ? 'true' : 'false',
                          },
                          className: cn([
                            'w-full flex items-center justify-between px-2.5 py-2 rounded-[var(--radius-sm)] text-xs text-left transition-colors cursor-pointer select-none',
                            isSelected
                                ? 'bg-[var(--bg-muted)] text-[var(--text)] font-medium'
                                : 'text-[var(--text)] hover:bg-[var(--bg-muted)]/50',
                            item.disabled
                                ? 'opacity-50 cursor-not-allowed pointer-events-none'
                                : '',
                          ]),
                          onClick: (_) {
                            if (!item.disabled) {
                              closeCommandPalette();
                              item.onSelect();
                            }
                          },
                          children: [
                            Div(
                              className: 'flex items-center gap-2',
                              children: [
                                if (item.icon != null)
                                  uiIcon(item.icon!, className: 'w-3.5 h-3.5 shrink-0 text-[var(--text-muted)]'),
                                Span(text: item.label),
                              ],
                            ),
                            if (item.shortcut != null)
                              Span(
                                className:
                                    'text-[10px] font-mono text-[var(--text-muted)] bg-[var(--bg-soft)] border border-[var(--border)] px-1.5 py-0.5 rounded',
                                text: item.shortcut!,
                              ),
                          ],
                        );
                      }),
                    ];
                  }),
              ],
            ),
          ],
        ),
      ],
    );
  });
}