PageSourceSearch

https://krm.fyi/command-palette.js

js krm.fyi collected 2026-10-10 02:44:49 UTC 8,023 bytes, 199 lines download raw bytes

1// command-palette.js — Cmd/Ctrl+K quick navigation
2(function () {
3  'use strict';
4
5  var dialog = document.getElementById('cmdk-dialog');
6  if (!dialog) return;
7
8  var langUrl     = dialog.getAttribute('data-lang-url') || '';
9  var currentLang = dialog.getAttribute('data-lang') || 'en';
10  var LOCALES     = ['ja', 'zh', 'ko', 'fr', 'es', 'de', 'it', 'lzh', 'yue', 'bo'];
11  var pathPrefix  = LOCALES.indexOf(currentLang) !== -1 ? '/' + currentLang : '';
12
13  var input    = dialog.querySelector('.cmdk-input');
14  var listEl   = dialog.querySelector('.cmdk-list');
15  var emptyEl  = dialog.querySelector('.cmdk-empty');
16
17  var EMAIL = '[email protected]';
18
19  var COMMANDS = [
20    { label: 'Home',                        hint: 'Overview & featured work',         url: pathPrefix + '/',                      keywords: 'index landing' },
21    { label: 'Work',                        hint: 'Portfolio projects',               url: pathPrefix + '/work.html',             keywords: 'projects portfolio' },
22    { label: 'Publications & Awards',       hint: 'Recognition',                      url: pathPrefix + '/#publications',         keywords: 'awards papers transparency report' },
23    { label: 'Blog',                        hint: 'Writing & thoughts',               url: pathPrefix + '/blog.html',             keywords: 'writing posts articles' },
24    { label: 'Transparency Report dashboard', hint: 'Google gov removals — interactive', url: pathPrefix + '/transparency.html',   keywords: 'google government removals data analysis dashboard charts' },
25    { label: 'Filter projects: AI / LLM',   hint: 'RAG, Gemini, prompt engineering',  url: '/work.html?filter=ai-llm',            keywords: 'rag gemini llm prompt' },
26    { label: 'Filter projects: Compliance', hint: 'CMA, EU DSA, transparency',        url: '/work.html?filter=compliance',        keywords: 'cma dsa regulatory' },
27    { label: 'Filter projects: Data & SQL', hint: 'SQL, ETL, pipelines',              url: '/work.html?filter=data',              keywords: 'sql etl bigquery' },
28    { label: 'Filter projects: Automation', hint: 'Chrome ext, AppsScript',           url: '/work.html?filter=automation',        keywords: 'chrome browser appsscript' },
29    { label: 'Copy email address',          hint: EMAIL,                              action: 'copy-email',                       keywords: 'contact mail' },
30    { label: 'Email Kieran',                hint: 'Compose new message',              url: 'mailto:' + EMAIL,                     keywords: 'contact mail' },
31    { label: 'LinkedIn',                    hint: 'Open profile',                     url: 'https://linkedin.com/in/KieranMaynard', external: true, keywords: 'social' },
32    { label: 'GitHub',                      hint: '@krmaynard',                       url: 'https://github.com/krmaynard',          external: true, keywords: 'code repos' }
33  ];
34
35  if (langUrl) {
36    COMMANDS.push({
37      label: currentLang === 'en' ? '日本語で読む' : 'Switch to English',
38      hint: 'Language switch',
39      url: langUrl,
40      keywords: 'language japanese english nihongo'
41    });
42  }
43
44  var filtered = COMMANDS.slice();
45  var cursor = 0;
46
47  function escapeHtml(s) {
48    return String(s).replace(/[&<>"']/g, function (c) {
49      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
50    });
51  }
52
53  function render() {
54    listEl.innerHTML = '';
55    if (filtered.length === 0) {
56      emptyEl.hidden = false;
57      return;
58    }
59    emptyEl.hidden = true;
60    var frag = document.createDocumentFragment();
61    filtered.forEach(function (cmd, i) {
62      var li = document.createElement('li');
63      li.className = 'cmdk-item' + (i === cursor ? ' is-active' : '');
64      li.setAttribute('role', 'option');
65      li.setAttribute('aria-selected', i === cursor ? 'true' : 'false');
66      li.setAttribute('data-index', String(i));
67      li.innerHTML =
68        '<span class="cmdk-label">' + escapeHtml(cmd.label) + '</span>' +
69        '<span class="cmdk-hint">' + escapeHtml(cmd.hint || '') + '</span>';
70      frag.appendChild(li);
71    });
72    listEl.appendChild(frag);
73  }
74
75  function applyFilter(q) {
76    q = (q || '').trim().toLowerCase();
77    if (!q) {
78      filtered = COMMANDS.slice();
79    } else {
80      var terms = q.split(/\s+/);
81      filtered = COMMANDS.filter(function (cmd) {
82        var hay = (cmd.label + ' ' + (cmd.hint || '') + ' ' + (cmd.keywords || '')).toLowerCase();
83        return terms.every(function (t) { return hay.indexOf(t) !== -1; });
84      });
85    }
86    cursor = 0;
87    render();
88  }
89
90  function toast(msg) {
91    var t = document.createElement('div');
92    t.className = 'cmdk-toast';
93    t.textContent = msg;
94    document.body.appendChild(t);
95    setTimeout(function () { t.classList.add('is-out'); }, 1600);
96    setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 2200);
97  }
98
99  function activate(cmd) {
100    if (!cmd) return;
101    closeDialog();
102    if (cmd.action === 'copy-email') {
103      if (navigator.clipboard && navigator.clipboard.writeText) {
104        navigator.clipboard.writeText(EMAIL).then(
105          function () { toast('Email copied to clipboard'); },
106          function () { window.location.href = 'mailto:' + EMAIL; }
107        );
108      } else {
109        window.location.href = 'mailto:' + EMAIL;
110      }
111      return;
112    }
113    if (cmd.url) {
114      if (cmd.external) window.open(cmd.url, '_blank', 'noopener');
115      else              window.location.href = cmd.url;
116    }
117  }
118
119  function openDialog() {
120    if (dialog.open) return;
121    input.value = '';
122    applyFilter('');
123    if (typeof dialog.showModal === 'function') dialog.showModal();
124    else dialog.setAttribute('open', '');
125    setTimeout(function () { input.focus(); }, 0);
126  }
127
128  function closeDialog() {
129    if (typeof dialog.close === 'function' && dialog.open) dialog.close();
130    else dialog.removeAttribute('open');
131  }
132
133  function scrollCursorIntoView() {
134    var el = listEl.querySelector('.cmdk-item.is-active');
135    if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' });
136  }
137
138  document.addEventListener('keydown', function (e) {
139    if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
140      e.preventDefault();
141      if (dialog.open) closeDialog(); else openDialog();
142    }
143  });
144
145  input.addEventListener('keydown', function (e) {
146    if (filtered.length === 0) return;
147    if (e.key === 'ArrowDown') {
148      e.preventDefault();
149      cursor = (cursor + 1) % filtered.length;
150      render();
151      scrollCursorIntoView();
152    } else if (e.key === 'ArrowUp') {
153      e.preventDefault();
154      cursor = (cursor - 1 + filtered.length) % filtered.length;
155      render();
156      scrollCursorIntoView();
157    } else if (e.key === 'Enter') {
158      e.preventDefault();
159      activate(filtered[cursor]);
160    }
161  });
162
163  input.addEventListener('input', function () { applyFilter(input.value); });
164
165  listEl.addEventListener('click', function (e) {
166    var li = e.target.closest('.cmdk-item');
167    if (!li) return;
168    activate(filtered[+li.getAttribute('data-index')]);
169  });
170
171  listEl.addEventListener('mousemove', function (e) {
172    var li = e.target.closest('.cmdk-item');
173    if (!li) return;
174    var i = +li.getAttribute('data-index');
175    if (i !== cursor) {
176      cursor = i;
177      listEl.querySelectorAll('.cmdk-item').forEach(function (n, idx) {
178        var active = idx === cursor;
179        n.classList.toggle('is-active', active);
180        n.setAttribute('aria-selected', active ? 'true' : 'false');
181      });
182    }
183  });
184
185  dialog.addEventListener('click', function (e) {
186    if (e.target === dialog) closeDialog();
187  });
188
189  Array.prototype.forEach.call(document.querySelectorAll('[data-open-cmdk]'), function (btn) {
190    btn.addEventListener('click', openDialog);
191  });
192
193  var isMac = /Mac|iPhone|iPad/.test(navigator.platform || '');
194  Array.prototype.forEach.call(document.querySelectorAll('.cmdk-shortcut'), function (el) {
195    el.textContent = isMac ? '⌘K' : 'Ctrl K';
196  });
197
198  render();
199})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.