Contents

Vanilla JS — HR Employee Directory

Pattern A drop-in. Add CtrlK to any existing page with zero build step. Shows column profiles, density control, view sharing.

Vanilla JS HTML Table No build tools 25 columns 200 rows

Include the runtime

Add the single script tag. CtrlK auto-initializes, scans the DOM, and registers all buttons and links as commands.

index.htmlHTML
<!-- Drop this before </body>. Nothing else needed. -->
<script src="ctrlk.runtime.js"></script>

<!-- Ctrl+K now opens command palette automatically. -->
<!-- All <button> elements are auto-discovered as commands. -->
Zero-config result: Command palette opens with Ctrl+K. Every button on the page becomes a searchable command. Density control (Ctrl+D) is active. This is Pattern A.

Register custom commands

Go beyond auto-discovery. Register named commands with shortcuts, categories, and execute functions.

app.jsJavaScript
// Register a command — appears in Ctrl+K palette
ctrlk.commands.register({
  id: 'view.engineering',
  title: 'Show Engineering Department',
  category: 'Department Views',
  icon: '⚙',
  shortcut: 'Alt+1',
  execute: () => filterByDept('Engineering'),
});

// Bind the shortcut
ctrlk.keys.bind('Alt+1', 'view.engineering');

Add column profiles (named column sets)

Define which columns to show for different tasks. Users switch profiles from the palette or via shortcuts.

app.jsJavaScript
// Define column profiles
const PROFILES = {
  people: ['empId','name','department','title','status','location'],
  compensation: ['empId','name','band','salary','tenure'],
  contact: ['empId','name','email','phone','deskId'],
};

function showColumns(profileName) {
  const cols = PROFILES[profileName];
  // Hide all columns, show only these
  allColumns.forEach(col => {
    col.visible = cols.includes(col.id);
  });
  renderGrid();
}

ctrlk.commands.register({
  id: 'cols.people',
  title: 'Columns: People Overview',
  category: 'Column Profiles',
  icon: '👤',
  execute: () => showColumns('people'),
});

Add column search (Ctrl+G)

Build a column search overlay. On selection, scroll the grid horizontally to the column and highlight it.

column-search.jsJavaScript
// Get all columns for the search index
const searchableColumns = COLUMNS.map((col, i) => ({
  id: col.id,
  name: col.headerName,
  index: i,
}));

// Search function — filter by query
function searchColumns(query) {
  const q = query.toLowerCase();
  return searchableColumns.filter(c =>
    c.name.toLowerCase().includes(q)
  );
}

// Jump to column — scroll + highlight
function jumpToColumn(col) {
  const gridWrap = document.querySelector('.grid-wrap');
  const header = document.querySelectorAll('th')[col.index];
  // Scroll horizontally to center the column
  const scrollLeft = header.offsetLeft - gridWrap.offsetWidth / 2;
  gridWrap.scrollTo({ left: scrollLeft, behavior: 'smooth' });
  // Flash the column
  header.classList.add('col-highlight');
  setTimeout(() => header.classList.remove('col-highlight'), 1500);
}

// Register Ctrl+G shortcut
ctrlk.keys.bind('Ctrl+G', 'ctrlk.colsearch');

Add view sharing

Capture the current view state (filters, columns, sort) and encode it into a shareable URL.

share.jsJavaScript
function shareCurrentView() {
  const state = {
    filter: currentFilter,
    sort: sortColumn,
    columns: visibleColumns,
    selected: [...selectedRows],
  };
  const encoded = btoa(JSON.stringify(state));
  const link = location.href.split('#')[0] + '#ctrlk=' + encoded;
  navigator.clipboard.writeText(link);
}

// On page load — check for shared view in URL
if (location.hash.startsWith('#ctrlk=')) {
  const state = JSON.parse(atob(location.hash.slice(7)));
  applyViewState(state);
}
What the user gets
Ctrl+K — Command palette with all commands Ctrl+G — Column search with jump-to Ctrl+D — Density cycling (compact/spacious) Alt+1-5 — Quick department views Ctrl+Shift+S — Share view as URL Right-click headers to bookmark columns Ctrl+←→ — Jump between bookmarks Column profiles from palette

React — Support Ticket Queue

Pattern C deep integration using React hooks. Shows session tracking with batch review progress, selection with batch actions.

React 18 Hooks (useState, useMemo) Custom row components 120 tickets

Install and wrap with Provider

terminalShell
npm install ctrlk
App.jsxReact
import { CtrlKProvider } from '@ctrlk/react';
import ctrlk from 'ctrlk';

