mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-10 13:18:25 +00:00
feat(site): add keyboard shortcuts to the icons page (#5035)
* feat(docs): add keyboard shortcuts to the icons page Add the shortcuts suggested in #4916 and list them on the page: - Ctrl/Cmd + click on an icon copies its name - Arrow keys move from the search field into the results and between the icons - Ctrl/Cmd + C copies the name of the selected icon - Ctrl/Cmd + Shift + C copies the SVG of the selected icon Icons can now be focused with the keyboard, and a keyboard button next to the sort select opens a dialog with all shortcuts, including the existing Ctrl/Cmd + K and Shift + click. The dialog also opens with Shift + ? (except while typing in a field). Closes #4916 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(docs): add copy toasts, open icons with Enter and a shared Kbd component - Copying an icon shows a toast (vue-sonner, bottom-center) that says whether its name or its SVG was copied - Enter opens the selected icon's drawer - The SVG shortcut is now Shift + C, as Ctrl/Cmd + Shift + C opens the browser's inspect tool - Kbd component shared by the search inputs and the shortcuts dialog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Adjust keyboard shortcuts * Revert customizer change * Adjust kbd style * Fix toaster styling * Fix types --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Eric <eric.fennis@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Eric
parent
3fef7aca71
commit
bfa381ef26
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import { search } from '../../../data/iconNodes';
|
||||
import Kbd from './Kbd.vue';
|
||||
|
||||
defineProps({
|
||||
shortcut: {
|
||||
@@ -17,10 +18,10 @@ defineProps({
|
||||
class="search-icon"
|
||||
/>
|
||||
<slot />
|
||||
<kbd
|
||||
<Kbd
|
||||
v-if="shortcut"
|
||||
class="shortcut"
|
||||
>{{ shortcut }}</kbd
|
||||
>{{ shortcut }}</Kbd
|
||||
>
|
||||
</button>
|
||||
</template>
|
||||
@@ -59,7 +60,9 @@ defineProps({
|
||||
|
||||
.search-icon {
|
||||
flex-shrink: 0;
|
||||
transition: color 0.25s, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transition:
|
||||
color 0.25s,
|
||||
transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transition-duration: 0.25s, 0.3s;
|
||||
}
|
||||
|
||||
@@ -70,15 +73,6 @@ defineProps({
|
||||
|
||||
.shortcut {
|
||||
margin-left: auto;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-3);
|
||||
background: var(--vp-c-default-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ref, onMounted, nextTick, watch } from 'vue';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import { x } from '../../../data/iconNodes';
|
||||
import Button from './Button.vue';
|
||||
import Kbd from './Kbd.vue';
|
||||
|
||||
const props = withDefaults(defineProps<InputProps>(), {
|
||||
type: 'text',
|
||||
@@ -29,7 +30,7 @@ const emit = defineEmits(['change', 'input', 'update:modelValue']);
|
||||
const updateShortcutSpacing = () => {
|
||||
nextTick(() => {
|
||||
if (shortcutEl.value && wrapperEl.value) {
|
||||
const shortcutWidth = shortcutEl.value.offsetWidth;
|
||||
const shortcutWidth = shortcutEl.value.$el.offsetWidth;
|
||||
wrapperEl.value.style.setProperty('--shortcut-width', `${shortcutWidth}px`);
|
||||
}
|
||||
});
|
||||
@@ -83,11 +84,11 @@ defineExpose({
|
||||
:size="20"
|
||||
/>
|
||||
</Button>
|
||||
<kbd
|
||||
<Kbd
|
||||
v-if="shortcut"
|
||||
class="shortcut"
|
||||
ref="shortcutEl"
|
||||
>{{ shortcut }}</kbd
|
||||
>{{ shortcut }}</Kbd
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
@@ -146,33 +147,24 @@ defineExpose({
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-3);
|
||||
background: var(--vp-c-default-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.input[type="search"]::-webkit-search-cancel-button {
|
||||
.input[type='search']::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.input[type="search"]::-ms-clear {
|
||||
.input[type='search']::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.input[type="search"]::-o-clear {
|
||||
.input[type='search']::-o-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.input[type="search"]::-moz-clear {
|
||||
.input[type='search']::-moz-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -195,7 +187,9 @@ defineExpose({
|
||||
z-index: 1;
|
||||
color: var(--vp-c-text-2);
|
||||
pointer-events: none;
|
||||
transition: color 0.25s, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transition:
|
||||
color 0.25s,
|
||||
transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transition-duration: 0.25s, 0.3s;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
id: string
|
||||
}>()
|
||||
label: string;
|
||||
id: string;
|
||||
class?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-field">
|
||||
<div class="input-label" v-if="label">
|
||||
<label :for="id" class="customize-label">
|
||||
<div
|
||||
class="input-field"
|
||||
:class="class"
|
||||
>
|
||||
<div
|
||||
class="input-label"
|
||||
v-if="label"
|
||||
>
|
||||
<label
|
||||
:for="id"
|
||||
class="customize-label"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<div class="display-value" >
|
||||
<slot name="display"/>
|
||||
<div class="display-value">
|
||||
<slot name="display" />
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
@@ -25,7 +35,7 @@ defineProps<{
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vt-c-text-1);
|
||||
transition: color .5s;
|
||||
transition: color 0.5s;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -43,6 +53,6 @@ defineProps<{
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
gap: 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<kbd class="kbd"><slot /></kbd>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.kbd {
|
||||
display: inline-block;
|
||||
min-width: 24px;
|
||||
padding: 2px 6px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-2);
|
||||
background: var(--vp-c-default-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { Toaster } from 'vue-sonner';
|
||||
import 'vue-sonner/style.css';
|
||||
import { useData } from 'vitepress';
|
||||
import Icon from '@lucide/vue/src/Icon.ts';
|
||||
import { check } from '../../../data/iconNodes';
|
||||
|
||||
const { isDark } = useData();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<Toaster
|
||||
position="bottom-center"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
>
|
||||
<template #success-icon>
|
||||
<Icon
|
||||
:iconNode="check"
|
||||
name="check"
|
||||
:size="24"
|
||||
/>
|
||||
</template>
|
||||
</Toaster>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Same surface as base/Card.vue. The extra :root outranks sonner's per-theme rules */
|
||||
:root [data-sonner-toaster][data-sonner-theme] {
|
||||
--normal-bg: var(--vp-c-bg-soft);
|
||||
--normal-border: var(--vp-c-divider);
|
||||
--normal-text: var(--vp-c-text-1);
|
||||
--border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Keep the icon and the text on one centred line; the site's line-height pushes the text down */
|
||||
[data-sonner-toast] {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-sonner-toast] [data-icon] {
|
||||
align-self: center;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Sonner's icon box is 16px, so match the 24px check to stop it overflowing into the text */
|
||||
[data-sonner-toast][data-styled='true'][data-type='success'] [data-icon] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--vp-c-success-1);
|
||||
}
|
||||
|
||||
[data-sonner-toast] [data-content],
|
||||
[data-sonner-toast] [data-title] {
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { computed } from 'vue';
|
||||
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { useRouter } from 'vitepress';
|
||||
import { toast } from 'vue-sonner';
|
||||
import getSVGIcon from '../../utils/getSVGIcon';
|
||||
import useConfetti from '../../composables/useConfetti';
|
||||
import Tooltip from '../base/Tooltip.vue';
|
||||
@@ -38,17 +39,37 @@ const href = computed(() =>
|
||||
props.externalLibrary ? `/icons/${props.externalLibrary}/${props.name}` : `/icons/${props.name}`,
|
||||
);
|
||||
|
||||
// The toast says what was copied, since the name and the SVG are copied the same way
|
||||
async function copyToClipboard(text: string, confettiMessage: string, toastMessage: string) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
||||
confettiText.value = confettiMessage;
|
||||
confetti();
|
||||
toast.success(toastMessage);
|
||||
}
|
||||
|
||||
function copyName() {
|
||||
return copyToClipboard(props.name, `Copied name`, `Copied the name of ${props.name}`);
|
||||
}
|
||||
|
||||
function copySVG(iconButton: HTMLElement) {
|
||||
const svgString = getSVGIcon(iconButton.querySelector('svg'), {
|
||||
class: `lucide lucide-${props.name}`,
|
||||
});
|
||||
|
||||
return copyToClipboard(svgString, `Copied svg`, `Copied the SVG of ${props.name}`);
|
||||
}
|
||||
|
||||
async function navigateToIcon(event) {
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
const svgString = getSVGIcon(event.target.firstChild, {
|
||||
class: `lucide lucide-${props.name}`,
|
||||
});
|
||||
await copySVG(event.currentTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(svgString);
|
||||
|
||||
confettiText.value = copiedText;
|
||||
confetti();
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
await copyName();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,6 +97,34 @@ async function navigateToIcon(event) {
|
||||
}
|
||||
}
|
||||
|
||||
// On a selected (focused) icon: Enter opens it, Ctrl/Cmd + C copies its name, Shift + C its SVG.
|
||||
// (Ctrl/Cmd + Shift + C is the browser's inspect shortcut, so it can't be used for the SVG.)
|
||||
async function handleKeydown(event: KeyboardEvent) {
|
||||
if (
|
||||
event.key === 'Enter' &&
|
||||
!(event.shiftKey || event.ctrlKey || event.metaKey || event.altKey)
|
||||
) {
|
||||
await navigateToIcon(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() !== 'c' || event.altKey) return;
|
||||
|
||||
if (event.shiftKey && !(event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
await copySVG(event.currentTarget as HTMLElement);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(event.ctrlKey || event.metaKey) || event.shiftKey) return;
|
||||
|
||||
// Leave a normal copy alone when some text on the page is selected
|
||||
if (window.getSelection()?.toString()) return;
|
||||
|
||||
event.preventDefault();
|
||||
await copyName();
|
||||
}
|
||||
|
||||
const DiamondIcon = createLucideIcon('Diamond', diamond);
|
||||
</script>
|
||||
|
||||
@@ -83,7 +132,9 @@ const DiamondIcon = createLucideIcon('Diamond', diamond);
|
||||
<Tooltip :title="name">
|
||||
<a
|
||||
class="icon-button confetti-button vp-raw"
|
||||
tabindex="0"
|
||||
@click="navigateToIcon"
|
||||
@keydown="handleKeydown"
|
||||
:class="{ active, animate }"
|
||||
:aria-label="name"
|
||||
:data-confetti-text="confettiText"
|
||||
@@ -186,10 +237,15 @@ const DiamondIcon = createLucideIcon('Diamond', diamond);
|
||||
background-color: var(--vp-button-alt-active-bg);
|
||||
}
|
||||
|
||||
.icon-button.active {
|
||||
.icon-button.active,
|
||||
.icon-button:focus-visible {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.icon-button:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.lucide-icon {
|
||||
margin: auto;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -208,6 +208,43 @@ function handleCloseDrawer() {
|
||||
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
const ARROW_STEPS = {
|
||||
ArrowLeft: () => -1,
|
||||
ArrowRight: () => 1,
|
||||
ArrowUp: () => -columnSize.value,
|
||||
ArrowDown: () => columnSize.value,
|
||||
};
|
||||
|
||||
// Arrow keys move through the search results: from the search field down into the grid, then
|
||||
// between the icons. The grid shows `columnSize` icons per row, so up and down skip a whole row.
|
||||
function handleArrowKeys(event: KeyboardEvent) {
|
||||
const step = ARROW_STEPS[event.key]?.();
|
||||
if (step === undefined || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
|
||||
return;
|
||||
|
||||
const iconButtons = Array.from(
|
||||
overviewEl.value?.querySelectorAll<HTMLElement>('.icons .icon-button') ?? [],
|
||||
);
|
||||
const target = event.target as HTMLElement;
|
||||
const fromSearch = target.tagName === 'INPUT' && target.closest('.input-wrapper') !== null;
|
||||
|
||||
let next: HTMLElement | undefined;
|
||||
if (fromSearch) {
|
||||
if (event.key !== 'ArrowDown') return;
|
||||
next = iconButtons[0];
|
||||
} else {
|
||||
const index = iconButtons.indexOf(target);
|
||||
if (index === -1) return;
|
||||
next = iconButtons[index + step];
|
||||
}
|
||||
|
||||
if (next) {
|
||||
event.preventDefault();
|
||||
next.focus();
|
||||
next.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -215,6 +252,7 @@ function handleCloseDrawer() {
|
||||
ref="overviewEl"
|
||||
class="overview-container"
|
||||
:class="{ 'icon-drawer-open': activeIconName }"
|
||||
@keydown="handleArrowKeys"
|
||||
>
|
||||
<StickyBar>
|
||||
<InputSearch
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import Kbd from '../base/Kbd.vue';
|
||||
import { x } from '~/.vitepress/data/iconNodes';
|
||||
|
||||
const dialog = ref<HTMLDialogElement | null>(null);
|
||||
const isClosing = ref(false);
|
||||
const modifierKey = ref('Ctrl');
|
||||
|
||||
const shortcuts = [
|
||||
{ keys: ['mod', 'K'], alternative: ['/'], description: 'Search' },
|
||||
{ keys: ['Shift', 'Click'], description: 'Copy the SVG of an icon' },
|
||||
{ keys: ['mod', 'Click'], description: 'Copy the name of an icon' },
|
||||
{ keys: ['Arrow keys'], description: 'Move through the search results' },
|
||||
{ keys: ['Enter'], description: 'Open the selected icon' },
|
||||
{ keys: ['mod', 'C'], description: 'Copy the name of the selected icon' },
|
||||
{ keys: ['Shift', 'C'], description: 'Copy the SVG of the selected icon' },
|
||||
{ keys: ['Shift', '?'], description: 'Show keyboard shortcuts' },
|
||||
];
|
||||
|
||||
function keyLabel(key: string) {
|
||||
return key === 'mod' ? modifierKey.value : key;
|
||||
}
|
||||
|
||||
function open() {
|
||||
dialog.value?.showModal();
|
||||
// Focus the dialog itself rather than its close button, so the button is not shown as selected
|
||||
dialog.value?.focus();
|
||||
}
|
||||
|
||||
// The dialog leaves the top layer as soon as it closes, so play the exit transition first
|
||||
async function close() {
|
||||
const element = dialog.value;
|
||||
|
||||
if (!element?.open || isClosing.value) return;
|
||||
|
||||
isClosing.value = true;
|
||||
await nextTick();
|
||||
await Promise.allSettled(element.getAnimations().map((animation) => animation.finished));
|
||||
element.close();
|
||||
isClosing.value = false;
|
||||
}
|
||||
|
||||
function handleDialogClick(event: MouseEvent) {
|
||||
// A click on the backdrop lands on the dialog element itself
|
||||
if (event.target === dialog.value) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function isTyping(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement | null;
|
||||
|
||||
return (
|
||||
target?.isContentEditable ||
|
||||
target?.tagName === 'INPUT' ||
|
||||
target?.tagName === 'TEXTAREA' ||
|
||||
target?.tagName === 'SELECT'
|
||||
);
|
||||
}
|
||||
|
||||
// The ? key opens the dialog, except while typing in a field
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === '?' && !event.ctrlKey && !event.metaKey && !event.altKey && !isTyping(event)) {
|
||||
event.preventDefault();
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
modifierKey.value = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl';
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="keyboard-shortcuts-hint"
|
||||
aria-haspopup="dialog"
|
||||
@click="open"
|
||||
>
|
||||
<span class="hint-label">Keyboard shortcuts</span>
|
||||
<span class="hint-keys">
|
||||
<Kbd>Shift</Kbd>
|
||||
<span>+</span>
|
||||
<Kbd>?</Kbd>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<dialog
|
||||
ref="dialog"
|
||||
class="shortcuts-dialog"
|
||||
:class="{ closing: isClosing }"
|
||||
tabindex="-1"
|
||||
aria-labelledby="shortcuts-title"
|
||||
@click="handleDialogClick"
|
||||
@cancel.prevent="close"
|
||||
>
|
||||
<div class="shortcuts-header">
|
||||
<h2 id="shortcuts-title">Keyboard shortcuts</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="shortcuts-close"
|
||||
aria-label="Close"
|
||||
@click="close"
|
||||
>
|
||||
<Icon
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<dl class="shortcuts-list">
|
||||
<div
|
||||
v-for="shortcut in shortcuts"
|
||||
:key="shortcut.description"
|
||||
class="shortcut-row"
|
||||
>
|
||||
<dt class="shortcut-keys">
|
||||
<template
|
||||
v-for="(key, index) in shortcut.keys"
|
||||
:key="key"
|
||||
>
|
||||
<span
|
||||
v-if="index > 0"
|
||||
class="shortcut-plus"
|
||||
>+</span
|
||||
>
|
||||
<Kbd>{{ keyLabel(key) }}</Kbd>
|
||||
</template>
|
||||
<template v-if="shortcut.alternative">
|
||||
<span class="shortcut-or">or</span>
|
||||
<Kbd
|
||||
v-for="key in shortcut.alternative"
|
||||
:key="key"
|
||||
>{{ key }}</Kbd
|
||||
>
|
||||
</template>
|
||||
</dt>
|
||||
<dd class="shortcut-description">{{ shortcut.description }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.keyboard-shortcuts-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
padding: 24px 0px;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
position: sticky;
|
||||
bottom: -96px;
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hint-keys {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.shortcuts-dialog {
|
||||
width: min(480px, calc(100vw - 32px));
|
||||
padding: 20px 24px 24px;
|
||||
color: var(--vp-c-text-1);
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--vp-shadow-4);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.shortcuts-dialog::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.shortcuts-dialog[open] {
|
||||
transition:
|
||||
opacity 150ms ease-out,
|
||||
transform 200ms cubic-bezier(0.34, 1.4, 0.64, 1);
|
||||
}
|
||||
|
||||
.shortcuts-dialog[open]::backdrop {
|
||||
transition: background-color 150ms ease-out;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
.shortcuts-dialog[open] {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px) scale(0.98);
|
||||
}
|
||||
|
||||
.shortcuts-dialog[open]::backdrop {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcuts-dialog.closing {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px) scale(0.98);
|
||||
transition:
|
||||
opacity 150ms ease-in,
|
||||
transform 150ms ease-in;
|
||||
}
|
||||
|
||||
.shortcuts-dialog.closing::backdrop {
|
||||
background-color: transparent;
|
||||
transition: background-color 150ms ease-in;
|
||||
}
|
||||
|
||||
.shortcuts-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.shortcuts-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.shortcuts-close {
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
color: var(--vp-c-text-2);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shortcuts-close:hover,
|
||||
.shortcuts-close:focus-visible {
|
||||
color: var(--vp-c-text-1);
|
||||
background: var(--vp-c-default-soft);
|
||||
}
|
||||
|
||||
.shortcuts-close svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.shortcuts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shortcut-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.shortcut-keys {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.shortcut-description {
|
||||
margin: 0;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.shortcut-plus,
|
||||
.shortcut-or {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.shortcut-or {
|
||||
margin: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@ function onReset() {
|
||||
<InputField
|
||||
id="non-scaling-stroke"
|
||||
label="Non-scaling stroke"
|
||||
class="non-scaling-stroke-switch"
|
||||
>
|
||||
<Switch
|
||||
id="non-scaling-stroke-switch"
|
||||
|
||||
@@ -13,6 +13,7 @@ import { EXTERNAL_LIBS_CONTEXT, externalLibContext } from './composables/useExte
|
||||
import SideBarNavBefore from './layouts/SideBarNavBefore.vue';
|
||||
import NavBarSponsorButton from './layouts/NavBarSponsorButton.vue';
|
||||
import SnackPlayer from './components/editors/SnackPlayer.vue';
|
||||
import Toaster from './components/base/Toaster.vue';
|
||||
import { setupTracking } from './utils/openpanel';
|
||||
import trackUnderlineDirection from './utils/underlineDirection';
|
||||
|
||||
@@ -25,6 +26,7 @@ const theme: Partial<Theme> = {
|
||||
'sidebar-nav-after': () => h(IconsSidebarNavAfter),
|
||||
'home-hero-image': () => h(HomeHeroIconsCard),
|
||||
'home-hero-actions-after': () => h(HomeHeroAfter),
|
||||
'layout-bottom': () => h(Toaster),
|
||||
'nav-bar-content-after': () => h(NavBarSponsorButton),
|
||||
// Keep this here for now, we might want to add more things to the top bar in the future
|
||||
// 'layout-top': () => h(LayoutTop),
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
import { computed } from 'vue';
|
||||
import { useData } from 'vitepress';
|
||||
import SidebarIconCustomizer from '../components/icons/SidebarIconCustomizer.vue';
|
||||
import ExternalLibrarySelect from '../components/icons/SidebarExternalLibrarySelect.vue';
|
||||
import CategoryList from '../components/icons/CategoryList.vue';
|
||||
import KeyboardShortcuts from '../components/icons/KeyboardShortcuts.vue';
|
||||
|
||||
import CategoryList from '../components/icons/CategoryList.vue'
|
||||
import SidebarIconCustomizer from '../components/icons/SidebarIconCustomizer.vue'
|
||||
import ExternalLibrarySelect from '../components/icons/SidebarExternalLibrarySelect.vue'
|
||||
|
||||
const { page } = useData()
|
||||
const { page } = useData();
|
||||
|
||||
const isIconsPage = computed(() => page.value?.relativePath?.startsWith?.('icons'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SidebarIconCustomizer v-if="page?.relativePath?.startsWith?.('icons')"/>
|
||||
<ExternalLibrarySelect v-if="page?.relativePath?.startsWith?.('icons')"/>
|
||||
<CategoryList v-if="page?.relativePath?.startsWith?.('icons')"/>
|
||||
<div v-if="isIconsPage">
|
||||
<SidebarIconCustomizer />
|
||||
<ExternalLibrarySelect />
|
||||
<CategoryList />
|
||||
<KeyboardShortcuts />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -385,6 +385,12 @@ html.dark .VPNav .VPSwitchAppearance .icon .moon {
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.4, 0.64, 1);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPSidebar {
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Links in the page text: a faint underline shows they are links, and on hover a solid
|
||||
underline draws in from the left. Drawn with backgrounds so it also follows links that
|
||||
wrap onto a second line. */
|
||||
|
||||
@@ -11,7 +11,7 @@ const allowedAttrs = [
|
||||
'class',
|
||||
];
|
||||
|
||||
export default function getSVGIcon(element?: HTMLElement, attrs?: Record<string, string>) {
|
||||
export default function getSVGIcon(element?: Element, attrs?: Record<string, string>) {
|
||||
const svg = element ?? document.querySelector('#previewer svg');
|
||||
if (!svg) return;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export default function useSearchShortcut(callback: () => void) {
|
||||
// Check for Cmd+K (Mac), Ctrl+K (Windows/Linux), or forward slash
|
||||
if (((event.metaKey || event.ctrlKey) && event.key === 'k') || event.key === '/') {
|
||||
event.preventDefault();
|
||||
// Algolia listens for the same keys on window, so claim them before it sees them
|
||||
event.stopImmediatePropagation();
|
||||
callback();
|
||||
}
|
||||
}
|
||||
@@ -24,11 +26,11 @@ export default function useSearchShortcut(callback: () => void) {
|
||||
shortcutText.value = isMac ? '⌘K' : 'Ctrl+K';
|
||||
|
||||
// Add keyboard shortcut listener
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
window.addEventListener('keydown', handleKeydown, { capture: true });
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
window.removeEventListener('keydown', handleKeydown, { capture: true });
|
||||
});
|
||||
|
||||
return { shortcutText };
|
||||
|
||||
+2
-1
@@ -77,6 +77,7 @@
|
||||
"react": "19.3.1",
|
||||
"react-dom": "19.3.1",
|
||||
"sandpack-vue3": "3.1.12",
|
||||
"vue": "^3.5.44"
|
||||
"vue": "^3.5.44",
|
||||
"vue-sonner": "^2.0.9"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+19
@@ -212,6 +212,9 @@ importers:
|
||||
vue:
|
||||
specifier: ^3.5.44
|
||||
version: 3.5.44(typescript@6.0.3)
|
||||
vue-sonner:
|
||||
specifier: ^2.0.9
|
||||
version: 2.0.9
|
||||
devDependencies:
|
||||
'@docsearch/js':
|
||||
specifier: 3.8.2
|
||||
@@ -13348,6 +13351,20 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
vue-sonner@2.0.9:
|
||||
resolution: {integrity: sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==}
|
||||
peerDependencies:
|
||||
'@nuxt/kit': ^4.0.3
|
||||
'@nuxt/schema': ^4.0.3
|
||||
nuxt: ^4.0.3
|
||||
peerDependenciesMeta:
|
||||
'@nuxt/kit':
|
||||
optional: true
|
||||
'@nuxt/schema':
|
||||
optional: true
|
||||
nuxt:
|
||||
optional: true
|
||||
|
||||
vue-tsc@3.3.11:
|
||||
resolution: {integrity: sha512-gOb0B9rtU2+f1dszwPqSH5kAieIF9ReeLhD3kSRNHv5WZZUQz/JdVXW0RTdqhNTMlQkqKzrTTviqKr/4FYZraQ==}
|
||||
hasBin: true
|
||||
@@ -27098,6 +27115,8 @@ snapshots:
|
||||
- unloader
|
||||
- webpack
|
||||
|
||||
vue-sonner@2.0.9: {}
|
||||
|
||||
vue-tsc@3.3.11(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.28(typescript@6.0.3)
|
||||
|
||||
Reference in New Issue
Block a user