diff --git a/package-lock.json b/package-lock.json
index 21f7769..a49392c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,8 @@
"jsdom": "^26.1.0",
"primevue": "^4.3.6",
"vite-plugin-inspect": "^11.3.0",
- "vue": "^3.5.17"
+ "vue": "^3.5.17",
+ "vue-router": "^4.5.1"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
@@ -2424,6 +2425,12 @@
"he": "^1.2.0"
}
},
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
"node_modules/@vue/devtools-core": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.7.tgz",
@@ -6305,6 +6312,21 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/vue-router": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
+ "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
"node_modules/vue-tsc": {
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
diff --git a/package.json b/package.json
index 15bfe1f..aa23439 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,8 @@
"jsdom": "^26.1.0",
"primevue": "^4.3.6",
"vite-plugin-inspect": "^11.3.0",
- "vue": "^3.5.17"
+ "vue": "^3.5.17",
+ "vue-router": "^4.5.1"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
diff --git a/src/App.vue b/src/App.vue
index ac826ea..c7eecad 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,10 +1,12 @@
-
+
+
diff --git a/src/assets/style/primevue-style/button.css b/src/assets/style/primevue-style/button.css
index 6d8fb80..f281ec1 100644
--- a/src/assets/style/primevue-style/button.css
+++ b/src/assets/style/primevue-style/button.css
@@ -64,4 +64,8 @@
--p-button-primary-focus-ring-color: var(--focus-color);
--p-button-secondary-focus-ring-color: var(--focus-color);
--p-button-danger-focus-ring-color: var(--focus-color);
+ /* link */
+ --p-button-link-color: var(--text-action-high-blue-france);
+ --p-button-link-hover-color: var(--text-action-high-blue-france);
+ --p-button-link-active-color: var(--text-action-high-blue-france);
}
diff --git a/src/components/button/IVLink.type.ts b/src/components/button/IVLink.type.ts
new file mode 100644
index 0000000..088230c
--- /dev/null
+++ b/src/components/button/IVLink.type.ts
@@ -0,0 +1,46 @@
+import type { RouteLocationRaw } from "vue-router";
+
+/**
+ * Interface representing a navigational link component.
+ * This can be used for both internal routing (via `to`) and external links (via `href`).
+ */
+export default interface IVLink {
+ /**
+ * The text label displayed for the link.
+ */
+ label: string;
+
+ /**
+ * Internal route destination using Vue Router's RouteLocationRaw.
+ * Optional – used for client-side navigation.
+ */
+ to?: RouteLocationRaw;
+
+ /**
+ * External URL for the link.
+ * Optional – used for standard anchor navigation.
+ */
+ href?: string;
+
+ /**
+ * Optional icon name or path to be displayed alongside the label.
+ */
+ icon?: string;
+
+ /**
+ * Specifies where to open the linked document (e.g., "_blank", "_self").
+ * Optional – applies to external links.
+ */
+ target?: string;
+
+ /**
+ * If true, the link is disabled and not clickable.
+ */
+ disabled?: boolean;
+
+ /**
+ * If true, the icon is displayed on the right side of the label.
+ * Defaults to false (icon on the left).
+ */
+ iconRight?: boolean;
+}
diff --git a/src/components/button/VLink.vue b/src/components/button/VLink.vue
new file mode 100644
index 0000000..c20a6c5
--- /dev/null
+++ b/src/components/button/VLink.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
diff --git a/test/VLink.spec.ts b/test/VLink.spec.ts
new file mode 100644
index 0000000..f8b2578
--- /dev/null
+++ b/test/VLink.spec.ts
@@ -0,0 +1,58 @@
+import { mount } from '@vue/test-utils'
+import VLink from '../src/components/button/VLink.vue'
+import {test, expect, describe, vi} from 'vitest'
+
+describe('VLink', () => {
+ test('renders the label correctly', () => {
+ const wrapper = mount(VLink, {
+ props: {
+ label: 'Link'
+ }
+ })
+ expect(wrapper.text()).toContain('Link');
+ })
+
+ test('renders as an anchor tag when `href` is provided', () => {
+ const wrapper = mount(VLink, {
+ props: {label: 'External', href: 'https://example.com' }
+ });
+
+ const a = wrapper.find('a')
+ expect(a.exists()).toBe(true);
+ expect(a.attributes('href')).toBe('https://example.com');
+ })
+
+ test('disables the link when `disabled` is true', () => {
+ const wrapper = mount(VLink, {
+ props: {label: 'Disabled', disabled: true }
+ })
+
+ const button = wrapper.find('.p-button');
+ expect(button.classes()).toContain('disabled');
+ expect(button.attributes('aria-disabled')).toBe('true');
+ });
+
+ test('prevents click when disabled', async () => {
+ const clickHandler = vi.fn();
+ const wrapper = mount(VLink, {
+ props: {label: 'Disabled', disabled: true, onClick: clickHandler}
+ })
+
+ await wrapper.trigger('click')
+ expect(clickHandler).not.toHaveBeenCalled();
+ });
+
+ test('places icon on the right when `iconRight` is true', () => {
+ const wrapper = mount(VLink, {
+ props: {
+ label: 'link',
+ icon: "ri-external-link-line",
+ iconRight: true,
+ }
+ })
+ const icon = wrapper.find('.p-button-icon-right');
+ expect(icon.exists()).toBe(true);
+ expect(icon.classes()).toContain('ri-external-link-line');
+ })
+})
+