import { html, css, TemplateResult, CSSResultArray, PropertyValues, nothing } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import NileElement from '../../internal/nile-element'; const isMac = typeof navigator !== 'undefined' && /Mac|iP(hone|[oa]d)/.test(navigator.platform); /** Renders a binding like 'Mod-B' with platform-appropriate key labels. */ function keyLabel(binding: string): string { return binding .split('-') .map(part => { switch (part) { case 'Mod': return isMac ? '⌘' : 'Ctrl'; case 'Shift': return isMac ? '⇧' : 'Shift'; case 'Alt': return isMac ? '⌥' : 'Alt'; case 'Ctrl': return isMac ? '⌃' : 'Ctrl'; default: return part; } }) .join(isMac ? '' : '+'); } interface ShortcutGroup { title: string; items: { keys: string[]; label: string }[]; } const GROUPS: ShortcutGroup[] = [ { title: 'Formatting', items: [ { keys: ['Mod-B'], label: 'Bold' }, { keys: ['Mod-I'], label: 'Italic' }, { keys: ['Mod-U'], label: 'Underline' }, { keys: ['Mod-Shift-X'], label: 'Strikethrough' }, { keys: ['Mod-,'], label: 'Subscript' }, { keys: ['Mod-.'], label: 'Superscript' }, ], }, { title: 'Blocks', items: [ { keys: ['Ctrl-Shift-0'], label: 'Paragraph' }, { keys: ['Ctrl-Shift-1…6'], label: 'Heading 1–6' }, { keys: ['Mod-Shift-8'], label: 'Bulleted list' }, { keys: ['Mod-Shift-7'], label: 'Numbered list' }, { keys: ['Mod-Shift-9'], label: 'Blockquote' }, { keys: ['Mod-Alt-C'], label: 'Code block' }, { keys: ['Tab', 'Shift-Tab'], label: 'Indent / outdent list item' }, { keys: ['Mod-]', 'Mod-['], label: 'Indent / outdent block' }, { keys: ['Shift-Enter'], label: 'Line break' }, ], }, { title: 'Editing & tools', items: [ { keys: ['Mod-Z'], label: 'Undo' }, { keys: ['Mod-Shift-Z'], label: 'Redo' }, { keys: ['Mod-K'], label: 'Insert or edit link' }, { keys: ['Mod-F'], label: 'Find & replace' }, { keys: ['Mod-Shift-V'], label: 'Paste without formatting' }, { keys: ['Mod-/'], label: 'This dialog' }, ], }, { title: 'Typing shortcuts', items: [ { keys: ['# + space'], label: 'Heading (## for level 2 …)' }, { keys: ['- + space'], label: 'Bulleted list' }, { keys: ['1. + space'], label: 'Numbered list' }, { keys: ['[] + space'], label: 'Checklist' }, { keys: ['> + space'], label: 'Blockquote' }, { keys: ['```'], label: 'Code block' }, { keys: ['**text**'], label: 'Bold as you type' }, { keys: ['URL + space'], label: 'Automatic link' }, { keys: ['/'], label: 'Command menu' }, { keys: [': + name'], label: 'Emoji picker' }, ], }, ]; /** * Keyboard shortcut reference dialog, opened from the toolbar or Mod-/. * Emits `wysiwyg-shortcut-help-close` when dismissed (Esc, ✕ or backdrop). */ @customElement('nile-wysiwyg-shortcut-help') export class NileWysiwygShortcutHelp extends NileElement { @property({ type: Boolean, reflect: true }) open = false; @query('.help__panel') private panelEl?: HTMLElement; /** Body overflow value before we locked it; null = not locked by us. */ private previousBodyOverflow: string | null = null; public static get styles(): CSSResultArray { return [ css` /* Fixed so the dialog overlays the viewport instead of being clipped by a small editor. */ :host { font-family: var( --nile-font-family-sans-serif, var(--ng-font-family-body, system-ui, -apple-system, sans-serif) ); position: fixed; inset: 0; z-index: var(--nile-wysiwyg-dialog-z-index, 1100); display: none; } :host([open]) { display: flex; align-items: center; justify-content: center; } .help__backdrop { position: absolute; inset: 0; background: rgba(16, 24, 40, 0.4); } .help__panel { position: relative; max-width: 560px; max-height: min(80%, 480px); margin: 16px; padding: 16px 20px; overflow-y: auto; overscroll-behavior: contain; outline: none; background: var(--nile-wysiwyg-popover-bg, #ffffff); border: 1px solid var(--nile-colors-neutral-400, #d0d5dd); border-radius: 10px; box-shadow: var( --nile-wysiwyg-popover-shadow, 0 8px 24px rgba(16, 24, 40, 0.18) ); font-size: 13px; color: var(--nile-colors-neutral-700, #344054); } .help__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .help__title { font-size: 14px; font-weight: 600; color: var(--nile-colors-dark-900, #101828); } .help__close { font: inherit; border: none; background: transparent; border-radius: 5px; padding: 4px 8px; cursor: pointer; color: var(--nile-colors-neutral-700, #344054); } .help__close:hover { background: var(--nile-colors-neutral-100, #f2f4f7); } .help__columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 4px 24px; } .help__group-title { margin: 10px 0 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--nile-colors-neutral-500, #98a2b3); } .help__row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 3px 0; } .help__keys { white-space: nowrap; } kbd { display: inline-block; padding: 1px 6px; font-family: inherit; font-size: 12px; line-height: 1.5; background: var(--nile-colors-neutral-100, #f2f4f7); border: 1px solid var(--nile-colors-neutral-400, #d0d5dd); border-bottom-width: 2px; border-radius: 5px; } `, ]; } /** * Locks page scrolling while the dialog is open (the fixed backdrop blocks * clicks but not wheel/touch scrolling of the document behind it). * Save/restore keeps it composable with other locks (e.g. fullscreen). */ protected updated(changed: PropertyValues): void { if (!changed.has('open')) return; if (this.open) { if (this.previousBodyOverflow === null) { this.previousBodyOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; } } else if (this.previousBodyOverflow !== null) { document.body.style.overflow = this.previousBodyOverflow; this.previousBodyOverflow = null; } } disconnectedCallback(): void { super.disconnectedCallback(); if (this.previousBodyOverflow !== null) { document.body.style.overflow = this.previousBodyOverflow; this.previousBodyOverflow = null; } } /** Moves focus into the dialog so Esc lands here. */ public focusPanel(): void { this.updateComplete.then(() => this.panelEl?.focus()); } private close(): void { this.emit('wysiwyg-shortcut-help-close'); } private handleKeydown(event: KeyboardEvent): void { if (event.key === 'Escape') { event.stopPropagation(); this.close(); } } public render(): TemplateResult | typeof nothing { if (!this.open) return nothing; return html`
`; } } declare global { interface HTMLElementTagNameMap { 'nile-wysiwyg-shortcut-help': NileWysiwygShortcutHelp; } }