feat(command-palette): add command icons and enhance search panel styling

style(shell): update rail shortcut styles and improve hover effects
This commit is contained in:
4gray committed 2026-03-14 23:42:43 +01:00
1 parent 963049a7cc
commit b0a1d8da77
7 files changed
+169 -91

No files matched your search

@@ -269,7 +269,7 @@
gap: var(--widget-grid-gap);
align-items: stretch;
position: relative;
padding: 12px;
//padding: 12px;
min-width: 0;
overflow-x: clip;
}
@@ -318,7 +318,7 @@
}
.widget-slot:hover {
transform: translateY(-1px);
//transform: translateY(-1px);
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--dashboard-frame-border) 52%, transparent),
0 18px 28px -20px var(--dashboard-frame-glow);
@@ -4,24 +4,26 @@
<p>{{ 'WORKSPACE.COMMAND_PALETTE.SUBTITLE' | translate }}</p>
</header>
<label class="palette-search">
<mat-icon>search</mat-icon>
<input
#queryInput
type="search"
spellcheck="false"
[value]="query()"
(input)="onQueryInput($event)"
(keydown)="onInputKeydown($event)"
[placeholder]="
'WORKSPACE.COMMAND_PALETTE.SEARCH_PLACEHOLDER' | translate
"
[attr.aria-label]="
'WORKSPACE.COMMAND_PALETTE.SEARCH_LABEL' | translate
"
/>
<span class="palette-search__hint">Ctrl/Cmd+K</span>
</label>
<div class="palette-search-panel">
<label class="palette-search">
<mat-icon>search</mat-icon>
<input
#queryInput
type="search"
spellcheck="false"
[value]="query()"
(input)="onQueryInput($event)"
(keydown)="onInputKeydown($event)"
[placeholder]="
'WORKSPACE.COMMAND_PALETTE.SEARCH_PLACEHOLDER' | translate
"
[attr.aria-label]="
'WORKSPACE.COMMAND_PALETTE.SEARCH_LABEL' | translate
"
/>
<span class="palette-search__hint">Ctrl/Cmd+K</span>
</label>
</div>
<div class="palette-results">
@if (commandGroups().length === 0) {
@@ -37,17 +39,29 @@
<button
type="button"
class="palette-command"
[class.palette-command--with-icon]="
!!getCommandIcon(command)
"
[class.is-selected]="isCommandSelected(command)"
[disabled]="!command.enabled"
(mouseenter)="onCommandHover(command)"
(click)="onCommandClick(command)"
>
<span class="palette-command__label">{{
command.label
}}</span>
<span class="palette-command__description">{{
command.description
}}</span>
@if (getCommandIcon(command); as icon) {
<span class="palette-command__icon">
<mat-icon>{{ icon }}</mat-icon>
</span>
}
<span class="palette-command__content">
<span class="palette-command__label">{{
command.label
}}</span>
<span
class="palette-command__description"
>
{{ command.description }}
</span>
</span>
</button>
}
</div>
@@ -35,18 +35,37 @@
}
}
.palette-search-panel {
margin: 0 -2px;
padding: 10px 12px 12px;
border: 1px solid var(--app-separator, var(--mat-sys-outline-variant));
border-radius: 12px;
background: var(
--app-widget-header-bg,
var(--mat-sys-surface-container-high)
);
}
.palette-search {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
border-radius: 10px;
border: 1px solid var(--mat-sys-outline-variant);
background: var(--mat-sys-surface-container);
min-height: 38px;
padding: 0 10px;
border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant));
background: var(--app-search-bg, var(--mat-sys-surface-container));
min-height: 40px;
padding: 0 12px;
box-sizing: border-box;
transition:
border-color 0.15s ease,
background-color 0.15s ease,
box-shadow 0.15s ease;
&:focus-within {
border-color: var(--mat-sys-primary);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
}
mat-icon {
@@ -58,12 +77,13 @@
}
input {
flex: 1;
border: none;
outline: none;
background: transparent;
color: inherit;
color: var(--mat-sys-on-surface);
font: inherit;
font-size: 0.9rem;
font-size: 0.875rem;
width: 100%;
&::placeholder {
@@ -76,11 +96,15 @@
.palette-search__hint {
font-size: 0.72rem;
color: var(--mat-sys-on-surface-variant);
border: 1px solid var(--mat-sys-outline-variant);
border: 1px solid var(--app-separator, var(--mat-sys-outline-variant));
border-radius: 6px;
padding: 2px 6px;
line-height: 1;
flex-shrink: 0;
background: var(
--app-widget-header-bg,
var(--mat-sys-surface-container-high)
);
}
.palette-results {
@@ -139,6 +163,40 @@
}
}
.palette-command--with-icon {
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 10px;
}
.palette-command__icon {
width: 30px;
height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 9px;
background: color-mix(
in srgb,
var(--app-selection-surface-strong) 72%,
transparent
);
color: var(--app-selection-color);
flex-shrink: 0;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
.palette-command__content {
min-width: 0;
display: grid;
gap: 1px;
}
.palette-command__label {
font-size: 0.9rem;
font-weight: 560;
@@ -228,4 +228,23 @@ export class WorkspaceCommandPaletteComponent implements AfterViewInit {
}
return 'WORKSPACE.COMMAND_PALETTE.GROUP_SECTION';
}
getCommandIcon(command: WorkspaceCommandItem): string | null {
if (command.scope !== 'global') {
return null;
}
switch (command.id) {
case 'global-search':
return 'search';
case 'open-global-favorites':
return 'star';
case 'open-downloads':
return 'download';
case 'open-global-recent':
return 'history';
default:
return 'bolt';
}
}
}
@@ -41,7 +41,7 @@
<div class="rail-footer">
<a
class="rail-shortcut"
class="rail-shortcut nav-item"
routerLink="/workspace/settings"
[class.is-active]="isSettingsRoute()"
[matTooltip]="'WORKSPACE.SHELL.RAIL_SETTINGS' | translate"
@@ -1,3 +1,5 @@
@use '../../nav-list.scss';
:host {
display: block;
width: 100%;
@@ -146,6 +148,44 @@
.rail-footer {
margin-top: auto;
.rail-shortcut {
width: 44px;
height: 44px;
padding: 0;
gap: 0;
border-radius: 14px;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--mat-sys-on-surface-variant);
app-region: no-drag;
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
&:hover:not(.is-active) {
background: var(--app-selection-surface);
color: var(--mat-sys-on-surface);
}
&.is-active::after {
content: '';
position: absolute;
right: -4px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 20px;
border-radius: 999px;
background: var(--app-selection-color);
box-shadow: 0 0 10px var(--app-selection-glow);
}
}
}
// ─── Workspace Header ─────────────────────────────────────────────────────────
@@ -52,7 +52,6 @@ import {
import { PortalRailLinksComponent } from '@iptvnator/portal/shared/ui';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import {
DownloadsService,
PlaylistsService,
SettingsStore,
} from 'services';
@@ -121,7 +120,6 @@ export class WorkspaceShellComponent {
private readonly store = inject(Store);
private readonly xtreamStore = inject(XtreamStore);
private readonly destroyRef = inject(DestroyRef);
private readonly downloadsService = inject(DownloadsService);
readonly externalPlayback = inject(PORTAL_EXTERNAL_PLAYBACK);
private readonly settingsStore = inject(SettingsStore);
private readonly playlistsService = inject(PlaylistsService);
@@ -192,13 +190,14 @@ export class WorkspaceShellComponent {
return routeContext;
}
// On dashboard, sources, settings, and global favorites we still want provider-specific rail links
// for the currently active playlist selected in the switcher.
// On dashboard, sources, settings, global favorites, and global downloads we still want
// provider-specific rail links for the currently active playlist selected in the switcher.
if (
!this.isDashboardRoute() &&
!this.isSourcesRoute() &&
!this.isSettingsRoute() &&
!this.isGlobalFavoritesRoute()
!this.isGlobalFavoritesRoute() &&
!this.isGlobalDownloadsRoute()
) {
return null;
}
@@ -410,44 +409,6 @@ export class WorkspaceShellComponent {
const context = this.currentContext();
const section = this.currentSection();
const isGlobalDownloads = this.isGlobalDownloadsRoute();
if (
isGlobalDownloads ||
(context &&
(context.provider === 'xtreams' ||
context.provider === 'stalker') &&
section === 'downloads')
) {
const playlistId = context?.playlistId;
const hasClearable = this.downloadsService
.downloads()
.some(
(item) =>
(!playlistId || item.playlistId === playlistId) &&
(item.status === 'completed' ||
item.status === 'failed' ||
item.status === 'canceled')
);
return {
icon: 'delete_sweep',
tooltip: isGlobalDownloads
? this.translateText(
'WORKSPACE.SHELL.CLEAR_COMPLETED_DOWNLOADS_ALL'
)
: this.translateText(
'WORKSPACE.SHELL.CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST'
),
ariaLabel: isGlobalDownloads
? this.translateText(
'WORKSPACE.SHELL.CLEAR_COMPLETED_DOWNLOADS_ALL_ARIA'
)
: this.translateText(
'WORKSPACE.SHELL.CLEAR_COMPLETED_DOWNLOADS_THIS_PLAYLIST_ARIA'
),
disabled: !hasClearable,
};
}
if (!context || !section) {
return null;
@@ -695,20 +656,6 @@ export class WorkspaceShellComponent {
async runHeaderBulkAction(): Promise<void> {
const context = this.currentContext();
const section = this.currentSection();
const isGlobalDownloads = this.isGlobalDownloadsRoute();
if (
isGlobalDownloads ||
(context &&
(context.provider === 'xtreams' ||
context.provider === 'stalker') &&
section === 'downloads')
) {
const playlistId = context?.playlistId;
await this.downloadsService.clearCompleted(playlistId);
await this.downloadsService.loadDownloads(playlistId);
return;
}
if (!context || !section) {
return;