| <script lang="ts"> |
| import { getContext } from "svelte"; |
|
|
| import { type KeyRaw, type LayoutKeysGroup, type Key, type MouseMotion } from "@graphite/messages"; |
| import type { FullscreenState } from "@graphite/state-providers/fullscreen"; |
| import type { IconName } from "@graphite/utility-functions/icons"; |
| import { platformIsMac } from "@graphite/utility-functions/platform"; |
|
|
| import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; |
| import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; |
| import Separator from "@graphite/components/widgets/labels/Separator.svelte"; |
| import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; |
|
|
| type LabelData = { label?: string; icon?: IconName; width: string }; |
|
|
| |
| const ICON_WIDTHS_MAC = { |
| Shift: 2, |
| Control: 2, |
| Option: 2, |
| Command: 2, |
| }; |
| const ICON_WIDTHS = { |
| ArrowUp: 1, |
| ArrowRight: 1, |
| ArrowDown: 1, |
| ArrowLeft: 1, |
| Backspace: 2, |
| Enter: 2, |
| Tab: 2, |
| Space: 3, |
| ...(platformIsMac() ? ICON_WIDTHS_MAC : {}), |
| }; |
|
|
| const fullscreen = getContext<FullscreenState>("fullscreen"); |
|
|
| export let keysWithLabelsGroups: LayoutKeysGroup[] = []; |
| export let mouseMotion: MouseMotion | undefined = undefined; |
| export let requiresLock = false; |
| export let textOnly = false; |
|
|
| $: keyboardLockInfoMessage = watchKeyboardLockInfoMessage(fullscreen.keyboardLockApiSupported); |
|
|
| $: displayKeyboardLockNotice = requiresLock && !$fullscreen.keyboardLocked; |
|
|
| function watchKeyboardLockInfoMessage(keyboardLockApiSupported: boolean): string { |
| const RESERVED = "This hotkey is reserved by the browser. "; |
| const USE_FULLSCREEN = "It is made available in fullscreen mode."; |
| const USE_SECURE_CTX = "It is made available in fullscreen mode when this website is served from a secure context (https or localhost)."; |
| const SWITCH_BROWSER = "Use a Chromium-based browser (like Chrome or Edge) in fullscreen mode to directly use the shortcut."; |
|
|
| if (keyboardLockApiSupported) return `${RESERVED} ${USE_FULLSCREEN}`; |
| if (!("chrome" in window)) return `${RESERVED} ${SWITCH_BROWSER}`; |
| if (!window.isSecureContext) return `${RESERVED} ${USE_SECURE_CTX}`; |
| return RESERVED; |
| } |
|
|
| function keyTextOrIconList(keyGroup: LayoutKeysGroup): LabelData[] { |
| return keyGroup.map((key) => keyTextOrIcon(key)); |
| } |
|
|
| function keyTextOrIcon(keyWithLabel: Key): LabelData { |
| |
| let key = keyWithLabel.key; |
| const label = keyWithLabel.label; |
|
|
| |
| if (platformIsMac()) { |
| if (key === "Alt") key = "Option"; |
| if (key === "Accel") key = "Command"; |
| } |
|
|
| |
| |
| const iconWidth: number | undefined = ICON_WIDTHS[key]; |
| const icon = iconWidth !== undefined && iconWidth > 0 && (keyboardHintIcon(key) || false); |
| if (icon) return { icon, width: `width-${iconWidth}` }; |
|
|
| |
| return { label, width: `width-${label.length}` }; |
| } |
|
|
| function mouseHintIcon(input?: MouseMotion): IconName { |
| return `MouseHint${input}` as IconName; |
| } |
|
|
| function keyboardHintIcon(input: KeyRaw): IconName | undefined { |
| switch (input) { |
| case "ArrowDown": |
| return "KeyboardArrowDown"; |
| case "ArrowLeft": |
| return "KeyboardArrowLeft"; |
| case "ArrowRight": |
| return "KeyboardArrowRight"; |
| case "ArrowUp": |
| return "KeyboardArrowUp"; |
| case "Backspace": |
| return "KeyboardBackspace"; |
| case "Command": |
| return "KeyboardCommand"; |
| case "Control": |
| return "KeyboardControl"; |
| case "Enter": |
| return "KeyboardEnter"; |
| case "Option": |
| return "KeyboardOption"; |
| case "Shift": |
| return "KeyboardShift"; |
| case "Space": |
| return "KeyboardSpace"; |
| case "Tab": |
| return "KeyboardTab"; |
| default: |
| return undefined; |
| } |
| } |
| </script> |
|
|
| {#if displayKeyboardLockNotice} |
| <IconLabel class="user-input-label keyboard-lock-notice" icon="Info" tooltip={keyboardLockInfoMessage} /> |
| {:else} |
| <LayoutRow class="user-input-label" classes={{ "text-only": textOnly }}> |
| {#each keysWithLabelsGroups as keysWithLabels, groupIndex} |
| {#if groupIndex > 0} |
| <Separator type="Related" /> |
| {/if} |
| {#each keyTextOrIconList(keysWithLabels) as keyInfo} |
| <div class={`input-key ${keyInfo.width}`}> |
| {#if keyInfo.icon} |
| <IconLabel icon={keyInfo.icon} /> |
| {:else if keyInfo.label !== undefined} |
| <TextLabel>{keyInfo.label}</TextLabel> |
| {/if} |
| </div> |
| {/each} |
| {/each} |
| {#if mouseMotion} |
| <div class="input-mouse"> |
| <IconLabel icon={mouseHintIcon(mouseMotion)} /> |
| </div> |
| {/if} |
| {#if $$slots.default} |
| <div class="hint-text"> |
| <slot /> |
| </div> |
| {/if} |
| </LayoutRow> |
| {/if} |
|
|
| <style lang="scss" global> |
| .user-input-label { |
| flex: 0 0 auto; |
| align-items: center; |
| white-space: nowrap; |
|
|
| &.text-only { |
| display: flex; |
|
|
| .input-key { |
| display: flex; |
| align-items: center; |
|
|
| .icon-label { |
| margin: calc(calc(18px - 12px) / 2) 0; |
| } |
|
|
| & + .input-key { |
| margin-left: 4px; |
| } |
| } |
| } |
|
|
| &:not(.text-only) { |
| .input-key, |
| .input-mouse { |
| & + .input-key, |
| & + .input-mouse { |
| margin-left: 2px; |
| } |
| } |
|
|
| .input-key { |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| font-family: "Inconsolata", monospace; |
| font-weight: 400; |
| text-align: center; |
| height: 16px; |
| box-sizing: border-box; |
| border: 1px solid; |
| border-radius: 4px; |
| border-color: var(--color-5-dullgray); |
| color: var(--color-e-nearwhite); |
|
|
| .text-label { |
| |
| |
| line-height: 15px; |
| } |
|
|
| &.width-1 { |
| width: 16px; |
| } |
|
|
| &.width-2 { |
| width: 24px; |
| } |
|
|
| &.width-3 { |
| width: 32px; |
| } |
|
|
| &.width-4 { |
| width: 40px; |
| } |
|
|
| &.width-5 { |
| width: 48px; |
| } |
|
|
| .icon-label { |
| margin: 1px; |
| } |
| } |
| } |
|
|
| .input-mouse { |
| .bright { |
| fill: var(--color-e-nearwhite); |
| } |
|
|
| .dim { |
| fill: var(--color-8-uppergray); |
| } |
| } |
|
|
| .hint-text:not(:empty) { |
| margin-left: 4px; |
| } |
|
|
| .floating-menu-content .row > & { |
| .input-key { |
| border-color: var(--color-3-darkgray); |
| color: var(--color-8-uppergray); |
| } |
|
|
| .input-key .icon-label svg, |
| &.keyboard-lock-notice.keyboard-lock-notice svg, |
| .input-mouse .bright { |
| fill: var(--color-8-uppergray); |
| } |
|
|
| .input-mouse .dim { |
| fill: var(--color-3-darkgray); |
| } |
| } |
|
|
| .floating-menu-content .row:hover > & { |
| .input-key { |
| border-color: var(--color-8-uppergray); |
| } |
|
|
| .input-mouse .dim { |
| fill: var(--color-8-uppergray); |
| } |
| } |
| } |
| </style> |
|
|