function App() {
  return (
    <CtrlKProvider instance={ctrlk}>
      <TicketQueue />
    </CtrlKProvider>
  );
}

Register commands with useCtrlkCommand

Commands auto-unregister on component unmount. The hook wraps the latest closure, so execute always has fresh state.

TicketQueue.jsxReact
import { useCtrlkCommand } from '@ctrlk/react';

function TicketQueue() {
  const [view, setView] = useState('all');

  // This command auto-registers and auto-unregisters
  useCtrlkCommand({
    id: 'view.open',
    title: 'Show Open Tickets',
    category: 'Views',
    icon: '🟢',
    execute: () => setView('open'),
  }, []);  // empty deps = register once
}

Add session tracking (batch review)

Track which tickets have been reviewed. Show progress. Navigate to next unreviewed with Alt+N.

TicketQueue.jsxReact
const [reviewed, setReviewed] = useState(new Set());

// Toggle reviewed status (Alt+R)
function toggleReviewed(ticketId) {
  setReviewed(prev => {
    const next = new Set(prev);
    next.has(ticketId) ? next.delete(ticketId) : next.add(ticketId);
    return next;
  });
}

// Next unreviewed (Alt+N)
function nextUnreviewed() {
  const next = filteredTickets.find(t => !reviewed.has(t.id));
  if (next) {
    setFocused(next.id);
    document.querySelector(`[data-ticket="${next.id}"]`)
      ?.scrollIntoView({ behavior: 'smooth' });
  }
}

// Progress bar data
const progress = {
  done: filteredTickets.filter(t => reviewed.has(t.id)).length,
  total: filteredTickets.length,
};

// Register as commands
useCtrlkCommand({ id:'review.toggle', shortcut:'Alt+R', execute: toggleReviewed });
useCtrlkCommand({ id:'review.next', shortcut:'Alt+N', execute: nextUnreviewed });

Render with visual indicators

TicketRow.jsxReact
// Green left border = reviewed
// Blue background = selected
<div
  className={`ticket-row
    ${selected.has(ticket.id) ? 'selected' : ''}
    ${reviewed.has(ticket.id) ? 'reviewed' : ''}`}
  data-ticket={ticket.id}
  onDoubleClick={() => toggleReviewed(ticket.id)}
>
  ...
</div>
What the user gets
Alt+R — Toggle reviewed on focused ticket Alt+N — Jump to next unreviewed Progress bar: "7/45 reviewed (16%)" Double-click to mark reviewed (green border) Batch selection with checkbox + actions Smart views in sidebar

Vue 3 — Patient Record Detail Page

Field-level CtrlK integration. Shows jump-to-field, empty field navigation, dirty tracking, field pinning, and completeness.

Vue 3 Composition API v-model binding 52 fields 7 sections

Define field schema with sections

Structure fields into logical sections. Each field has a label, section, and editability flag.

patient-schema.jsJavaScript
const SECTIONS = [
  {
    id: 'vitals',
    title: 'Vitals',
    icon: '❤️',
    fields: ['bloodPressure', 'heartRate', 'temperature',
             'weight', 'height', 'bmi', 'oxygenSat'],
  },
  // ... more sections
];

const LABELS = {
  bloodPressure: 'Blood Pressure',
  heartRate: 'Heart Rate (bpm)',
  // ... one label per field
};

const READONLY = new Set(['mrn', 'ssn', 'bmi']);
const REQUIRED = new Set(['firstName', 'bloodPressure']);

Build the field search (Ctrl+G for fields)

Unlike grid column search, field search operates on form fields. Shows section, empty/dirty status, and pin state.

field-search.jsJavaScript
function searchFields(query) {
  const q = query.toLowerCase();
  const results = [];

  for (const section of SECTIONS) {
    for (const fieldId of section.fields) {
      const label = LABELS[fieldId];
      if (label.toLowerCase().includes(q)) {
        results.push({
          field: fieldId,
          label: label,
          section: section.title,
          empty: isEmpty(fieldId),
          dirty: isDirty(fieldId),
          pinned: pinnedFields.has(fieldId),
        });
      }
    }
  }
  return results;
}

// Jump to field — scroll, highlight, focus input
function scrollToField(fieldId) {
  const el = document.querySelector(`[data-field="${fieldId}"]`);
  el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
  const input = el?.querySelector('input');
  input?.focus();
}

Add dirty tracking and pre-save diff

PatientRecord.vueVue 3
// Track original values for dirty comparison
const data = ref({...PATIENT});
const original = ref({...PATIENT});

