+
+
-
-
-
-
- {{ $t('footer.about') }}
-
-
- {{ $t('footer.blog') }}
-
-
- {{ $t('privacy_policy.title') }}
-
-
- {{ $t('a11y.footer_title') }}
-
-
-
-
-
- {{ $t('shortcuts.section.global') }}
-
-
- -
- /
- {{ $t('shortcuts.focus_search') }}
-
- -
- ?
- {{ $t('shortcuts.show_kbd_hints') }}
-
- -
- ,
- {{ $t('shortcuts.settings') }}
-
- -
- c
- {{ $t('shortcuts.compare') }}
-
-
-
- {{ $t('shortcuts.section.search') }}
-
-
- -
- ↑/↓
- {{ $t('shortcuts.navigate_results') }}
-
- -
- Enter
- {{ $t('shortcuts.go_to_result') }}
-
-
-
- {{ $t('shortcuts.section.package') }}
-
-
- -
- .
- {{ $t('shortcuts.open_code_view') }}
-
- -
- d
- {{ $t('shortcuts.open_docs') }}
-
- -
- c
- {{ $t('shortcuts.compare_from_package') }}
-
-
-
-
-
-
- {{ $t('settings.title') }}
-
-
-
-
-
+
+
-
-
- {{ $t('footer.docs') }}
-
-
- {{ $t('footer.source') }}
-
-
- {{ $t('footer.social') }}
-
-
- {{ discord.label }}
-
+
+
+
+
+
+
{{ $t(section.title) }}
+
+
+
+
+ {{ $t(item.i18n ?? '') || item.i18n }}
+
+
+
+
+
+
+ {{ $t('shortcuts.section.global') }}
+
+
+ -
+ /
+ {{ $t('shortcuts.focus_search') }}
+
+ -
+ ?
+ {{ $t('shortcuts.show_kbd_hints') }}
+
+ -
+ ,
+ {{ $t('shortcuts.settings') }}
+
+ -
+ c
+ {{ $t('shortcuts.compare') }}
+
+
+
+ {{ $t('shortcuts.section.search') }}
+
+
+ -
+ ↑/↓
+ {{ $t('shortcuts.navigate_results') }}
+
+ -
+ Enter
+ {{ $t('shortcuts.go_to_result') }}
+
+
+
+ {{ $t('shortcuts.section.package') }}
+
+
+ -
+ .
+ {{ $t('shortcuts.open_code_view') }}
+
+ -
+ d
+ {{ $t('shortcuts.open_docs') }}
+
+ -
+ c
+ {{ $t('shortcuts.compare_from_package') }}
+
+
+
+
+
+
+ {{ $t('settings.title') }}
+
+
+
+
+
+
+ {{ $t('non_affiliation_disclaimer') }}
+ {{ $t('trademark_disclaimer') }}
+
diff --git a/app/components/Link/Base.vue b/app/components/Link/Base.vue
index 5bc5328364..4f394aa966 100644
--- a/app/components/Link/Base.vue
+++ b/app/components/Link/Base.vue
@@ -11,7 +11,7 @@ const props = withDefaults(
* */
type?: never
/** Visual style of the link */
- variant?: 'button-primary' | 'button-secondary' | 'link'
+ variant?: 'button-primary' | 'button-secondary' | 'link' | 'footer'
/** Size (only applicable for button variants) */
size?: 'small' | 'medium'
/** Makes the link take full width */
@@ -62,7 +62,8 @@ const isLinkAnchor = computed(
)
/** size is only applicable for button like links */
-const isLink = computed(() => props.variant === 'link')
+const isFooterLink = computed(() => props.variant === 'footer')
+const isLink = computed(() => props.variant === 'link' || isFooterLink.value)
const isButton = computed(() => !isLink.value)
const isButtonSmall = computed(() => props.size === 'small' && !isLink.value)
const isButtonMedium = computed(() => props.size === 'medium' && !isLink.value)
@@ -92,9 +93,11 @@ const keyboardShortcutsEnabled = useKeyboardShortcuts()
'flex': block,
'inline-flex': !block,
'underline-offset-[0.2rem] underline decoration-1 decoration-fg/30':
- !isLinkAnchor && isLink && !noUnderline,
+ !isLinkAnchor && isLink && !isFooterLink && !noUnderline,
'justify-start font-mono text-fg hover:(decoration-accent text-accent) focus-visible:(decoration-accent text-accent) transition-colors duration-200':
- isLink,
+ variant === 'link',
+ 'justify-start rounded-md px-2 py-1 font-mono text-fg-subtle transition-colors duration-200 hover:(bg-bg-subtle text-fg) focus-visible:(bg-bg-subtle text-fg)':
+ isFooterLink,
'justify-center font-mono border border-border rounded-md transition-all duration-200':
isButton,
'text-sm px-4 py-2': isButtonMedium,
diff --git a/app/components/Link/Link.stories.ts b/app/components/Link/Link.stories.ts
index 9ceeaeb7eb..23e6893e7c 100644
--- a/app/components/Link/Link.stories.ts
+++ b/app/components/Link/Link.stories.ts
@@ -63,6 +63,13 @@ export const ButtonSecondary: Story = {
},
}
+export const FooterLink: Story = {
+ args: {
+ variant: 'footer',
+ default: 'blog',
+ },
+}
+
export const SmallButton: Story = {
args: {
variant: 'button-primary',
@@ -112,6 +119,7 @@ export const Snapshot: Story = {
Anchor Link
Link with icon
Link without underline
+
blog
Disabled Link
diff --git a/app/utils/footer-navigation.ts b/app/utils/footer-navigation.ts
new file mode 100644
index 0000000000..40c787201e
--- /dev/null
+++ b/app/utils/footer-navigation.ts
@@ -0,0 +1,40 @@
+import { NPMX_DOCS_SITE } from '#shared/utils/constants'
+import type { RouteLocationRaw } from 'vue-router'
+
+export interface FooterItem {
+ to?: RouteLocationRaw
+ i18n: string
+ btn?: boolean
+}
+
+export interface FooterSection {
+ title: string
+ items: FooterItem[]
+}
+
+export const footerSections: FooterSection[] = [
+ {
+ title: 'footer.sections.resources',
+ items: [
+ { to: { name: 'blog' }, i18n: 'footer.blog' },
+ { to: { name: 'about' }, i18n: 'footer.about' },
+ { to: { name: 'accessibility' }, i18n: 'a11y.footer_title' },
+ { to: { name: 'privacy' }, i18n: 'privacy_policy.title' },
+ ],
+ },
+ {
+ title: 'footer.sections.features',
+ items: [
+ { to: { name: 'compare' }, i18n: 'shortcuts.compare' },
+ { to: { name: 'settings' }, i18n: 'shortcuts.settings' },
+ { i18n: 'footer.keyboard_shortcuts', btn: true },
+ ],
+ },
+ {
+ title: 'footer.sections.other',
+ items: [
+ { to: { name: 'pds' }, i18n: 'pds.title' },
+ { to: NPMX_DOCS_SITE, i18n: 'footer.docs' },
+ ],
+ },
+]
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 4374ceff5b..233b22228a 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -19,7 +19,12 @@
"social": "social",
"chat": "chat",
"builders_chat": "builders",
- "keyboard_shortcuts": "keyboard shortcuts"
+ "keyboard_shortcuts": "keyboard shortcuts",
+ "sections": {
+ "resources": "Resources",
+ "features": "Features",
+ "other": "Other"
+ }
},
"shortcuts": {
"section": {
diff --git a/i18n/schema.json b/i18n/schema.json
index 5842a37bca..5ef03dfa0b 100644
--- a/i18n/schema.json
+++ b/i18n/schema.json
@@ -63,6 +63,21 @@
},
"keyboard_shortcuts": {
"type": "string"
+ },
+ "sections": {
+ "type": "object",
+ "properties": {
+ "resources": {
+ "type": "string"
+ },
+ "features": {
+ "type": "string"
+ },
+ "other": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
}
},
"additionalProperties": false
From 2f8f8a76f4594c520e9b8324a0b15d8e146cfebd Mon Sep 17 00:00:00 2001
From: j0u1 <158160207+j0u1@users.noreply.github.com>
Date: Fri, 10 Apr 2026 11:56:50 +0300
Subject: [PATCH 12/12] refactor(ui): revert to p tag in footer with correct
closing tag
---
app/components/AppFooter.vue | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/components/AppFooter.vue b/app/components/AppFooter.vue
index 74088f6ecb..d900910d04 100644
--- a/app/components/AppFooter.vue
+++ b/app/components/AppFooter.vue
@@ -152,12 +152,12 @@ const socialLinks = computed(() => [
-