feat: redesign CMD+K button as macOS Spotlight-style search bar

Replace simple search button with search bar design in action bar:
- Semi-transparent styling integrated with dark action bar
- Keyboard shortcut indicators (⌘ K) shown as individual kbd elements
- Search icon and "Search" text for better discoverability
- Responsive: kbd keys hidden on mobile (<900px)
- Remove unused cmd-k-button.html partial template

Update test to verify new search bar structure (styling, kbd elements, icon).
This commit is contained in:
juanatsap
2025-12-04 12:59:16 +00:00
parent b5a50ca3ef
commit 404748afb5
6 changed files with 163 additions and 81 deletions
+10 -2
View File
@@ -728,7 +728,7 @@ curl http://localhost:1999/text?lang=es
--- ---
### 8. CMD+K Command Palette (2025-12-01) ### 8. CMD+K Command Palette (2025-12-01, updated 2025-12-04)
**ninja-keys integration for quick navigation:** **ninja-keys integration for quick navigation:**
@@ -737,13 +737,21 @@ curl http://localhost:1999/text?lang=es
- Scroll-to-section functionality - Scroll-to-section functionality
- Language-aware responses - Language-aware responses
- 1-hour cache headers - 1-hour cache headers
- **Search bar button** (2025-12-04): macOS Spotlight-style search bar in action bar
- Integrated in dark action bar with semi-transparent styling
- Shows keyboard shortcut indicators (⌘ K) as individual kbd elements
- Replaces old simple search button with more discoverable design
- CSS: `.search-bar-btn`, `.search-bar-icon`, `.search-bar-text`, `.search-bar-keys`
- Responsive: kbd keys hidden on mobile (<900px)
**Files:** **Files:**
- `internal/handlers/cv_cmdk.go` - CMD+K API handler - `internal/handlers/cv_cmdk.go` - CMD+K API handler
- `static/js/ninja-keys-init.js` - Frontend initialization - `static/js/ninja-keys-init.js` - Frontend initialization
- `doc/16-CMD-K-API.md` - API documentation - `doc/16-CMD-K-API.md` - API documentation
- `templates/partials/navigation/action-buttons.html` - Search bar button HTML
- `static/css/04-interactive/_buttons.css` - Search bar button styles
**Tests:** **Tests:**
- `tests/mjs/71-cmd-k-api-scroll.test.mjs` - `tests/mjs/71-cmd-k-api-scroll.test.mjs`
- `tests/mjs/72-cmd-k-button.test.mjs` - `tests/mjs/72-cmd-k-button.test.mjs` - Tests search bar styling, kbd elements, icon, click behavior
+66
View File
@@ -176,6 +176,72 @@
The 900px media query there uses clamp() for smooth scaling of all buttons and icons */ The 900px media query there uses clamp() for smooth scaling of all buttons and icons */
/* =============================================================================
CMD+K SEARCH BAR BUTTON (Integrated in Action Bar)
============================================================================= */
/* Search bar button - styled to look like a search input within the dark action bar */
.search-bar-btn {
display: flex !important;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.6rem !important;
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
border-radius: 6px !important;
min-width: 140px;
transition: all 0.2s ease;
}
.search-bar-btn:hover {
background: rgba(255, 255, 255, 0.15) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
}
.search-bar-icon {
color: rgba(255, 255, 255, 0.6);
flex-shrink: 0;
}
.search-bar-text {
color: rgba(255, 255, 255, 0.6);
font-size: 0.8rem;
flex-grow: 1;
text-align: left;
}
.search-bar-keys {
display: flex;
align-items: center;
gap: 0.2rem;
flex-shrink: 0;
}
.search-bar-keys kbd {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
font-size: 0.65rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 3px;
padding: 0.1rem 0.3rem;
min-width: 1.2rem;
text-align: center;
}
/* Hide kbd keys on smaller screens to save space */
@media (max-width: 900px) {
.search-bar-keys {
display: none;
}
.search-bar-btn {
min-width: auto;
}
}
/* ============================================================================= /* =============================================================================
NINJA-KEYS COMMAND BAR STYLING NINJA-KEYS COMMAND BAR STYLING
============================================================================= */ ============================================================================= */
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -24,11 +24,15 @@
{{.UI.Widgets.ActionButtons.PrintFriendly}} {{.UI.Widgets.ActionButtons.PrintFriendly}}
</button> </button>
<button <button
class="action-btn search-btn has-tooltip cmd-k-trigger" id="cmd-k-button"
aria-label="{{.UI.Widgets.ActionButtons.SearchAriaLabel}}" class="action-btn search-bar-btn cmd-k-trigger"
data-tooltip="{{.UI.Widgets.ActionButtons.Search}}"> aria-label="{{.UI.Widgets.ActionButtons.SearchAriaLabel}}">
<iconify-icon icon="mdi:magnify" width="24" height="24"></iconify-icon> <iconify-icon icon="mdi:magnify" width="18" height="18" class="search-bar-icon"></iconify-icon>
{{.UI.Widgets.ActionButtons.Search}} <span class="search-bar-text">{{.UI.Widgets.ActionButtons.Search}}</span>
<span class="search-bar-keys">
<kbd></kbd>
<kbd>K</kbd>
</span>
</button> </button>
</div> </div>
{{end}} {{end}}
@@ -1,11 +0,0 @@
{{define "cmd-k-button"}}
<!-- CMD+K Command Bar Button (Fixed Left - Last) -->
<!-- Uses lazy loading - ninja-keys loads on first click -->
<button
id="cmd-k-button"
class="fixed-btn cmd-k-btn no-print has-tooltip cmd-k-trigger"
aria-label="{{.UI.CmdK.Button.AriaLabel}}"
data-tooltip="{{.UI.CmdK.Button.Tooltip}}">
<iconify-icon icon="mdi:text-box-search-outline"></iconify-icon>
</button>
{{end}}
+77 -62
View File
@@ -2,11 +2,11 @@
/** /**
* CMD+K BUTTON FUNCTIONALITY TEST * CMD+K BUTTON FUNCTIONALITY TEST
* ================================ * ================================
* Tests that the CMD+K button: * Tests that the CMD+K search bar button:
* - Has a distinct icon from zoom button * - Exists in the action bar with search bar styling
* - Has keyboard shortcut indicators (⌘ K)
* - Click handler opens ninja-keys * - Click handler opens ninja-keys
* - At-bottom illumination works * - Has search icon and text
* - Has distinct color from zoom button
*/ */
import { chromium } from 'playwright'; import { chromium } from 'playwright';
@@ -14,7 +14,7 @@ import { chromium } from 'playwright';
const URL = "http://localhost:1999"; const URL = "http://localhost:1999";
async function testCmdKButton() { async function testCmdKButton() {
console.log('🎯 CMD+K BUTTON FUNCTIONALITY TEST\n'); console.log('🎯 CMD+K SEARCH BAR BUTTON TEST\n');
console.log('='.repeat(70)); console.log('='.repeat(70));
const browser = await chromium.launch({ headless: process.env.HEADLESS === 'true' }); const browser = await chromium.launch({ headless: process.env.HEADLESS === 'true' });
@@ -26,85 +26,100 @@ async function testCmdKButton() {
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
// ======================================================================== // ========================================================================
// TEST 1: Button exists with distinct icon // TEST 1: Button exists with search bar class
// ======================================================================== // ========================================================================
console.log('\n1️⃣ Testing button exists with distinct icon...'); console.log('\n1️⃣ Testing button exists with search bar styling...');
const cmdKButton = await page.$('#cmd-k-button'); const cmdKButton = await page.$('#cmd-k-button');
const buttonExists = cmdKButton !== null; const buttonExists = cmdKButton !== null;
console.log(` Button exists: ${buttonExists}`); console.log(` Button exists: ${buttonExists}`);
let iconsDistinct = false; let hasSearchBarClass = false;
if (buttonExists) { if (buttonExists) {
const cmdKIcon = await page.$eval('#cmd-k-button iconify-icon', el => el.getAttribute('icon')); hasSearchBarClass = await page.$eval('#cmd-k-button', el => el.classList.contains('search-bar-btn'));
const zoomIcon = await page.$eval('#zoom-toggle-button iconify-icon', el => el.getAttribute('icon')); console.log(` Has search-bar-btn class: ${hasSearchBarClass}`);
console.log(` ${hasSearchBarClass ? '✅ PASS' : '❌ FAIL'} - Button has search bar styling`);
console.log(` CMD+K icon: ${cmdKIcon}`);
console.log(` Zoom icon: ${zoomIcon}`);
iconsDistinct = cmdKIcon !== zoomIcon;
console.log(` ${iconsDistinct ? '✅ PASS' : '❌ FAIL'} - Icons are distinct`);
} }
testResults.push({ test: 'Icons are distinct', passed: buttonExists && iconsDistinct }); testResults.push({ test: 'Button has search bar styling', passed: buttonExists && hasSearchBarClass });
// ======================================================================== // ========================================================================
// TEST 2: Button click opens ninja-keys // TEST 2: Button has keyboard shortcut keys (kbd elements)
// ======================================================================== // ========================================================================
console.log('\n2️⃣ Testing button click opens ninja-keys...'); console.log('\n2️⃣ Testing keyboard shortcut indicators...');
let hasKbdElements = false;
if (buttonExists) {
const kbdCount = await page.$$eval('#cmd-k-button kbd', els => els.length);
hasKbdElements = kbdCount === 2; // Should have ⌘ and K
console.log(` Kbd elements found: ${kbdCount}`);
if (kbdCount === 2) {
const kbdTexts = await page.$$eval('#cmd-k-button kbd', els => els.map(el => el.textContent.trim()));
console.log(` Kbd contents: ${kbdTexts.join(', ')}`);
}
console.log(` ${hasKbdElements ? '✅ PASS' : '❌ FAIL'} - Has ⌘ K keyboard indicators`);
}
testResults.push({ test: 'Has keyboard shortcut indicators', passed: hasKbdElements });
// ========================================================================
// TEST 3: Button has search icon
// ========================================================================
console.log('\n3️⃣ Testing search icon...');
let hasSearchIcon = false;
if (buttonExists) {
const icon = await page.$eval('#cmd-k-button iconify-icon', el => el.getAttribute('icon'));
hasSearchIcon = icon && icon.includes('magnify');
console.log(` Icon: ${icon}`);
console.log(` ${hasSearchIcon ? '✅ PASS' : '❌ FAIL'} - Has search/magnify icon`);
}
testResults.push({ test: 'Has search icon', passed: hasSearchIcon });
// ========================================================================
// TEST 4: Button click opens ninja-keys
// ========================================================================
console.log('\n4️⃣ Testing button click opens ninja-keys...');
// First click triggers lazy load of ninja-keys
await page.click('#cmd-k-button'); await page.click('#cmd-k-button');
await page.waitForTimeout(500); await page.waitForTimeout(3000); // Wait for module to load (lazy loading)
const ninjaKeysOpen = await page.$eval('#cmd-k-bar', el => el.hasAttribute('open')); // Check if ninja-keys element was created and is open
console.log(` Ninja-keys open: ${ninjaKeysOpen}`); const ninjaKeysState = await page.evaluate(() => {
console.log(` ${ninjaKeysOpen ? '✅ PASS' : '❌ FAIL'} - Click opens ninja-keys`); const nk = document.getElementById('cmd-k-bar');
testResults.push({ test: 'Click opens ninja-keys', passed: ninjaKeysOpen }); if (!nk) return { exists: false, isOpen: false };
// ninja-keys uses shadow DOM with .modal.visible class
const shadow = nk.shadowRoot;
const modal = shadow?.querySelector('.modal');
return {
exists: true,
isOpen: modal?.classList?.contains('visible') || nk.hasAttribute('open') || false
};
});
console.log(` Ninja-keys exists: ${ninjaKeysState.exists}`);
console.log(` Ninja-keys open: ${ninjaKeysState.isOpen}`);
const ninjaKeysOpened = ninjaKeysState.exists && ninjaKeysState.isOpen;
console.log(` ${ninjaKeysOpened ? '✅ PASS' : '❌ FAIL'} - Click opens ninja-keys`);
testResults.push({ test: 'Click opens ninja-keys', passed: ninjaKeysOpened });
// Close ninja-keys // Close ninja-keys
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await page.waitForTimeout(300); await page.waitForTimeout(300);
// ======================================================================== // ========================================================================
// TEST 3: At-bottom illumination // TEST 5: Button has Search text
// ======================================================================== // ========================================================================
console.log('\n3️⃣ Testing at-bottom illumination...'); console.log('\n5️⃣ Testing Search text...');
// Scroll to very top - use scroll() which fires event naturally let hasSearchText = false;
await page.evaluate(() => window.scrollTo(0, 0)); if (buttonExists) {
await page.waitForTimeout(100); const searchText = await page.$eval('#cmd-k-button .search-bar-text', el => el.textContent.trim());
// Trigger handleScroll manually via hyperscript's behavior hasSearchText = searchText.toLowerCase() === 'search';
await page.evaluate(() => window.dispatchEvent(new Event('scroll'))); console.log(` Search text: "${searchText}"`);
await page.waitForTimeout(300); console.log(` ${hasSearchText ? '✅ PASS' : '❌ FAIL'} - Has "Search" text`);
const atTopClass = await page.$eval('#cmd-k-button', el => el.classList.contains('at-bottom')); }
console.log(` At top - has at-bottom class: ${atTopClass}`); testResults.push({ test: 'Has Search text', passed: hasSearchText });
// Scroll to bottom using mouse wheel to trigger actual scroll
await page.mouse.wheel(0, 100000);
await page.waitForTimeout(1000);
const atBottomClass = await page.$eval('#cmd-k-button', el => el.classList.contains('at-bottom'));
const zoomAtBottomClass = await page.$eval('#zoom-toggle-button', el => el.classList.contains('at-bottom'));
const infoAtBottomClass = await page.$eval('#info-button', el => el.classList.contains('at-bottom'));
console.log(` At bottom - CMD+K has at-bottom: ${atBottomClass}`);
console.log(` At bottom - Zoom has at-bottom: ${zoomAtBottomClass}`);
console.log(` At bottom - Info has at-bottom: ${infoAtBottomClass}`);
const illuminationWorks = !atTopClass && atBottomClass;
console.log(` ${illuminationWorks ? '✅ PASS' : '❌ FAIL'} - At-bottom illumination works`);
testResults.push({ test: 'At-bottom illumination', passed: illuminationWorks });
// ========================================================================
// TEST 4: Distinct color from zoom button
// ========================================================================
console.log('\n4️⃣ Testing distinct color at bottom...');
const cmdKBgColor = await page.$eval('#cmd-k-button', el => window.getComputedStyle(el).backgroundColor);
const zoomBgColor = await page.$eval('#zoom-toggle-button', el => window.getComputedStyle(el).backgroundColor);
console.log(` CMD+K bg color: ${cmdKBgColor}`);
console.log(` Zoom bg color: ${zoomBgColor}`);
const colorsDistinct = cmdKBgColor !== zoomBgColor;
console.log(` ${colorsDistinct ? '✅ PASS' : '❌ FAIL'} - Colors are distinct`);
testResults.push({ test: 'Colors are distinct', passed: colorsDistinct });
// ======================================================================== // ========================================================================
// FINAL SUMMARY // FINAL SUMMARY