From 6e25d21b2fe8691cd915c4541eb4d18bae526710 Mon Sep 17 00:00:00 2001 From: PrimeTestLab Date: Fri, 30 Oct 2026 19:07:29 +0500 Subject: [PATCH] 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 Co-authored-by: Karsa --- docs/.vitepress/data/teamData.json | 8 +- .../.vitepress/theme/components/base/Card.vue | 3 +- .../theme/components/base/Checkbox.vue | 13 + .../theme/components/base/FakeInput.vue | 24 +- .../theme/components/base/Input.vue | 16 +- .../theme/components/base/OverviewLink.vue | 37 +- .../theme/components/base/RangeSlider.vue | 30 ++ .../theme/components/base/ResetButton.vue | 31 +- .../theme/components/base/Select.vue | 40 +- .../theme/components/base/Switch.vue | 39 +- .../components/home/HomeIconCustomizer.vue | 11 +- .../theme/components/home/TeamMemberCard.vue | 107 ++++- .../theme/components/icons/CategoryList.vue | 13 +- .../components/icons/CategoryListItem.vue | 53 ++- .../components/icons/IconDetailOverlay.vue | 2 + .../icons/SidebarIconCustomizer.vue | 11 +- .../components/overrides/VPCarbonAds.vue | 3 +- .../components/showcase/ShowcaseListItem.vue | 69 +++ docs/.vitepress/theme/index.ts | 5 + docs/.vitepress/theme/style.css | 394 +++++++++++++++++- .../theme/utils/underlineDirection.ts | 20 + 21 files changed, 855 insertions(+), 74 deletions(-) create mode 100644 docs/.vitepress/theme/utils/underlineDirection.ts diff --git a/docs/.vitepress/data/teamData.json b/docs/.vitepress/data/teamData.json index 4f46f57f1..07cf6a1cb 100644 --- a/docs/.vitepress/data/teamData.json +++ b/docs/.vitepress/data/teamData.json @@ -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": [ { diff --git a/docs/.vitepress/theme/components/base/Card.vue b/docs/.vitepress/theme/components/base/Card.vue index 23500f5a6..03d471851 100644 --- a/docs/.vitepress/theme/components/base/Card.vue +++ b/docs/.vitepress/theme/components/base/Card.vue @@ -31,7 +31,7 @@ const rel = computed(() => { diff --git a/docs/.vitepress/theme/components/base/Checkbox.vue b/docs/.vitepress/theme/components/base/Checkbox.vue index a13e9469a..058d9fdf5 100644 --- a/docs/.vitepress/theme/components/base/Checkbox.vue +++ b/docs/.vitepress/theme/components/base/Checkbox.vue @@ -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); +} + diff --git a/docs/.vitepress/theme/components/base/FakeInput.vue b/docs/.vitepress/theme/components/base/FakeInput.vue index a3f0fde03..c2603f6f6 100644 --- a/docs/.vitepress/theme/components/base/FakeInput.vue +++ b/docs/.vitepress/theme/components/base/FakeInput.vue @@ -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 { diff --git a/docs/.vitepress/theme/components/base/Input.vue b/docs/.vitepress/theme/components/base/Input.vue index 52e385ad1..1444b6b49 100644 --- a/docs/.vitepress/theme/components/base/Input.vue +++ b/docs/.vitepress/theme/components/base/Input.vue @@ -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); } diff --git a/docs/.vitepress/theme/components/base/OverviewLink.vue b/docs/.vitepress/theme/components/base/OverviewLink.vue index 9fb899e62..614118016 100644 --- a/docs/.vitepress/theme/components/base/OverviewLink.vue +++ b/docs/.vitepress/theme/components/base/OverviewLink.vue @@ -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 { diff --git a/docs/.vitepress/theme/components/base/RangeSlider.vue b/docs/.vitepress/theme/components/base/RangeSlider.vue index 871452650..f2be66967 100644 --- a/docs/.vitepress/theme/components/base/RangeSlider.vue +++ b/docs/.vitepress/theme/components/base/RangeSlider.vue @@ -111,6 +111,36 @@ const percentage = computed(() => `${((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 { diff --git a/docs/.vitepress/theme/components/base/ResetButton.vue b/docs/.vitepress/theme/components/base/ResetButton.vue index 8d04d8037..f078ce45d 100644 --- a/docs/.vitepress/theme/components/base/ResetButton.vue +++ b/docs/.vitepress/theme/components/base/ResetButton.vue @@ -1,11 +1,20 @@