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:
PrimeTestLab
2026-11-10 08:29:51 +01:00
committed by GitHub
co-authored by Claude Opus 5.5 Eric
parent 3fef7aca71
commit bfa381ef26
16 changed files with 563 additions and 61 deletions
@@ -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) {
+11 -17
View File
@@ -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"
+2
View File
@@ -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>
+6
View File
@@ -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. */
+1 -1
View File
@@ -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
View File
@@ -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"
}
}
+19
View File
@@ -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)