[READ-ONLY] Mirror of https://github.com/flo-bit/ui-kit. 🦊 fox ui, svelte 5 and tailwind 4 flo-bit.dev/ui-kit/
svelte tailwindcss ui-components
0

Configure Feed

Select the types of activity you want to include in your feed.

hotkeys

Florian (Oct 23, 2025, 3:10 AM +0200) c8cc5d32 4c93c077

+205 -7
+4 -2
apps/docs/src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import '../app.css'; 3 3 4 - import { Head, Toaster } from '@foxui/all'; 4 + import { Head, Hotkeys, Toaster } from '@foxui/all'; 5 5 import { base } from '$app/paths'; 6 6 import { dev } from '$app/environment'; 7 7 ··· 46 46 alt="fox ui" 47 47 class="pointer-events-none fixed inset-0 -z-50 h-full w-full object-cover brightness-100 filter" 48 48 /> 49 - {/if} 49 + {/if} 50 + 51 + <Hotkeys />
+1
packages/core/src/lib/components/index.ts
··· 32 32 export * from './toggle'; 33 33 export * from './toggle-group'; 34 34 export * from './tooltip'; 35 + export * from './hotkeys';
+19
packages/core/src/lib/components/button/Button.svelte
··· 66 66 variant?: ButtonVariant; 67 67 size?: ButtonSize; 68 68 checkCurrent?: boolean; 69 + 70 + hotkey?: string; 71 + showHotkey?: boolean; 69 72 }; 70 73 </script> 71 74 ··· 78 81 href = undefined, 79 82 type = 'button', 80 83 children, 84 + hotkey, 85 + showHotkey, 81 86 ...restProps 82 87 }: ButtonProps = $props(); 83 88 </script> ··· 87 92 bind:this={ref} 88 93 class={cn('rounded-box', buttonVariants({ variant, size }), className)} 89 94 {href} 95 + aria-keyshortcuts={hotkey} 90 96 {...restProps} 91 97 > 92 98 {@render children?.()} 99 + 100 + {#if showHotkey && hotkey} 101 + <span class="text-accent-300 border-accent-300/10 rounded-md border px-1 py-0.5 text-xs" 102 + >{hotkey}</span 103 + > 104 + {/if} 93 105 </a> 94 106 {:else} 95 107 <button 96 108 bind:this={ref} 97 109 class={cn('rounded-box', buttonVariants({ variant, size }), className)} 98 110 {type} 111 + aria-keyshortcuts={hotkey} 99 112 {...restProps} 100 113 > 101 114 {@render children?.()} 115 + 116 + {#if showHotkey && hotkey} 117 + <span class="text-accent-300 border-accent-300/10 rounded-md border px-1 py-0.5 text-xs" 118 + >{hotkey}</span 119 + > 120 + {/if} 102 121 </button> 103 122 {/if}
+168
packages/core/src/lib/components/hotkeys/Hotkeys.svelte
··· 1 + <script lang="ts" module> 2 + export let hotkeys: Record<string, () => void> = $state({}); 3 + 4 + export let hotkeysState: { 5 + enabled: boolean; 6 + } = $state({ 7 + enabled: true 8 + }); 9 + </script> 10 + 11 + <script lang="ts"> 12 + import { onMount } from 'svelte'; 13 + 14 + let NON_ENGLISH_LAYOUT = /^[^\x00-\x7F]$/; 15 + 16 + let IGNORE_INPUTS = { 17 + checkbox: true, 18 + file: true, 19 + radio: true 20 + }; 21 + 22 + let CLICK_INPUTS = { 23 + button: true, 24 + reset: true, 25 + submit: true 26 + }; 27 + 28 + let KEY_REPLACERS = { 29 + ' ': 'space', 30 + '+': 'plus' 31 + }; 32 + 33 + function isInsideIgnored(parent: HTMLElement, node: HTMLElement) { 34 + if (node.tagName !== 'BODY' && parent !== node) { 35 + if ( 36 + node.hasAttribute('data-keyux-ignore-hotkeys') || 37 + node.getAttribute('aria-hidden') === 'true' || 38 + node.hasAttribute('inert') 39 + ) { 40 + return true; 41 + } else { 42 + return isInsideIgnored(parent, node.parentNode as HTMLElement); 43 + } 44 + } 45 + } 46 + 47 + function findNonIgnored(activeElement: HTMLElement, elements: HTMLElement[]) { 48 + for (let element of elements) { 49 + if (isInsideIgnored(activeElement, element)) continue; 50 + return element; 51 + } 52 + } 53 + 54 + function checkHotkey(window: Window, code: string) { 55 + let realCode = code; 56 + 57 + let where = window.document; 58 + let activeElement = where.activeElement; 59 + 60 + let areaId = activeElement?.getAttribute('data-keyux-hotkeys'); 61 + if (areaId) { 62 + let area = where.querySelector(`#${areaId}`); 63 + if (area) { 64 + let element = area.querySelector(`[aria-keyshortcuts="${realCode}" i]`); 65 + if (element) return element; 66 + } 67 + } 68 + 69 + return ( 70 + findNonIgnored( 71 + activeElement as HTMLElement, 72 + activeElement?.querySelectorAll( 73 + `[aria-keyshortcuts="${realCode}" i]` 74 + ) as unknown as HTMLElement[] 75 + ) || 76 + findNonIgnored( 77 + where as unknown as HTMLElement, 78 + where?.querySelectorAll(`[aria-keyshortcuts="${realCode}" i]`) as unknown as HTMLElement[] 79 + ) 80 + ); 81 + } 82 + 83 + function findHotKey(event: KeyboardEvent, window: Window) { 84 + let prefix = ''; 85 + if (event.metaKey) prefix += 'meta+'; 86 + if (event.ctrlKey) prefix += 'ctrl+'; 87 + if (event.altKey) prefix += 'alt+'; 88 + if (event.shiftKey) prefix += 'shift+'; 89 + 90 + let code = prefix; 91 + code += KEY_REPLACERS[event.key as keyof typeof KEY_REPLACERS] ?? event.key.toLowerCase(); 92 + 93 + let hotkey = checkHotkey(window, code); 94 + if ( 95 + !hotkey && 96 + (event.key.length > 1 || NON_ENGLISH_LAYOUT.test(event.key)) && 97 + /^(Key.|Digit\d)$/.test(event.code) 98 + ) { 99 + let enKey = event.code.replace(/^Key|^Digit/, '').toLowerCase(); 100 + hotkey = checkHotkey(window, prefix + enKey); 101 + } 102 + 103 + return hotkey; 104 + } 105 + 106 + // function findHotKey(event: KeyboardEvent) { 107 + // let prefix = ''; 108 + // if (event.metaKey) prefix += 'meta+'; 109 + // if (event.ctrlKey) prefix += 'ctrl+'; 110 + // if (event.altKey) prefix += 'alt+'; 111 + // if (event.shiftKey) prefix += 'shift+'; 112 + 113 + // let code = prefix; 114 + // code += event.key.toLowerCase(); 115 + 116 + // let hotkey = hotkeys[code]; 117 + // if ( 118 + // !hotkey && 119 + // (event.key.length > 1 || NON_ENGLISH_LAYOUT.test(event.key)) && 120 + // /^(Key.|Digit\d)$/.test(event.code) 121 + // ) { 122 + // let enKey = event.code.replace(/^Key|^Digit/, '').toLowerCase(); 123 + // hotkey = hotkeys[prefix + enKey]; 124 + // } 125 + 126 + // return hotkey; 127 + // } 128 + 129 + function handleKeyDown(event: KeyboardEvent) { 130 + if (!hotkeysState.enabled) { 131 + return; 132 + } 133 + 134 + let isSpecialKey = event.ctrlKey || event.metaKey || event.altKey; 135 + let insideEditable = 136 + (event.target as HTMLElement)?.isContentEditable || 137 + (event.target as HTMLElement)?.tagName === 'TEXTAREA' || 138 + ((event.target as HTMLElement)?.tagName === 'INPUT' && 139 + !IGNORE_INPUTS[(event.target as HTMLInputElement)?.type as keyof typeof IGNORE_INPUTS]); 140 + let insideFocusGroup = (event.target as HTMLElement)?.role === 'menuitem'; 141 + if (!isSpecialKey && (insideEditable || insideFocusGroup)) { 142 + return; 143 + } 144 + 145 + let active = findHotKey(event, window); 146 + if (!active) return; 147 + if ( 148 + active.tagName === 'TEXTAREA' || 149 + (active.tagName === 'INPUT' && !CLICK_INPUTS[active.type]) 150 + ) { 151 + setTimeout(() => { 152 + (active as HTMLElement).focus(); 153 + }); 154 + } else { 155 + (active as HTMLElement).click(); 156 + } 157 + 158 + // let hotkey = findHotKey(event); 159 + // hotkey?.(); 160 + } 161 + onMount(() => { 162 + window.addEventListener('keydown', handleKeyDown); 163 + 164 + return () => { 165 + window.removeEventListener('keydown', handleKeyDown); 166 + }; 167 + }); 168 + </script>
+4
packages/core/src/lib/components/hotkeys/helpers.ts
··· 1 + export function likelyWithKeyboard(window = globalThis) { 2 + const agent = window.navigator.userAgent.toLowerCase(); 3 + return !['iphone', 'ipad', 'android'].some((device) => agent.includes(device)); 4 + }
+1
packages/core/src/lib/components/hotkeys/index.ts
··· 1 + export { default as Hotkeys } from './Hotkeys.svelte';
+8 -5
apps/docs/src/routes/(main)/components/core/button/Example.svelte
··· 4 4 5 5 <h3>Primary</h3> 6 6 7 - <div class="flex items-center gap-2 flex-wrap"> 8 - <Button size="sm" 7 + <div class="flex flex-wrap items-center gap-2"> 8 + <Button 9 + size="sm" 10 + hotkey="s" 11 + showHotkey 12 + onclick={() => { 13 + console.log('s'); 14 + }} 9 15 ><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> 10 16 <path 11 17 d="M11.7 2.805a.75.75 0 0 1 .6 0A60.65 60.65 0 0 1 22.83 8.72a.75.75 0 0 1-.231 1.337 49.948 49.948 0 0 0-9.902 3.912l-.003.002c-.114.06-.227.119-.34.18a.75.75 0 0 1-.707 0A50.88 50.88 0 0 0 7.5 12.173v-.224c0-.131.067-.248.172-.311a54.615 54.615 0 0 1 4.653-2.52.75.75 0 0 0-.65-1.352 56.123 56.123 0 0 0-4.78 2.589 1.858 1.858 0 0 0-.859 1.228 49.803 49.803 0 0 0-4.634-1.527.75.75 0 0 1-.231-1.337A60.653 60.653 0 0 1 11.7 2.805Z" ··· 47 53 </svg>large</Button 48 54 > 49 55 50 - 51 56 <Button disabled 52 57 ><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> 53 58 <path ··· 61 66 /> 62 67 </svg>disabled</Button 63 68 > 64 - 65 69 </div> 66 - 67 70 68 71 <h3>Secondary</h3> 69 72