// Computed: which fields changed?
const dirtyFields = computed(() => {
  return Object.keys(data.value)
    .filter(k => data.value[k] !== original.value[k])
    .map(k => ({
      field: k,
      label: LABELS[k],
      old: original.value[k],
      new: data.value[k],
    }));
});

// Show diff modal with Ctrl+Shift+D
// Yellow border on dirty inputs via :class binding
<input :class="{dirty: isDirty(fieldId)}" v-model="data[fieldId]" />

Add field pinning for cross-record comparison

PatientRecord.vueVue 3
const pinnedFields = ref(new Set());

function togglePin(fieldId) {
  const p = new Set(pinnedFields.value);
  p.has(fieldId) ? p.delete(fieldId) : p.add(fieldId);
  pinnedFields.value = p;
}

<!-- Pinned bar at top of page -->
<div class="pinned-bar" v-if="pinnedFields.size > 0">
  <div v-for="f in pinnedFields" class="pinned-item">
    {{ LABELS[f] }}: {{ data[f] }}
  </div>
</div>
Cross-record use: When navigating to the next patient, pinned field values stay visible at the top — showing the previous patient's blood pressure next to the current one without memorizing.
What the user gets
Ctrl+G — Jump to any field by name Alt+N — Navigate to next empty field Ctrl+Shift+D — Pre-save diff (old → new) Yellow highlight on dirty fields Dashed border on empty fields 📌 Pin fields for cross-record viewing Completeness: "40/52 fields (77%)" Section-level fill counts in sidebar

AG Grid — Inventory Management

CtrlK + AG Grid Community. Shows the GridAdapter in action — view state capture/restore, column search via grid API, density affecting row height.

AG Grid Community v31 GridAdapter pattern 21 columns 500 rows

Initialize AG Grid and connect CtrlK

inventory.jsJavaScript
import { AgGridAdapter } from '@ctrlk/ag-grid';

// After AG Grid is ready
const gridOptions = {
  columnDefs: columnDefs,
  rowData: products,
  onGridReady: (params) => {
    const adapter = new AgGridAdapter(params.api, {
      rowIdField: 'sku',
    });

    // Wire CtrlK to AG Grid
    ctrlk.connectGrid(adapter);
    // This single call connects:
    //   → views (capture/restore column state)
    //   → selection (sync selected rows)
    //   → columnNav (search via AG Grid API)
  },
};
One call: ctrlk.connectGrid(adapter) wires views, selection, and column navigation to the grid simultaneously. No per-module configuration needed.

Column search uses AG Grid API directly

The column search overlay reads from gridApi.getColumnState() and jumps using gridApi.ensureColumnVisible().

column-search.jsJavaScript
// Column search reads from AG Grid API
function getColumns() {
  return gridApi.getColumnState().map(cs => ({
    colId: cs.colId,
    name: columnDefs.find(cd => cd.field === cs.colId)?.headerName,
    hidden: cs.hide,
  }));
}

// Jump uses AG Grid's native scroll + flash
function jumpToColumn(colId) {
  // Make visible if hidden
  if (col.hidden) {
    gridApi.applyColumnState({
      state: [{ colId: colId, hide: false }]
    });
  }
  // Scroll to it
  gridApi.ensureColumnVisible(colId);
  // Flash highlight
  gridApi.flashCells({ columns: [colId] });
}

Density changes AG Grid row height

density.jsJavaScript
const densityConfig = {
  compact:     { rowHeight: 28, headerHeight: 30 },
  comfortable: { rowHeight: 36, headerHeight: 36 },
  spacious:    { rowHeight: 48, headerHeight: 42 },
};

function setDensity(level) {
  const cfg = densityConfig[level];
  // AG Grid API for row height
  gridApi.updateGridOptions({
    rowHeight: cfg.rowHeight,
    headerHeight: cfg.headerHeight,
  });
  gridApi.resetRowHeights();
}

Save and restore views via AG Grid state API

views.jsJavaScript
// Save: capture AG Grid's full state
function saveView(name) {
  const state = {
    columns: gridApi.getColumnState(),
    filters: gridApi.getFilterModel(),
    sort: gridApi.getColumnState()
      .filter(c => c.sort)
      .map(c => ({ colId: c.colId, sort: c.sort })),
  };
  localStorage.setItem(`view-${name}`, JSON.stringify(state));
}

// Restore: apply everything back
function loadView(name) {
  const state = JSON.parse(localStorage.getItem(`view-${name}`));
  gridApi.applyColumnState({ state: state.columns, applyOrder: true });
  gridApi.setFilterModel(state.filters);
}

DevExtreme — Inventory Management

