G Gentelella v4.0.0

⌘K Command Palette

Press ⌘K (or Ctrl+K) anywhere in Gentelella to open a fuzzy-search modal across all 58 pages plus inline actions. Auto-built from the NAV array. No external library — the matcher is a small subsequence + word-boundary scorer.

Last updated May 22, 2026

The command palette is a global keyboard shortcut — ⌘K on macOS, Ctrl+K on Windows/Linux — that opens a fuzzy-search modal over every page in NAV plus a curated list of inline actions.

It’s also wired into the topbar search box: clicking or focusing the search input opens the palette instead of typing inline. That makes the search field a discoverable affordance for users who don’t try keyboard shortcuts.

The implementation lives in src/v4/command-palette.js — ~250 lines, no external dependencies.

What’s in it

Two kinds of items:

Pages — built from the NAV array in shell-render.js:

NAV.forEach((group) => {
  group.items.forEach((it) => {
    out.push({
      kind:     'page',
      label:    it.text,
      section:  group.label,
      href:     it.href,
      keywords: `${it.text} ${group.label} ${it.key}`.toLowerCase()
    });
  });
});

A nav item like { key: 'kanban', href: 'kanban.html', text: 'Kanban' } in the Apps group becomes a palette result with the label “Kanban”, section tag “Apps”, and search keywords “kanban apps kanban”. Submenu items work too — the palette flattens nested children into individual results.

Actions — a hand-curated list of inline commands that aren’t tied to a page:

const actions = [
  { label: 'Toggle theme',     keywords: 'theme dark light mode toggle', action: toggleTheme },
  { label: 'Open profile',     keywords: 'profile account user me',      action: () => { window.location.href = 'profile.html'; } },
  { label: 'Open settings',    keywords: 'settings preferences config',  action: () => { window.location.href = 'settings.html'; } },
  { label: 'Theme generator',  keywords: 'theme color customize brand',  action: () => { window.location.href = 'theme.html'; } },
  { label: 'Help & support',   keywords: 'help faq support docs',        action: () => { window.location.href = 'faq.html'; } },
  { label: 'Sign out',         keywords: 'sign out logout exit',         action: () => showModal({ /* confirm dialog */ }) }
];

To add a new action, append to the actions array in command-palette.js with an action function. The handler runs when the user activates the row (click or Enter).

The fuzzy matcher

No external library. The score function is a subsequence matcher with two bonuses:

function score(query, target) {
  if (!query) return 0;
  const t = target;
  const q = query;
  let ti = 0, qi = 0, s = 0, lastMatchedAt = -2;

  while (qi < q.length && ti < t.length) {
    if (t[ti] === q[qi]) {
      // Bonus for word boundary
      if (ti === 0 || t[ti - 1] === ' ' || t[ti - 1] === '-' || t[ti - 1] === '_') s -= 6;
      // Bonus for consecutive
      if (lastMatchedAt === ti - 1) s -= 4;
      lastMatchedAt = ti;
      qi += 1;
    } else {
      s += 1;
    }
    ti += 1;
  }
  if (qi < q.length) return Infinity;
  s += (t.length - q.length) * 0.1;  // Prefer shorter targets that match
  return s;
}

Lower scores win. The two bonuses are what makes it feel right:

  • Word-boundary bonus (-6) means typing kb matches “Kanban Board” much better than “Markdown”.
  • Consecutive bonus (-4) means kan ranks “Kanban” above “Kalendar” (typo-tolerant but still prefers exact substrings).

The matcher is “good enough for ~50 items” — fast, no allocations beyond the score itself. If you scale the palette to thousands of items (a fuzzy file finder, say), swap in a real library like fzf-for-js.

Open / close

The global keybinding is installed once via initCommandPalette(), called from main-v4.js:

export function initCommandPalette() {
  if (initCommandPalette._wired) return;
  initCommandPalette._wired = true;

  document.addEventListener('keydown', (e) => {
    const isK = e.key === 'k' || e.key === 'K';
    if (isK && (e.metaKey || e.ctrlKey)) {
      e.preventDefault();
      host ? close() : open();
    }
  });

  // Repurpose the topbar search box
  const search = document.querySelector('.search-box input');
  if (search) {
    const opener = (e) => { e.preventDefault(); search.blur(); open(); };
    search.addEventListener('focus', opener);
    search.addEventListener('click', opener);
    search.setAttribute('readonly', '');
    search.setAttribute('aria-label', 'Open command palette');
  }
}

The _wired flag makes the function idempotent — calling it twice (e.g. from two scripts on the same page) doesn’t double-bind.

The palette DOM is created on first open and torn down on close — no persistent host element in the body, which means it doesn’t interfere with other modals or layout when closed.

Keyboard inside the palette

Once open:

KeyAction
TypeFilter — scores all items, re-renders top results
↑ / ↓Move active selection
EnterActivate — navigate to the page or run the action
EscClose
Click backdropClose

The active item gets aria-selected="true" and a visible highlight; it’s the one Enter would activate. Hover overrides selection — mouseenter on a row updates activeIndex, so mouse + keyboard mix smoothly.

Adding a custom action

A common case: integrate the palette with your own app’s commands.

import { openCommandPalette, closeCommandPalette } from '/src/v4/command-palette.js';

// Bind to a different shortcut
document.addEventListener('keydown', (e) => {
  if (e.key === 'p' && (e.metaKey || e.ctrlKey)) {
    e.preventDefault();
    openCommandPalette();
  }
});

The exported openCommandPalette / closeCommandPalette are aliases so you can drive the palette programmatically (e.g. open it after a successful action’s confirmation).

For a more invasive change — adding actions from outside the file — edit buildItems() in command-palette.js directly. The actions array isn’t exposed for runtime extension because the palette is a static UI feature, not a plugin host.

Why the topbar search box opens the palette

Two reasons:

  • Most admin templates have a “Search” box in the topbar that does nothing. Wiring it to the palette makes the affordance functional without a separate search infrastructure.
  • Users who don’t try ⌘K still discover the palette. Click “Search…”, get a fuzzy-matching modal — same UX, no learning curve.

The downside: there’s no separate “global search across documents” feature in the template. If your app needs that (search across emails, projects, customers, etc.), you’d build it as a separate page or backend-driven endpoint — the palette is intentionally limited to navigation + commands.

See also

  • Architecture — how main-v4.js boots initCommandPalette() alongside other shell behaviors
  • Adding a page — every new NAV entry automatically becomes a palette result