Menu

Change language

GitHub repo

Shortcuts Dialog

blocklayout

Linear-style keyboard reference sheet: a '?' dialog listing shortcut groups (label, description, Kbd combo) in a responsive two-column grid. Ships a built-in trigger button, controlled/uncontrolled open, custom groups via input, and an Esc footer hint. Display only — wire real handlers in your app.

Installation

$npx uipkge-ng@latest add shortcuts-dialog
Manifest:https://uipkge.dev/r/angular/shortcuts-dialog.jsonInstalls to:src/app/components/blocks/shortcuts-dialog/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

Shortcut
interface Shortcut {
  keys: string[]
  description: string
}
ShortcutGroup
interface ShortcutGroup {
  label: string
  shortcuts: Shortcut[]
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/shortcuts-dialog/shortcuts-dialog.component.ts6 kB
    // Linear-style keyboard reference sheet: a '?' dialog listing shortcut groups (label,
    // description, Kbd combo) in a responsive two-column grid. Ships a built-in trigger button,
    // controlled/uncontrolled open, custom groups via input, and an Esc footer hint.
    // Display only -- wire real handlers in your app. Port of the React block 1:1.
    import {
      ChangeDetectionStrategy,
      Component,
      EventEmitter,
      Input,
      Output,
      booleanAttribute,
      signal,
    } from '@angular/core'
    import { Keyboard, LucideAngularModule } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiDialogComponent,
      UiDialogContentComponent,
      UiDialogDescriptionComponent,
      UiDialogFooterComponent,
      UiDialogHeaderComponent,
      UiDialogTitleComponent,
      UiDialogTriggerComponent,
    } from '@/ui/dialog/dialog.component'
    import { UiKbdComponent } from '@/ui/kbd/kbd.component'
    
    export interface Shortcut {
      keys: string[]
      description: string
    }
    
    export interface ShortcutGroup {
      label: string
      shortcuts: Shortcut[]
    }
    
    const defaultGroups: ShortcutGroup[] = [
      {
        label: 'Navigation',
        shortcuts: [
          { keys: ['G', 'I'], description: 'Go to inbox' },
          { keys: ['G', 'D'], description: 'Go to drafts' },
          { keys: ['⌘', 'K'], description: 'Open command menu' },
          { keys: ['⌥', '↑', '↓'], description: 'Move between items' },
        ],
      },
      {
        label: 'Actions',
        shortcuts: [
          { keys: ['C'], description: 'Compose new' },
          { keys: ['/'], description: 'Focus search' },
          { keys: ['⌘', 'S'], description: 'Save changes' },
          { keys: ['E'], description: 'Archive selection' },
        ],
      },
      {
        label: 'General',
        shortcuts: [
          { keys: ['?'], description: 'Show this dialog' },
          { keys: ['⇧', '?'], description: 'Show cheat sheet' },
          { keys: ['Esc'], description: 'Close dialogs' },
        ],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-shortcuts-dialog, [ui-shortcuts-dialog]',
      standalone: true,
      // React renders the Dialog root itself (no element): the host stays out of layout.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiButtonComponent,
        UiDialogComponent,
        UiDialogContentComponent,
        UiDialogDescriptionComponent,
        UiDialogFooterComponent,
        UiDialogHeaderComponent,
        UiDialogTitleComponent,
        UiDialogTriggerComponent,
        UiKbdComponent,
      ],
      template: `
        <ui-dialog [open]="dialogOpen" (openChange)="onOpenChange($event)">
          <div data-slot="shortcuts-dialog" class="contents">
            @if (trigger) {
              <button type="button" ui-button ui-dialog-trigger variant="outline" size="sm">
                <lucide-icon [img]="Keyboard" aria-hidden="true" />
                Shortcuts
                <span ui-kbd>?</span>
              </button>
            } @else {
              <!-- Custom trigger: projected content cannot inject the dialog (Angular resolves
                   projected DI at the declaration site), so clicks are wired here instead.
                   Project any button -- no ui-dialog-trigger needed. -->
              <span class="contents" (click)="openDialog()">
                <ng-content />
              </span>
            }
            <ui-dialog-content [class]="contentClass">
              <ui-dialog-header>
                <h2 ui-dialog-title>Keyboard shortcuts</h2>
                <p ui-dialog-description>Move faster without leaving the keyboard.</p>
              </ui-dialog-header>
              <div class="grid max-h-[60vh] gap-x-8 gap-y-6 overflow-y-auto pr-1 sm:grid-cols-2">
                @for (group of groups; track group.label) {
                  <div class="space-y-2">
                    <p class="text-muted-foreground text-xs font-medium tracking-widest uppercase">{{ group.label }}</p>
                    <ul class="divide-y">
                      @for (shortcut of group.shortcuts; track shortcut.description) {
                        <li class="flex items-center justify-between gap-4 py-1.5">
                          <span class="text-sm">{{ shortcut.description }}</span>
                          <span class="flex shrink-0 items-center gap-1">
                            @for (key of shortcut.keys; track key) {
                              <span ui-kbd>{{ key }}</span>
                            }
                          </span>
                        </li>
                      }
                    </ul>
                  </div>
                }
              </div>
              @if (footer) {
                <ui-dialog-footer class="justify-start">
                  <p class="text-muted-foreground flex items-center gap-1.5 text-xs">
                    Press <span ui-kbd>Esc</span> to close
                  </p>
                </ui-dialog-footer>
              }
            </ui-dialog-content>
          </div>
        </ui-dialog>
      `,
    })
    export class UiShortcutsDialogComponent {
      /** Shortcut sections to render. Defaults to the Navigation / Actions / General set. */
      @Input() groups: ShortcutGroup[] = defaultGroups
      /** Render the built-in trigger button. Set false to project your own trigger (clicks open the dialog). */
      @Input({ transform: booleanAttribute }) trigger = true
      /** Show the 'Press Esc to close' footer hint. */
      @Input({ transform: booleanAttribute }) footer = true
      /** Controlled open state (pair with openChange); omit for uncontrolled. */
      @Input() set open(v: boolean | undefined) {
        this.controlledOpen.set(v)
      }
      @Output() openChange = new EventEmitter<boolean>()
      @Input('class') className?: string
    
      private readonly controlledOpen = signal<boolean | undefined>(undefined)
      private readonly internalOpen = signal(false)
    
      protected readonly Keyboard = Keyboard
    
      get dialogOpen(): boolean {
        return this.controlledOpen() ?? this.internalOpen()
      }
    
      get contentClass(): string {
        return cn('max-w-lg', this.className)
      }
    
      onOpenChange(value: boolean): void {
        this.internalOpen.set(value)
        this.openChange.emit(value)
      }
    
      /** Custom-trigger path: the projected button has no trigger directive, so open directly. */
      openDialog(): void {
        this.onOpenChange(true)
      }
    }
    
  • src/app/components/blocks/shortcuts-dialog/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/shortcuts-dialog.json