Same app as 4A, different grid library. Shows the adapter abstraction — same CtrlK commands, different API calls underneath.

DevExtreme DataGrid GridAdapter pattern Same 21 columns Same 500 rows

The key difference: API translation

Every CtrlK operation maps to a different DevExtreme API call. The user experience is identical.

CtrlK OperationAG Grid APIDevExtreme API
Get column stategridApi.getColumnState()dxGrid.state()
Restore stategridApi.applyColumnState()dxGrid.state(saved)
Show/hide columnapplyColumnState({hide})columnOption(field,'visible')
Scroll to columnensureColumnVisible()getScrollable().scrollToElement()
Flash cellsflashCells({columns})CSS animation (manual)
Set filtersetFilterModel()filter(['field','=','val'])
Clear filterssetFilterModel(null)clearFilter()
Select allselectAllFiltered()selectAll()
Export CSVexportDataAsCsv()exportToExcel()
Row heightupdateGridOptions({rowHeight})CSS padding on cells
This is why the adapter layer exists. The CtrlK command says "save view." The adapter translates that to the grid library's specific API. Switch from AG Grid to DevExtreme? Change the adapter. All commands, shortcuts, and user workflows remain identical.

DevExtreme-specific view save/restore

dx-views.jsJavaScript
// DevExtreme has a built-in state() method
// It captures EVERYTHING — columns, filters, sort, grouping

function saveView(name) {
  const state = dxGrid.state();  // One call. Done.
  localStorage.setItem(`view-${name}`, JSON.stringify(state));
}

function loadView(name) {
  const state = JSON.parse(localStorage.getItem(`view-${name}`));
  dxGrid.state(state);  // One call. Everything restored.
}

// Column visibility toggle — different API than AG Grid
function showOnlyColumns(fieldNames) {
  ALL_COLUMNS.forEach(col => {
    dxGrid.columnOption(
      col.dataField,
      'visible',
      fieldNames.includes(col.dataField)
    );
  });
}

Keyboard Shortcut Map

These shortcuts work across all demos. They follow Excel conventions where applicable.

ShortcutActionDemosExcel Equivalent
Ctrl+KOpen command paletteAll
Ctrl+GJump to column / fieldAllF5 / Ctrl+G (Go To)
Ctrl+DCycle densityAll
Ctrl+/Show all shortcutsAll
Ctrl+Shift+SShare current view1, 4A, 4B
Alt+NNext empty / unreviewed2, 3
Alt+RToggle reviewed2
Ctrl+Shift+DShow dirty diff3
Ctrl+SSave changes3Ctrl+S
Ctrl+1-4Saved views4A, 4B
Alt+1-5Quick department views1
Ctrl+←→Jump between bookmarks1Ctrl+Arrow (boundaries)
F2Edit cell / fieldCore moduleF2
F6Cycle between zonesCore moduleF6 (panes)
EscapeClose overlay / cancelAllEscape

How it all fits together

architectureDiagram
  User Input (keyboard, mouse)
          │
          ▼
  ┌──────────────────────────┐
  │     CtrlK Runtime          │ ◄── The IOUX layer
  │                          │
  │  CommandRegistry         │     All actions are commands
  │  ShortcutEngine          │     Scope-aware key binding
  │  CommandPalette          │     Ctrl+K search UI
  │  ViewStateManager        │     Named views + auto-save
  │  SelectionModel          │     Cross-page persistent sets
  │  FieldRegistry           │     Field nav + dirty tracking
  │  ColumnNavigator         │     Column search + bookmarks
  │  SessionTracker          │     Batch review progress
  │  MacroEngine             │     Record + replay
  │  HistoryManager          │     Undo/redo + branching
  │  ViewShare               │     Shareable view links
  │  DensityController       │     Compact/comfortable/spacious
  └──────────┬───────────────┘
             │
             ▼
  ┌──────────────────────────┐
  │   Grid Adapter             │ ◄── Translates to grid API
  │                          │
  │  @ctrlk/ag-grid          │     AG Grid v28-31+
  │  @ctrlk/devextreme      │     DevExtreme DataGrid
  │  @ctrlk/kendo            │     Kendo UI Grid
  │  @ctrlk/generic          │     Plain HTML tables
  └──────────┬───────────────┘
             │
             ▼
  ┌──────────────────────────┐
  │   Your Application        │ ◄── No changes needed
  │                          │
  │  React / Angular / Vue   │     Framework components
  │  AG Grid / DevExtreme    │     Grid library
  │  Bootstrap / Material    │     CSS framework
  └──────────────────────────┘