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:
+10
-2
@@ -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:**
|
||||
|
||||
@@ -737,13 +737,21 @@ curl http://localhost:1999/text?lang=es
|
||||
- Scroll-to-section functionality
|
||||
- Language-aware responses
|
||||
- 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:**
|
||||
- `internal/handlers/cv_cmdk.go` - CMD+K API handler
|
||||
- `static/js/ninja-keys-init.js` - Frontend initialization
|
||||
- `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/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
|
||||
|
||||
|
||||
@@ -176,6 +176,72 @@
|
||||
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
|
||||
============================================================================= */
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -24,11 +24,15 @@
|
||||
{{.UI.Widgets.ActionButtons.PrintFriendly}}
|
||||
</button>
|
||||
<button
|
||||
class="action-btn search-btn has-tooltip cmd-k-trigger"
|
||||
aria-label="{{.UI.Widgets.ActionButtons.SearchAriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.ActionButtons.Search}}">
|
||||
<iconify-icon icon="mdi:magnify" width="24" height="24"></iconify-icon>
|
||||
{{.UI.Widgets.ActionButtons.Search}}
|
||||
id="cmd-k-button"
|
||||
class="action-btn search-bar-btn cmd-k-trigger"
|
||||
aria-label="{{.UI.Widgets.ActionButtons.SearchAriaLabel}}">
|
||||
<iconify-icon icon="mdi:magnify" width="18" height="18" class="search-bar-icon"></iconify-icon>
|
||||
<span class="search-bar-text">{{.UI.Widgets.ActionButtons.Search}}</span>
|
||||
<span class="search-bar-keys">
|
||||
<kbd>⌘</kbd>
|
||||
<kbd>K</kbd>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{{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}}
|
||||
@@ -2,11 +2,11 @@
|
||||
/**
|
||||
* CMD+K BUTTON FUNCTIONALITY TEST
|
||||
* ================================
|
||||
* Tests that the CMD+K button:
|
||||
* - Has a distinct icon from zoom button
|
||||
* Tests that the CMD+K search bar button:
|
||||
* - Exists in the action bar with search bar styling
|
||||
* - Has keyboard shortcut indicators (⌘ K)
|
||||
* - Click handler opens ninja-keys
|
||||
* - At-bottom illumination works
|
||||
* - Has distinct color from zoom button
|
||||
* - Has search icon and text
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
@@ -14,7 +14,7 @@ import { chromium } from 'playwright';
|
||||
const URL = "http://localhost:1999";
|
||||
|
||||
async function testCmdKButton() {
|
||||
console.log('🎯 CMD+K BUTTON FUNCTIONALITY TEST\n');
|
||||
console.log('🎯 CMD+K SEARCH BAR BUTTON TEST\n');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const browser = await chromium.launch({ headless: process.env.HEADLESS === 'true' });
|
||||
@@ -26,85 +26,100 @@ async function testCmdKButton() {
|
||||
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 buttonExists = cmdKButton !== null;
|
||||
console.log(` Button exists: ${buttonExists}`);
|
||||
|
||||
let iconsDistinct = false;
|
||||
let hasSearchBarClass = false;
|
||||
if (buttonExists) {
|
||||
const cmdKIcon = await page.$eval('#cmd-k-button iconify-icon', el => el.getAttribute('icon'));
|
||||
const zoomIcon = await page.$eval('#zoom-toggle-button iconify-icon', el => el.getAttribute('icon'));
|
||||
|
||||
console.log(` CMD+K icon: ${cmdKIcon}`);
|
||||
console.log(` Zoom icon: ${zoomIcon}`);
|
||||
iconsDistinct = cmdKIcon !== zoomIcon;
|
||||
console.log(` ${iconsDistinct ? '✅ PASS' : '❌ FAIL'} - Icons are distinct`);
|
||||
hasSearchBarClass = await page.$eval('#cmd-k-button', el => el.classList.contains('search-bar-btn'));
|
||||
console.log(` Has search-bar-btn class: ${hasSearchBarClass}`);
|
||||
console.log(` ${hasSearchBarClass ? '✅ PASS' : '❌ FAIL'} - Button has search bar styling`);
|
||||
}
|
||||
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.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'));
|
||||
console.log(` Ninja-keys open: ${ninjaKeysOpen}`);
|
||||
console.log(` ${ninjaKeysOpen ? '✅ PASS' : '❌ FAIL'} - Click opens ninja-keys`);
|
||||
testResults.push({ test: 'Click opens ninja-keys', passed: ninjaKeysOpen });
|
||||
// Check if ninja-keys element was created and is open
|
||||
const ninjaKeysState = await page.evaluate(() => {
|
||||
const nk = document.getElementById('cmd-k-bar');
|
||||
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
|
||||
await page.keyboard.press('Escape');
|
||||
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
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await page.waitForTimeout(100);
|
||||
// Trigger handleScroll manually via hyperscript's behavior
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('scroll')));
|
||||
await page.waitForTimeout(300);
|
||||
const atTopClass = await page.$eval('#cmd-k-button', el => el.classList.contains('at-bottom'));
|
||||
console.log(` At top - has at-bottom class: ${atTopClass}`);
|
||||
|
||||
// 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 });
|
||||
let hasSearchText = false;
|
||||
if (buttonExists) {
|
||||
const searchText = await page.$eval('#cmd-k-button .search-bar-text', el => el.textContent.trim());
|
||||
hasSearchText = searchText.toLowerCase() === 'search';
|
||||
console.log(` Search text: "${searchText}"`);
|
||||
console.log(` ${hasSearchText ? '✅ PASS' : '❌ FAIL'} - Has "Search" text`);
|
||||
}
|
||||
testResults.push({ test: 'Has Search text', passed: hasSearchText });
|
||||
|
||||
// ========================================================================
|
||||
// FINAL SUMMARY
|
||||
|
||||
Reference in New Issue
Block a user