diff --git a/lib/rdoc/generator/template/aliki/DESIGN.md b/lib/rdoc/generator/template/aliki/DESIGN.md
index 3703055f61..ea863d5281 100644
--- a/lib/rdoc/generator/template/aliki/DESIGN.md
+++ b/lib/rdoc/generator/template/aliki/DESIGN.md
@@ -302,6 +302,19 @@ One search form and one result list serve both header layouts. CSS changes the p
| Type badge (`.search-type-*`) | inline-block, `space-0 space-2` pad, `xs`, weight 500, `radius-sm`, colors per §2 |
| Matched term (`li em`) | `search-highlight-bg`, `font-style: normal` |
+The desktop field has a `/` keycap and an adjacent **Search shortcuts** button
+with a keyboard icon. On compact screens, the keycap and adjacent button are
+hidden. The same help is available below the search results, outside their
+scroll area.
+
+The help uses one native dialog: a 376 px panel below the desktop button and a
+compact sheet aligned with the search surface. Pressing `?` outside an input,
+text area, select, or editable content opens help. It lists this shortcut and
+the existing `/`, arrow, `Enter`, and `Esc` actions, grouped by context. Close and Escape dismiss
+help and restore the previous focus without clearing the search. Pressing `/`
+in help closes the dialog and focuses the search field. Colors, keycaps,
+borders, and focus indicators use the shared light and dark tokens.
+
### Right TOC (`#table-of-contents`)
| Property | Value |
diff --git a/lib/rdoc/generator/template/aliki/_header.rhtml b/lib/rdoc/generator/template/aliki/_header.rhtml
index b7bcce114f..018df4c013 100644
--- a/lib/rdoc/generator/template/aliki/_header.rhtml
+++ b/lib/rdoc/generator/template/aliki/_header.rhtml
@@ -15,9 +15,10 @@
🔍
+ /
esc
@@ -31,7 +32,22 @@
No recent searches
+
+
+
+
+ Search shortcuts
+
+
+ <%= render '_search_shortcuts.rhtml' %>
diff --git a/lib/rdoc/generator/template/aliki/_icons.rhtml b/lib/rdoc/generator/template/aliki/_icons.rhtml
index 9c11a4a120..25f46720b5 100644
--- a/lib/rdoc/generator/template/aliki/_icons.rhtml
+++ b/lib/rdoc/generator/template/aliki/_icons.rhtml
@@ -1,4 +1,10 @@
+
+
+
+
+
+
+
Search shortcuts
+ Close
+
+ A few keys to find your way around.
+
+
+
When you are not typing in an input
+
+
Focus search /
+
Open search shortcuts ?
+
+
+
+
When results are shown
+
+
+
Move through results
+ ↑ ↓
+
+
Open selected result Enter
+
+
+
+
While search is open
+
+
Close search Esc
+
+
+
diff --git a/lib/rdoc/generator/template/aliki/css/rdoc.css b/lib/rdoc/generator/template/aliki/css/rdoc.css
index 61a8329e26..320ab87e6d 100644
--- a/lib/rdoc/generator/template/aliki/css/rdoc.css
+++ b/lib/rdoc/generator/template/aliki/css/rdoc.css
@@ -476,6 +476,9 @@ header.top-navbar .navbar-brand:hover {
}
header.top-navbar .navbar-search {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
grid-column: 2;
justify-self: center;
box-sizing: border-box;
@@ -485,6 +488,8 @@ header.top-navbar .navbar-search {
header.top-navbar .search-surface {
position: relative;
+ flex: 0 1 var(--layout-search-width);
+ min-width: 0;
width: min(100%, var(--layout-search-width));
margin: 0;
}
@@ -1991,7 +1996,8 @@ header.top-navbar #search-field {
display: block;
box-sizing: border-box;
width: 100%;
- padding: var(--space-2) var(--space-4);
+ min-height: 2.625rem;
+ padding: var(--space-2) var(--space-12) var(--space-2) var(--space-4);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
font-size: var(--font-size-base);
@@ -2036,7 +2042,193 @@ header.top-navbar #search-results[aria-expanded="false"] {
display: none;
}
+/* Search shortcut hint and help */
+.search-shortcuts kbd,
+.search-shortcut-hint {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ min-width: var(--space-6);
+ height: var(--space-6);
+ padding: 0 var(--space-1);
+ border: 1px solid var(--color-border-default);
+ border-bottom-width: 2px;
+ border-radius: var(--radius-sm);
+ background: var(--color-background-secondary);
+ color: var(--color-text-secondary);
+ font: var(--font-size-xs) / 1 var(--font-family-mono);
+}
+
+.search-shortcut-hint {
+ position: absolute;
+ top: 50%;
+ right: var(--space-3);
+ transform: translateY(-50%);
+ pointer-events: none;
+}
+
+.search-shortcuts-toggle,
+.search-shortcuts-close {
+ display: inline-flex;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-2);
+ padding: var(--space-2);
+ border: 1px solid var(--color-border-subtle);
+ border-radius: var(--radius-md);
+ background: var(--color-background-secondary);
+ color: var(--color-text-secondary);
+ font: inherit;
+ font-size: var(--font-size-sm);
+ cursor: pointer;
+}
+
+.search-shortcuts-toggle {
+ min-height: 2.625rem;
+ white-space: nowrap;
+}
+
+.search-shortcuts-toggle svg {
+ width: 1.125rem;
+ height: var(--space-4);
+}
+
+.search-shortcuts-toggle:hover,
+.search-shortcuts-close:hover,
+.search-shortcuts-toggle[aria-expanded="true"] {
+ border-color: var(--color-border-default);
+ color: var(--color-text-primary);
+}
+
+.search-shortcuts-toggle:focus-visible,
+.search-shortcuts-close:focus-visible {
+ outline: 2px solid var(--color-accent-primary);
+ outline-offset: 2px;
+}
+
+.search-shortcuts-footer {
+ display: none;
+}
+
+.search-shortcuts {
+ --search-shortcuts-right: var(--space-4);
+
+ position: fixed;
+ inset: calc(var(--layout-header-height) + var(--space-2)) var(--search-shortcuts-right) auto auto;
+ box-sizing: border-box;
+ width: 23.5rem;
+ max-width: calc(100% - var(--space-6));
+ max-height: calc(100dvh - var(--layout-header-height) - var(--space-6));
+ margin: 0;
+ padding: var(--space-5);
+ overflow: auto;
+ border: 1px solid var(--color-border-default);
+ border-radius: var(--radius-lg);
+ background: var(--color-background-primary);
+ color: var(--color-text-primary);
+ box-shadow: var(--shadow-lg);
+}
+
+.search-shortcuts::backdrop {
+ background: transparent;
+}
+
+.search-shortcuts-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-3);
+}
+
+.search-shortcuts-heading h2 {
+ margin: 0;
+ color: var(--color-text-primary);
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-semibold);
+}
+
+.search-shortcuts-intro {
+ margin: var(--space-2) 0 var(--space-4);
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm);
+}
+
+.search-shortcuts-group + .search-shortcuts-group {
+ margin-top: var(--space-3);
+ padding-top: var(--space-3);
+ border-top: 1px solid var(--color-border-subtle);
+}
+
+.search-shortcuts-group h3 {
+ margin: 0;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-medium);
+}
+
+.search-shortcuts-group dl {
+ margin: 0;
+ font-size: var(--font-size-sm);
+}
+
+.search-shortcuts-group dl > div {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-4);
+ min-height: 2.75rem;
+}
+
+.search-shortcuts-group dd {
+ display: flex;
+ flex-shrink: 0;
+ gap: var(--space-1);
+ margin: 0;
+}
+
@media (width <= 1023px) {
+ .search-shortcut-hint,
+ .search-shortcuts-desktop {
+ display: none;
+ }
+
+ .search-shortcuts-footer {
+ display: block;
+ flex-shrink: 0;
+ padding: var(--space-1) var(--space-3);
+ border-top: 1px solid var(--color-border-subtle);
+ background: var(--color-background-secondary);
+ }
+
+ .search-shortcuts-footer .search-shortcuts-toggle {
+ min-height: 2.75rem;
+ border-color: transparent;
+ background: transparent;
+ }
+
+ .search-shortcuts,
+ header.top-navbar .navbar-search.is-open .search-surface {
+ inset: var(--space-16) auto auto 50%;
+ width: calc(100% - var(--space-8));
+ max-width: 600px;
+ transform: translateX(-50%);
+ }
+
+ .search-shortcuts {
+ max-height: 80dvh;
+ }
+
+ .search-shortcuts::backdrop {
+ background: var(--color-overlay);
+ }
+
+ .search-shortcuts-close {
+ min-height: 2.75rem;
+ min-width: 2.75rem;
+ }
+
body:has(header.top-navbar .navbar-search.is-open) {
overflow: hidden;
}
@@ -2055,20 +2247,15 @@ header.top-navbar #search-results[aria-expanded="false"] {
header.top-navbar .navbar-search.is-open .search-surface {
position: fixed;
- top: var(--space-16);
- left: 50%;
z-index: 2;
display: flex;
flex-direction: column;
- width: calc(100% - var(--space-8));
- max-width: 600px;
max-height: 80vh;
margin: 0;
overflow: hidden;
background: var(--color-background-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
- transform: translateX(-50%);
}
header.top-navbar .navbar-search.is-open .search-form {
@@ -2175,9 +2362,17 @@ header.top-navbar #search-results[aria-expanded="false"] {
}
@media (width <= 420px) {
+ .search-shortcuts,
header.top-navbar .navbar-search.is-open .search-surface {
top: var(--space-4);
width: calc(100% - var(--space-6));
+ }
+
+ .search-shortcuts {
+ padding: var(--space-4);
+ }
+
+ header.top-navbar .navbar-search.is-open .search-surface {
border-radius: var(--radius-md);
}
diff --git a/lib/rdoc/generator/template/aliki/js/aliki.js b/lib/rdoc/generator/template/aliki/js/aliki.js
index 2855cabb75..9af319d69e 100644
--- a/lib/rdoc/generator/template/aliki/js/aliki.js
+++ b/lib/rdoc/generator/template/aliki/js/aliki.js
@@ -132,6 +132,7 @@ function hookSearch() {
});
}
+ hookSearchShortcuts(container, search, openSearch);
hookFocus(openSearch);
// Hide search results when clicking outside the search area
@@ -172,6 +173,72 @@ function hookSearch() {
/* ===== Keyboard Shortcuts ===== */
+function hookSearchShortcuts(container, search, openSearch) {
+ const dialog = document.querySelector('#search-shortcuts');
+ if (!dialog) return;
+
+ const triggers = container.querySelectorAll('[data-search-shortcuts]');
+ let navigationActive = false;
+
+ const openHelp = () => {
+ navigationActive = search.navigationActive;
+ search.setNavigationActive(false);
+ const right = container.getBoundingClientRect().right;
+ dialog.style.setProperty('--search-shortcuts-right', `${window.innerWidth - right}px`);
+ triggers.forEach((trigger) => trigger.setAttribute('aria-expanded', 'true'));
+ dialog.showModal();
+ };
+
+ triggers.forEach((trigger) => {
+ // SearchNavigation also handles Enter on document, even outside the input.
+ trigger.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter') event.stopPropagation();
+ });
+ trigger.addEventListener('click', () => {
+ trigger.focus({ preventScroll: true });
+ openHelp();
+ });
+ });
+
+ document.addEventListener('keydown', (event) => {
+ if (event.key !== '?' || event.defaultPrevented || event.isComposing ||
+ event.ctrlKey || event.metaKey || event.altKey) return;
+ const active = document.activeElement;
+ if (active.matches('input, textarea, select') || active.isContentEditable) return;
+
+ event.preventDefault();
+ openHelp();
+ });
+
+ // Keep search navigation paused, but let users try the focus shortcut.
+ dialog.addEventListener('keydown', (event) => {
+ event.stopPropagation();
+ if (event.key === '/') {
+ event.preventDefault();
+ // Restore search focus after the dialog restores its navigation state.
+ dialog.addEventListener('close', () => {
+ if (container.classList.contains('is-open')) {
+ search.input.focus({ preventScroll: true });
+ } else {
+ openSearch();
+ }
+ }, { once: true });
+ dialog.close();
+ }
+ });
+ dialog.querySelector('.search-shortcuts-close').addEventListener('click', () => dialog.close());
+ dialog.addEventListener('click', (event) => {
+ if (event.target !== dialog) return;
+ const bounds = dialog.getBoundingClientRect();
+ if (event.clientX < bounds.left || event.clientX > bounds.right ||
+ event.clientY < bounds.top || event.clientY > bounds.bottom) dialog.close();
+ });
+ dialog.addEventListener('close', () => {
+ triggers.forEach((trigger) => trigger.setAttribute('aria-expanded', 'false'));
+ search.setNavigationActive(navigationActive);
+ });
+}
+
function hookFocus(openSearch) {
document.addEventListener("keydown", (event) => {
if (document.activeElement.tagName === 'INPUT') {