feat(site): Add Hover And Focus Animations Across The Docs Site (#5085)

* feat(docs): Add Hover And Focus Animations Across The Docs Site

- Nav bar and footer links: hover underline that draws in from the side the
  mouse enters and leaves (utils/underlineDirection.ts)
- Logo spins half a turn on hover; nav dropdown chevron turns and the menu
  slides in; social icons get a soft background and lift
- Sidebar, "On this page" outline, "Edit this page" and previous/next links
  get soft pill hovers; previous/next use Lucide arrow icons
- Footer is now shown on pages with a sidebar, offset by the sidebar width
- Form controls (switch, checkbox, select, range slider, color picker, fake
  input, reset button) get brand hover/focus states and smoother motion
- Cards (overview, package, showcase, team member, category list) lift with
  a brand border on hover
- Page and sidebar scrollbars styled in the site colours, with a Firefox fallback

* Revert changes

* Apply changes

* Added the global transition off

* fix(docs): remove !important markers from transition durations, very bad a11y

* fix(docs): remove !important markers from transition durations, very bad a11y

* Fixes last things

* Added one thing

---------

Co-authored-by: Eric <eric.fennis@gmail.com>
Co-authored-by: Karsa <contact@karsa.org>
This commit is contained in:
PrimeTestLab
2026-10-30 15:07:29 +01:00
committed by GitHub
co-authored by Eric Karsa
parent 5abef78c8e
commit 6e25d21b2f
21 changed files with 855 additions and 74 deletions
+6 -2
View File
@@ -1,7 +1,7 @@
[
{
"name": "Eric Fennis",
"title": "Creator of Lucide & Software engineer @nedap",
"title": "Creator & Maintainer of Lucide",
"image": "https://github.com/ericfennis.png?size=192",
"sponsor": "https://github.com/sponsors/ericfennis",
"socialLinks": [
@@ -12,6 +12,10 @@
{
"icon": "x",
"link": "https://x.com/ericfennis"
},
{
"icon": "linkedin",
"link": "https://www.linkedin.com/in/ericfennis"
}
]
},
@@ -32,7 +36,7 @@
},
{
"name": "Jakob Guddas",
"title": "Maintainer of Lucide & Software engineer @LEGO",
"title": "Creator of Lucide Studio",
"image": "https://github.com/jguddas.png?size=192",
"socialLinks": [
{
@@ -31,7 +31,7 @@ const rel = computed(() => {
<style scoped>
.card {
border: 1px solid var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background-color: var(--vp-c-bg-soft);
display: flex;
@@ -52,5 +52,4 @@ const rel = computed(() => {
.card[href]:hover {
border-color: var(--vp-c-brand-1);
}
</style>
@@ -79,6 +79,15 @@ const model = computed({
border: 1px solid var(--vp-input-border-color);
background-color: var(--vp-input-switch-bg-color);
border-radius: 4px;
transition: border-color 0.2s, box-shadow 0.2s;
transition-duration: 0.2s;
}
.checkbox:hover,
.checkbox:focus-visible {
outline: none;
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 20%, transparent);
}
.checkbox:checked {
@@ -87,4 +96,8 @@ const model = computed({
center no-repeat var(--vp-c-brand);;
}
.checkbox-wrapper:hover .checkbox-label {
color: var(--vp-c-text-1);
}
</style>
@@ -35,19 +35,37 @@ defineProps({
font-size: 14px;
/* box-shadow: var(--vp-shadow-4), var(--vp-shadow-2); */
text-align: left;
border: 1px solid transparent;
border: 1px solid var(--vp-c-divider);
cursor: text;
display: flex;
gap: 12px;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
background-color 0.25s,
box-shadow 0.25s;
transition-duration: 0.25s;
}
/* A quiet border at rest; on hover the border takes the brand colour with a soft ring,
and the search icon tilts as if it starts looking */
.fake-input:hover,
.fake-input:focus {
.fake-input:focus-visible {
outline: none;
color: var(--vp-c-text-1);
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 15%, transparent);
}
.search-icon {
flex-shrink: 0;
transition: color 0.25s, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
transition-duration: 0.25s, 0.3s;
}
.fake-input:hover .search-icon,
.fake-input:focus-visible .search-icon {
color: var(--vp-c-brand-1);
}
.shortcut {
@@ -99,7 +99,7 @@ defineExpose({
.input {
justify-content: flex-start;
border: 1px solid transparent;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0 10px 0 12px;
width: 100%;
@@ -109,16 +109,21 @@ defineExpose({
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
background-color 0.25s,
box-shadow 0.25s;
transition-duration: 0.25s;
}
.input.has-shortcut {
padding-right: calc(var(--shortcut-width, 40px) + 22px);
}
/* Same as FakeInput: a quiet border at rest, a brand border with a soft ring on hover and focus */
.input:hover,
.input:focus {
outline: none;
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 15%, transparent);
}
.input:focus {
background-color: var(--vp-c-bg);
@@ -190,8 +195,13 @@ 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-duration: 0.25s, 0.3s;
}
/* Like the FakeInput search icon, it tilts as if it starts looking */
.input-wrapper > svg:has(+ .input.has-icon:hover),
.input-wrapper > svg:has(+ .input.has-icon:focus) {
color: var(--vp-c-text-1);
color: var(--vp-c-brand-1);
}
</style>
@@ -33,12 +33,45 @@ defineProps<{
padding: 11px 16px 13px;
width: 100%;
height: 100%;
transition: border-color 0.25s;
transition: border-color 0.25s, background-color 0.25s, transform 0.25s ease, box-shadow 0.25s;
transition-duration: 0.25s;
text-decoration: none;
}
.overview-link:hover {
/* On hover the card lifts with a light brand tint, like the previous / next page links,
and Lucide's arrow-right icon slides in after the title */
.overview-link:hover,
.overview-link:focus-visible {
outline: none;
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
background-color: color-mix(in srgb, var(--vp-c-brand) 6%, transparent);
box-shadow: var(--vp-shadow-2);
}
.overview-link .title {
display: flex;
align-items: center;
gap: 6px;
}
.overview-link .title::after {
content: '';
flex-shrink: 0;
width: 16px;
height: 16px;
background-color: currentColor;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='m12 5 7 7-7 7'/%3E%3C/svg%3E") center / contain no-repeat;
opacity: 0;
transform: translateX(-6px);
transition: opacity 0.25s, transform 0.25s ease;
transition-duration: 0.25s;
}
.overview-link:hover .title::after,
.overview-link:focus-visible .title::after {
opacity: 1;
transform: translateX(0);
}
.desc {
@@ -111,6 +111,36 @@ const percentage = computed<string>(() => `${((Number(props.modelValue) - props.
border-radius: 10px;
box-shadow: var(--vp-shadow-2);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
transition-duration: 0.2s;
}
.slider input::-moz-range-thumb {
transition: transform 0.2s ease, box-shadow 0.2s ease;
transition-duration: 0.2s;
}
/* The thumb grows with a soft brand ring on hover and focus, and a little more while dragged */
.slider input:hover::-webkit-slider-thumb,
.slider input:focus-visible::-webkit-slider-thumb {
transform: scale(1.1);
box-shadow: var(--vp-shadow-2), 0 0 0 5px color-mix(in srgb, var(--vp-c-brand) 25%, transparent);
}
.slider input:active::-webkit-slider-thumb {
transform: scale(1.2);
box-shadow: var(--vp-shadow-2), 0 0 0 7px color-mix(in srgb, var(--vp-c-brand) 25%, transparent);
}
.slider input:hover::-moz-range-thumb,
.slider input:focus-visible::-moz-range-thumb {
transform: scale(1.1);
box-shadow: var(--vp-shadow-2), 0 0 0 5px color-mix(in srgb, var(--vp-c-brand) 25%, transparent);
}
.slider input:active::-moz-range-thumb {
transform: scale(1.2);
box-shadow: var(--vp-shadow-2), 0 0 0 7px color-mix(in srgb, var(--vp-c-brand) 25%, transparent);
}
.slider input::-moz-range-thumb {
@@ -1,11 +1,20 @@
<script setup lang="ts">
import { ref } from 'vue';
import { rotateCw } from '../../../data/iconNodes';
import Icon from '@lucide/vue/src/Icon';
import Button from './Button.vue';
const spinning = ref(false);
</script>
<template>
<Button class="reset-button" aria-label="Reset style">
<Button
class="reset-button"
:class="{ spinning }"
aria-label="Reset style"
@click="spinning = true"
@animationend="spinning = false"
>
<Icon
:size="20"
:iconNode="rotateCw"
@@ -16,13 +25,16 @@ import Button from './Button.vue';
<style scoped>
.reset-button {
transition: ease-in-out 0.1s opacity;
transition: ease-in-out 0.25s opacity;
transition-duration: 0.25s;
background: none;
padding: 0;
}
.reset-button .lucide {
transition: ease-in-out 0.1s transform;
/* The arrow spins once in place when clicked */
.reset-button.spinning .lucide {
animation: rotate 0.6s cubic-bezier(0.4, 0, 0.2, 1);
animation-duration: 0.6s !important;
}
.reset-button:hover {
@@ -36,18 +48,13 @@ import Button from './Button.vue';
opacity: 0.5;
}
/* a rotate css animation keyframes */
@keyframes rotate {
0% {
from {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
to {
transform: rotate(360deg);
}
}
.reset-button:active .lucide {
transform: rotate(45deg);
}
</style>
@@ -36,6 +36,8 @@ const selected = defineModel<{ name: string; value: string; icon?: string; iconD
</ListboxButton>
<transition
enter-active-class="transition-enter"
enter-from-class="transition-enter-from"
leave-active-class="transition-leave"
leave-from-class="transition-leave-from"
leave-to-class="transition-leave-to"
@@ -83,20 +85,33 @@ const selected = defineModel<{ name: string; value: string; icon?: string; iconD
padding: 7px 14px;
height: auto;
font-size: 14px;
border: 1px solid transparent;
cursor: text;
border: 1px solid var(--vp-c-divider);
cursor: pointer;
display: flex;
gap: 12px;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
background-color 0.25s,
box-shadow 0.25s;
transition-duration: 0.25s;
width: 100%;
align-items: center;
}
.select-button:focus {
/* Same as FakeInput: a quiet border at rest, a brand border with a soft ring on hover, focus
and while open, and the chevrons take the text colour */
.select-button:hover,
.select-button:focus,
.select-button[aria-expanded='true'] {
outline: none;
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 15%, transparent);
}
.select-button:hover .chevron-icon,
.select-button[aria-expanded='true'] .chevron-icon {
color: var(--vp-c-text-1);
}
.select-text {
@@ -121,10 +136,26 @@ const selected = defineModel<{ name: string; value: string; icon?: string; iconD
height: 1.25rem;
width: 1.25rem;
color: #9ca3af;
transition: color 0.25s;
transition-duration: 0.25s;
}
/* The list fades in and slides down a little from the button */
.transition-enter {
transition:
opacity 150ms ease-out,
transform 200ms cubic-bezier(0.34, 1.4, 0.64, 1);
transition-duration: 150ms, 200ms;
}
.transition-enter-from {
opacity: 0;
transform: translateY(-6px) scale(0.98);
}
.transition-leave {
transition: opacity 100ms ease-in;
transition-duration: 100ms;
}
.transition-leave-from {
@@ -172,6 +203,7 @@ const selected = defineModel<{ name: string; value: string; icon?: string; iconD
transition:
background-color 0.25s,
color 0.25s;
transition-duration: 0.25s;
list-style: none;
}
@@ -43,6 +43,7 @@
border: 1px solid var(--vp-input-border-color);
background-color: var(--vp-input-switch-bg-color);
transition: border-color 0.25s, background-color 0.4s ease;
transition-duration: 0.25s, 0.4s;
border-radius: 11px;
}
@@ -56,21 +57,51 @@
border-color: var(--vp-input-hover-border-color);
}
.switch:focus-visible {
outline: none;
}
/* The thumb sits 2px inside the track and slides with a small bounce. Like the RangeSlider
thumb, it grows with a soft brand ring on hover and focus, and a little more while pressed */
.thumb {
display: inline-block;
margin: 2px;
background-color: #fff;
transition: transform 0.25s;
width: 20px;
height: 20px;
border-radius: 50%;
transition:
transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.2s ease;
transition-duration: 0.4s, 0.2s;
width: 16px;
height: 16px;
border-radius: 8px;
/* background-color: var(--vp-c-neutral); */
box-shadow: var(--vp-shadow-1);
}
.switch:hover .thumb,
.switch:focus-visible .thumb {
transform: scale(1.1);
box-shadow: var(--vp-shadow-1), 0 0 0 5px color-mix(in srgb, var(--vp-c-brand) 25%, transparent);
}
.switch:active .thumb {
transform: scale(1.2);
box-shadow: var(--vp-shadow-1), 0 0 0 7px color-mix(in srgb, var(--vp-c-brand) 25%, transparent);
}
.switch.enabled .thumb {
transform: translateX(18px);
}
.switch.enabled:hover .thumb,
.switch.enabled:focus-visible .thumb {
transform: translateX(18px) scale(1.1);
}
.switch.enabled:active .thumb {
transform: translateX(18px) scale(1.2);
}
/* ------------------- */
.VPSwitch {
position: relative;
@@ -8,6 +8,7 @@ import ColorPicker from '../base/ColorPicker.vue';
import ResetButton from '../base/ResetButton.vue';
import HomeIconCustomizerIcons from './HomeIconCustomizerIcons.vue';
import Switch from '../base/Switch.vue';
import Card from '../base/Card.vue';
import useTrackStyleChanges from '../../composables/useTrackStyleChanges';
import { track } from '../../utils/openpanel';
@@ -31,7 +32,7 @@ const iconStyle = computed(() => ({
<template>
<HomeContainer>
<div class="card">
<Card class="customizer-card">
<div class="card-column">
<h2 class="title">
Style as you please
@@ -116,16 +117,14 @@ const iconStyle = computed(() => ({
:style="iconStyle"
/>
</div>
</div>
</Card>
</HomeContainer>
</template>
<style scoped>
.card {
display: block;
border-radius: 12px;
.customizer-card {
display: flex;
height: 100%;
background-color: var(--vp-c-bg-soft);
padding: 24px;
--slider-bar-color: var(--vp-c-bg-soft-down);
--color-picker-bg: var(--vp-c-bg-soft-down);
@@ -10,9 +10,10 @@ export interface TeamMember {
<script setup lang="ts">
import { DefaultTheme } from 'vitepress';
import Icon from '@lucide/vue/src/Icon'
import Card from '../base/Card.vue'
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue'
import VPSocialLinks from 'vitepress/dist/client/theme-default/components/VPSocialLinks.vue'
import { heart } from '../../../data/iconNodes'
defineProps<TeamMember>()
</script>
@@ -20,7 +21,9 @@ defineProps<TeamMember>()
<template>
<div>
<Card class="member-card">
<a :href="socialLinks[0]?.link" target="_blank" rel="noreferrer noopener">
<img :src="image" :alt="name" class="member-image"/>
</a>
<h3 class="member-name">
{{name}}
</h3>
@@ -28,16 +31,25 @@ defineProps<TeamMember>()
{{title}}
</p>
<div class="member-links">
<VPButton
<a
v-if="sponsor"
:href="sponsor"
text="Sponsor"
theme="sponsor"
class="sponsor-button"
size="medium"
/>
target="_blank"
rel="noreferrer noopener"
>
<Icon
:iconNode="heart"
name="heart"
:size="16"
class="sponsor-heart"
aria-hidden="true"
/>
Sponsor
</a>
<VPSocialLinks
:links="socialLinks"
class="member-social-links"
/>
</div>
@@ -57,6 +69,14 @@ defineProps<TeamMember>()
border-radius: 32px;
margin: 0 auto;
background-color: var(--vp-c-bg);
box-shadow: 0 0 0 0 transparent;
transition: transform 0.3s ease, box-shadow 0.3s ease;
transition-duration: 0.3s;
}
.member-image:hover {
transform: scale(1.06);
box-shadow: 0 0 0 3px var(--vp-c-bg-soft), 0 0 0 5px var(--vp-c-brand-1);
}
.member-name {
@@ -79,14 +99,81 @@ defineProps<TeamMember>()
margin-bottom: 16px;;
}
.sponsor-button {
width: auto;
}
.member-links {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.sponsor-button {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 16px 0 14px;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
text-decoration: none;
border: 1px solid var(--vp-c-divider);
border-radius: 18px;
background-color: var(--vp-c-bg);
transition: color 0.25s, background-color 0.25s, border-color 0.25s;
transition-duration: 0.25s;
}
.sponsor-heart {
color: var(--vp-c-sponsor);
transition: color 0.25s, fill 0.25s;
transition-duration: 0.25s;
}
.sponsor-button:hover,
.sponsor-button:focus-visible {
outline: none;
color: #fff;
border-color: var(--vp-c-sponsor);
background-color: var(--vp-c-sponsor);
}
.sponsor-button:hover .sponsor-heart,
.sponsor-button:focus-visible .sponsor-heart {
color: #fff;
fill: #fff;
animation: heartbeat 1s ease-in-out infinite;
animation-duration: 1s !important;
animation-iteration-count: infinite !important;
}
@keyframes heartbeat {
0%,
40%,
100% {
transform: scale(1);
}
20% {
transform: scale(1.25);
}
60% {
transform: scale(1.15);
}
}
/* Social icons get a soft rounded background and lift a little on hover */
.member-social-links :deep(.VPSocialLink) {
border-radius: 8px;
transition: color 0.25s, background-color 0.25s, transform 0.25s;
transition-duration: 0.25s;
}
.member-social-links :deep(.VPSocialLink:hover),
.member-social-links :deep(.VPSocialLink:focus-visible) {
outline: none;
color: var(--vp-c-text-1);
background-color: var(--vp-c-default-soft);
transform: translateY(-2px);
}
</style>
@@ -74,7 +74,8 @@ watch(headers, () => {
line-height: 24px;
font-size: 14px;
display: block;
transition: color 0.25s;
transition: color 0.25s, transform 0.25s ease;
transition-duration: 0.25s;
padding: 4px 0;
}
@@ -86,6 +87,13 @@ watch(headers, () => {
.sidebar-link:hover, .sidebar-link.active {
color: var(--vp-c-brand);
}
/* All and Categories slide in a little on hover */
.sidebar-link:hover,
.sidebar-link:focus-visible {
outline: none;
transform: translateX(4px);
}
.content {
margin-top: 12px;
position: relative;
@@ -101,8 +109,9 @@ watch(headers, () => {
left: -1px;
z-index: 0;
opacity: 0;
width: 1px;
width: 2px;
height: 18px;
border-radius: 1px;
background-color: var(--vp-c-brand);
transition: top 0.25s cubic-bezier(0, 1, 0.5, 1), background-color 0.5s, opacity 0.25s;
}
@@ -68,22 +68,45 @@ function onClick(categoryName: string) {
padding-left: 13px;
}
/* Each category is a soft pill on hover: the name slides in a little and the count badge
takes the brand colour. The category in view keeps the brand colour. */
.outline-link {
display: flex;
align-items: baseline;
line-height: 28px;
align-items: center;
margin: 1px -8px;
padding: 0 6px 0 8px;
line-height: 26px;
color: var(--vp-c-text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.5s;
border-radius: 6px;
transition:
color 0.25s,
background-color 0.25s;
transition-duration: 0.25s;
font-weight: 500;
}
.outline-link > span:first-child {
transition: transform 0.25s ease;
transition-duration: 0.25s;
}
.outline-link:hover,
.outline-link.active {
.outline-link:focus-visible {
outline: none;
color: var(--vp-c-text-1);
transition: color 0.25s;
background-color: var(--vp-c-default-soft);
}
.outline-link:hover > span:first-child,
.outline-link:focus-visible > span:first-child {
transform: translateX(3px);
}
.outline-link.active {
color: var(--vp-c-brand-1);
}
.outline-link.inactive {
@@ -96,10 +119,26 @@ function onClick(categoryName: string) {
}
.icon-count {
opacity: 0.5;
margin-left: auto;
padding: 0 6px;
min-width: 26px;
line-height: 18px;
text-align: center;
font-size: 11px;
font-weight: 400;
font-weight: 500;
color: var(--vp-c-text-3);
border-radius: 9px;
transition:
color 0.25s,
background-color 0.25s;
transition-duration: 0.25s;
}
.outline-link:hover .icon-count,
.outline-link:focus-visible .icon-count,
.outline-link.active .icon-count {
color: var(--vp-c-brand-1);
background-color: color-mix(in srgb, var(--vp-c-brand) 14%, transparent);
}
.outline-link.inactive .icon-count {
@@ -157,6 +157,8 @@ const Expand = createLucideIcon('Expand', expand);
margin: 0 auto;
padding: 1rem;
background-color: var(--vp-c-bg-elv);
border-top: 1px solid var(--vp-c-divider);
border-right: 1px solid var(--vp-c-divider);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
will-change: transform;
pointer-events: all;
@@ -7,6 +7,7 @@ import InputField from '../base/InputField.vue';
import ColorPicker from '../base/ColorPicker.vue';
import ResetButton from '../base/ResetButton.vue';
import Switch from '../base/Switch.vue';
import Card from '../base/Card.vue';
import useTrackStyleChanges from '../../composables/useTrackStyleChanges';
import { track } from '../../utils/openpanel';
@@ -29,7 +30,7 @@ function onReset() {
</script>
<template>
<div
<Card
class="customizer-card"
:class="{ customized: showCustomized }"
>
@@ -95,7 +96,7 @@ function onReset() {
v-model="nonScalingStroke"
/>
</InputField>
</div>
</Card>
</template>
<style scoped>
@@ -113,15 +114,13 @@ function onReset() {
font-size: 16px;
}
.customizer-card {
background: var(--vp-c-bg);
.card.customizer-card {
padding: 12px 24px 24px;
border-radius: 12px;
margin-bottom: 24px;
position: relative;
z-index: 0;
border: 1px solid transparent;
transition: border-color 0.4s ease-in-out;
background: var(--vp-c-bg);
}
.customizer-card.customized {
@@ -64,13 +64,14 @@ if (carbonOptions) {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
padding: 16px 20px 20px;
border-radius: 12px;
min-height: 256px;
text-align: center;
line-height: 18px;
font-size: 12px;
font-weight: 500;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-carbon-ads-bg-color);
}
@@ -26,21 +26,90 @@ defineProps<{
class="logo dark"
:alt="`${showcaseItem.name} logo`"
/>
<span
class="company-name"
aria-hidden="true"
>{{ showcaseItem.name }}</span
>
<span
class="company-arrow"
aria-hidden="true"
/>
</Card>
</div>
</template>
<style scoped>
.company {
position: relative;
overflow: hidden;
display: flex;
height: 100%;
padding: 48px 0;
}
/* On hover the card lifts with a brand border, the logo grows and moves up a little to make
room for the name sliding in below it, and an arrow shows the link opens the site */
.company:hover,
.company:focus-visible {
outline: none;
}
.logo {
height:64px;
width: 240px;
margin: auto;
opacity: 0.8;
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s;
transition-duration: 0.4s, 0.3s;
}
.company:hover .logo,
.company:focus-visible .logo {
opacity: 1;
transform: translateY(-8px) scale(1.02);
}
.company-name {
position: absolute;
right: 0;
bottom: 14px;
left: 0;
text-align: center;
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-2);
opacity: 0;
transform: translateY(3px);
transition: opacity 0.3s, transform 0.3s ease;
transition-duration: 0.3s;
}
.company:hover .company-name,
.company:focus-visible .company-name {
opacity: 1;
transform: translateY(0);
}
/* Lucide's arrow-up-right icon */
.company-arrow {
position: absolute;
top: 14px;
right: 14px;
width: 18px;
height: 18px;
background-color: var(--vp-c-brand-1);
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 7h10v10'/%3E%3Cpath d='M7 17 17 7'/%3E%3C/svg%3E") center / contain no-repeat;
opacity: 0;
transform: translate(-6px, 6px);
transition: opacity 0.3s, transform 0.3s ease;
transition-duration: 0.3s;
}
.company:hover .company-arrow,
.company:focus-visible .company-arrow {
opacity: 1;
transform: translate(0, 0);
}
html.dark .logo.dark {
+5
View File
@@ -13,6 +13,7 @@ import { EXTERNAL_LIBS_CONTEXT, externalLibContext } from './composables/useExte
import SideBarNavBefore from './layouts/SideBarNavBefore.vue';
import SnackPlayer from './components/editors/SnackPlayer.vue';
import { setupTracking } from './utils/openpanel';
import trackUnderlineDirection from './utils/underlineDirection';
const theme: Partial<Theme> = {
extends: DefaultTheme,
@@ -34,6 +35,10 @@ const theme: Partial<Theme> = {
app.component('SnackPlayer', SnackPlayer);
setupTracking(router);
if (!import.meta.env.SSR) {
trackUnderlineDirection();
}
},
};
+384 -10
View File
@@ -62,6 +62,23 @@
navigation: auto;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
/* ::view-transition-old(icons-search-box),
::view-transition-new(icons-search-box) {
animation: .3s transition 0s ease;
@@ -71,6 +88,40 @@ html {
scrollbar-gutter: stable;
}
/* Page scrollbar in the site's colours: a slim rounded thumb on the page background */
html::-webkit-scrollbar {
width: 12px;
height: 12px;
}
html::-webkit-scrollbar-track,
html::-webkit-scrollbar-corner {
background-color: var(--vp-c-bg);
}
html::-webkit-scrollbar-thumb {
background-color: var(--vp-c-text-3);
background-clip: padding-box;
border: 3px solid transparent;
border-radius: 12px;
}
html::-webkit-scrollbar-thumb:hover {
background-color: var(--vp-c-text-2);
}
html::-webkit-scrollbar-thumb:active {
background-color: var(--vp-c-brand-1);
}
/* Browsers without the scrollbar pseudo-elements, such as Firefox */
@supports not selector(::-webkit-scrollbar) {
html {
scrollbar-width: thin;
scrollbar-color: var(--vp-c-text-3) var(--vp-c-bg);
}
}
.VPHome {
overflow-x: hidden;
}
@@ -108,6 +159,10 @@ html {
justify-content: flex-end;
}
.VPFeature {
border: 1px solid var(--vp-c-divider) !important;
}
.VPFeature .icon {
background-color: var(--vp-c-bg);
}
@@ -166,21 +221,321 @@ html {
border-radius: 2px;
}
/* Nav bar links: on hover an underline draws in from the side the mouse enters and leaves
towards the side it exits (see utils/underlineDirection.ts). Without that script it
draws in from the left and leaves to the right. It stays under the link of the current page. */
.VPNav .VPNavBarMenuLink,
.VPNav .VPNavBarMenuGroup .button {
position: relative;
transition: opacity 0.25s;
}
.VPNav .VPNavBarMenuLink::before,
.VPNav .VPNavBarMenuGroup .button::before {
content: '';
position: absolute;
right: 12px;
bottom: 16px;
left: 12px;
height: 2px;
border-radius: 2px;
background-color: var(--vp-c-brand-1);
transform: scaleX(0);
transform-origin: var(--underline-origin, right);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.VPNav .VPNavBarMenuLink:hover::before,
.VPNav .VPNavBarMenuLink:focus-visible::before,
.VPNav .VPNavBarMenuLink.active::before,
.VPNav .VPNavBarMenuGroup:hover .button::before,
.VPNav .VPNavBarMenuGroup .button[aria-expanded='true']::before,
.VPNav .VPNavBarMenuGroup.active .button::before {
transform: scaleX(1);
transform-origin: var(--underline-origin, left);
}
/* On hover the link text dims, like the dropdown buttons do; the current page keeps its brand colour */
.VPNav .VPNavBarMenuLink:not(.active):hover {
color: var(--vp-c-text-1);
opacity: 0.8;
}
/* The chevron turns up while the dropdown is open, and the menu slides down into place.
VitePress draws the down chevron as a right chevron turned by 90deg. */
.VPNav .VPNavBarMenuGroup .text-icon {
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.VPNav .VPNavBarMenuGroup:hover .text-icon,
.VPNav .VPNavBarMenuGroup .button[aria-expanded='true'] .text-icon {
transform: rotate(-90deg);
}
.VPNav .VPNavBarMenuGroup .button[aria-expanded='false'] + .menu {
transform: translateY(-8px);
}
.VPNav .VPNavBarMenuGroup:hover .button + .menu,
.VPNav .VPNavBarMenuGroup .button[aria-expanded='true'] + .menu {
transform: translateY(0);
}
/* Appearance switch: the thumb sits 2px inside the track and slides over with a small bounce,
while the sun and the moon turn and grow as they swap places */
.VPNav .VPSwitchAppearance .check {
top: 2px;
left: 2px;
width: 16px;
height: 16px;
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
html.dark .VPNav .VPSwitchAppearance .check {
transform: translateX(18px);
}
.VPNav .VPSwitchAppearance:hover .check,
.VPNav .VPSwitchAppearance:focus-visible .check {
box-shadow:
var(--vp-shadow-1),
0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 25%, transparent);
}
.VPNav .VPSwitchAppearance .icon {
width: 16px;
height: 16px;
}
.VPNav .VPSwitchAppearance .icon .sun,
.VPNav .VPSwitchAppearance .icon .moon {
top: 2px;
left: 2px;
transition:
opacity 0.3s ease,
transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.VPNav .VPSwitchAppearance .icon .moon,
html.dark .VPNav .VPSwitchAppearance .icon .sun {
opacity: 0;
transform: rotate(-90deg) scale(0.4);
}
.VPNav .VPSwitchAppearance .icon .sun,
html.dark .VPNav .VPSwitchAppearance .icon .moon {
opacity: 1;
transform: rotate(0) scale(1);
}
/* Nav bar social icons get a soft rounded background and lift a little on hover */
.VPNav .VPSocialLinks .VPSocialLink {
border-radius: 8px;
transition:
color 0.25s,
background-color 0.25s,
transform 0.25s;
}
.VPNav .VPSocialLinks .VPSocialLink:hover,
.VPNav .VPSocialLinks .VPSocialLink:focus-visible {
outline: none;
color: var(--vp-c-text-1);
background-color: var(--vp-c-default-soft);
transform: translateY(-2px);
}
.package-lucide-logo {
margin: 0 auto 48px;
max-width: 480px;
}
.VPSidebar {
/* Hide Scrollbar */
-ms-overflow-style: none;
scrollbar-width: none;
scrollbar-width: thin; /* can also be normal, or none, to not render scrollbar */
scrollbar-color: var(--vp-c-text-4) transparent; /* foreground background */
/* Sidebar links: a soft pill on hover with the text sliding in a little, a light brand pill
for the current page, and a smooth turn of the section chevrons */
.VPSidebar .VPSidebarItem.is-link > .item > .link {
margin: 1px -8px;
padding: 0 8px;
border-radius: 6px;
transition: background-color 0.25s;
}
.VPSidebar .VPSidebarItem.is-link > .item > .link > .text {
transition:
color 0.25s,
transform 0.25s ease;
}
.VPSidebar .VPSidebarItem.is-link > .item > .link:hover,
.VPSidebar .VPSidebarItem.is-link > .item > .link:focus-visible {
outline: none;
background-color: var(--vp-c-default-soft);
}
.VPSidebar .VPSidebarItem.is-link > .item > .link:hover > .text,
.VPSidebar .VPSidebarItem.is-link > .item > .link:focus-visible > .text {
transform: translateX(3px);
}
.VPSidebar .VPSidebarItem.is-active > .item > .link {
background-color: color-mix(in srgb, var(--vp-c-brand) 10%, transparent);
}
.VPSidebar .VPSidebarItem .caret-icon {
transition: transform 0.3s cubic-bezier(0.34, 1.4, 0.64, 1);
}
/* 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. */
.vp-doc a:not([class]) {
text-decoration: none;
padding-bottom: 1px;
background-image:
linear-gradient(currentColor, currentColor),
linear-gradient(
color-mix(in srgb, var(--vp-c-brand) 35%, transparent),
color-mix(in srgb, var(--vp-c-brand) 35%, transparent)
);
background-position:
0 100%,
0 100%;
background-repeat: no-repeat;
background-size:
0% 2px,
100% 1px;
transition:
color 0.25s,
background-size 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.vp-doc a:not([class]):hover,
.vp-doc a:not([class]):focus-visible {
background-size:
100% 2px,
100% 1px;
}
.vp-doc a:not([class]):has(img) {
background-image: none;
}
/* "Edit this page": a soft brand pill on hover, with the pen tilting as if it starts writing */
.VPDocFooter .edit-link-button {
margin-left: -10px;
padding: 0 12px 0 10px;
border-radius: 8px;
transition:
color 0.25s,
background-color 0.25s;
}
.VPDocFooter .edit-link-button:hover,
.VPDocFooter .edit-link-button:focus-visible {
outline: none;
background-color: color-mix(in srgb, var(--vp-c-brand) 10%, transparent);
}
.VPDocFooter .edit-link-icon {
transform-origin: 30% 70%;
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.VPDocFooter .edit-link-button:hover .edit-link-icon,
.VPDocFooter .edit-link-button:focus-visible .edit-link-icon {
transform: rotate(-14deg) scale(1.1);
}
/* Previous / next page links: Lucide's arrow-left and arrow-right icons point the way, and
on hover the card lifts with a light brand tint while its arrow moves on */
.VPDocFooter .pager-link {
transition:
border-color 0.25s,
background-color 0.25s,
transform 0.25s ease,
box-shadow 0.25s;
}
.VPDocFooter .pager-link:hover,
.VPDocFooter .pager-link:focus-visible {
outline: none;
transform: translateY(-2px);
background-color: color-mix(in srgb, var(--vp-c-brand) 6%, transparent);
box-shadow: var(--vp-shadow-2);
}
.VPDocFooter .pager-link .title {
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
font-weight: 600;
}
.VPDocFooter .pager-link.next .title {
justify-content: flex-end;
}
.VPDocFooter .pager-link.prev .title::before,
.VPDocFooter .pager-link.next .title::after {
content: '';
flex-shrink: 0;
width: 16px;
height: 16px;
background-color: currentColor;
transition: transform 0.25s ease;
}
.VPDocFooter .pager-link.prev .title::before {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m12 19-7-7 7-7'/%3E%3Cpath d='M19 12H5'/%3E%3C/svg%3E")
center / contain no-repeat;
}
.VPDocFooter .pager-link.next .title::after {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='m12 5 7 7-7 7'/%3E%3C/svg%3E")
center / contain no-repeat;
}
.VPDocFooter .pager-link.prev:hover .title::before {
transform: translateX(-4px);
}
.VPDocFooter .pager-link.next:hover .title::after {
transform: translateX(4px);
}
/* "On this page" outline: each heading is a soft pill on hover and slides in a little,
the current heading takes the brand colour and the marker glides to it */
.VPDoc .VPDocAsideOutline .outline-link {
margin: 0 -8px;
padding: 0 8px;
border-radius: 6px;
transition:
color 0.25s,
background-color 0.25s,
padding-left 0.25s ease;
}
.VPDoc .VPDocAsideOutline .outline-link:hover,
.VPDoc .VPDocAsideOutline .outline-link:focus-visible {
outline: none;
padding-left: 12px;
color: var(--vp-c-text-1);
background-color: var(--vp-c-default-soft);
}
.VPDoc .VPDocAsideOutline .outline-link.active {
color: var(--vp-c-brand-1);
}
.VPDoc .VPDocAsideOutline .outline-marker {
transition:
top 0.35s cubic-bezier(0.34, 1.3, 0.64, 1),
opacity 0.25s;
}
/* Sidebar scrollbar: a slim rounded thumb that stays quiet until the sidebar is hovered */
.VPSidebar::-webkit-scrollbar {
width: 0;
display: none;
width: 8px;
}
.VPSidebar::-webkit-scrollbar-track {
@@ -188,8 +543,27 @@ html {
}
.VPSidebar::-webkit-scrollbar-thumb {
background: transparent;
border: none;
background-color: var(--vp-c-divider);
background-clip: padding-box;
border: 2px solid transparent;
border-radius: 8px;
}
.VPSidebar:hover::-webkit-scrollbar-thumb {
background-color: var(--vp-c-text-3);
}
.VPSidebar::-webkit-scrollbar-thumb:hover,
.VPSidebar::-webkit-scrollbar-thumb:active {
background-color: var(--vp-c-brand-1);
}
/* Browsers without the scrollbar pseudo-elements, such as Firefox */
@supports not selector(::-webkit-scrollbar) {
.VPSidebar {
scrollbar-width: thin;
scrollbar-color: var(--vp-c-text-3) transparent;
}
}
.custom-block.success {
@@ -0,0 +1,20 @@
// Links with a hover underline: the nav bar menu
const UNDERLINED_LINKS = '.VPNav .VPNavBarMenuLink, .VPNav .VPNavBarMenuGroup';
// Makes the hover underline follow the mouse: it draws in from the side the pointer enters
// and leaves towards the side the pointer exits. The side is passed to the CSS as a variable.
function setUnderlineOrigin(event: PointerEvent) {
const link = event.target;
if (!(link instanceof HTMLElement) || !link.matches(UNDERLINED_LINKS)) return;
const { left, width } = link.getBoundingClientRect();
const side = event.clientX < left + width / 2 ? 'left' : 'right';
link.style.setProperty('--underline-origin', side);
}
export default function trackUnderlineDirection() {
// pointerenter and pointerleave do not bubble, so they are caught on their way down instead
document.addEventListener('pointerenter', setUnderlineOrigin, true);
document.addEventListener('pointerleave', setUnderlineOrigin, true);
}