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 prop, and an Esc footer hint. Display only — wire real handlers in your app.

Installation

$npx shadcn@latest add https://uipkge.dev/r/lit/shortcuts-dialog.json
Manifest:https://uipkge.dev/r/lit/shortcuts-dialog.jsonInstalls to:src/uip/blocks/shortcuts-dialog/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
triggerbooleantrueoptional
footerbooleantrueoptional
openbooleanfalseoptional

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[]
}
HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-shortcuts-dialog': UipShortcutsDialog
  }
}

npm dependencies

Files installed (1)

  • src/uip/blocks/shortcuts-dialog/shortcuts-dialog.ts5 kB
    import { LitElement, css, html, nothing } from 'lit'
    import { Keyboard } from 'lucide'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    import { icon } from '../../lib/icon'
    // Side-effect imports: register the composed elements for standalone use
    // (consumers who install only this block still get working primitives).
    import '../../components/button/button'
    import '../../components/dialog/dialog'
    import '../../components/kbd/kbd'
    
    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' },
        ],
      },
    ]
    
    // Boolean props that default to `true`: `attr="false"` turns them off.
    const trueByDefault = { fromAttribute: (v: string | null) => v !== 'false' }
    
    /**
     * <uip-shortcuts-dialog> — the registry ShortcutsDialog block as a web component.
     *
     * Class strings are React's ShortcutsDialog verbatim. `groups` is a JS
     * property (same shape as React's prop, defaults to the built-in
     * Navigation/Actions/General groups). `trigger` / `footer` default to true
     * (`trigger="false"` swaps in your own trigger via the `trigger` slot —
     * React's `triggerSlot`; `footer="false"` drops the Esc hint). `open` is
     * React's controlled `open`; `onOpenChange` is the bubbling `open-change`
     * CustomEvent from the inner <uip-dialog> (detail: { open }).
     */
    export class UipShortcutsDialog extends LitElement {
      static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }
      // :host display can't be a utility class (the host has no template of its own).
      static styles = [tailwind, css`:host { display: block; }`]
    
      static properties = {
        groups: { attribute: false },
        trigger: { type: Boolean, converter: trueByDefault },
        footer: { type: Boolean, converter: trueByDefault },
        open: { type: Boolean },
      }
    
      groups: ShortcutGroup[] = defaultGroups
      trigger = true
      footer = true
      open = false
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'shortcuts-dialog')
      }
    
      render() {
        return html`
          <uip-dialog
            heading="Keyboard shortcuts"
            description="Move faster without leaving the keyboard."
            class="[&::part(content)]:max-w-lg"
            ?open=${this.open}
            @open-change=${(e: CustomEvent<{ open: boolean }>) => {
              this.open = e.detail.open
            }}
          >
            ${this.trigger
              ? html`<uip-button slot="trigger" variant="outline" size="sm">
                  ${icon(Keyboard, 'keyboard')} Shortcuts <uip-kbd>?</uip-kbd>
                </uip-button>`
              : html`<slot name="trigger" slot="trigger"></slot>`}
            <div class="grid max-h-[60vh] gap-x-8 gap-y-6 overflow-y-auto pr-1 sm:grid-cols-2">
              ${this.groups.map(
                (group) => html`
                  <div class="space-y-2">
                    <p class="text-muted-foreground text-xs font-medium tracking-widest uppercase">${group.label}</p>
                    <ul class="divide-y">
                      ${group.shortcuts.map(
                        (shortcut) => html`
                          <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">
                              ${shortcut.keys.map((key) => html`<uip-kbd>${key}</uip-kbd>`)}
                            </span>
                          </li>
                        `,
                      )}
                    </ul>
                  </div>
                `,
              )}
            </div>
            ${this.footer
              ? html`<div slot="footer" class="flex w-full justify-start">
                  <p class="text-muted-foreground flex items-center gap-1.5 text-xs">
                    Press <uip-kbd>Esc</uip-kbd> to close
                  </p>
                </div>`
              : nothing}
          </uip-dialog>
        `
      }
    }
    
    customElements.get('uip-shortcuts-dialog') || customElements.define('uip-shortcuts-dialog', UipShortcutsDialog)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-shortcuts-dialog': UipShortcutsDialog
      }
    }
    

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