feat(ui): add FooterLink component
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import FooterLink from './FooterLink.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Shared/FooterLink',
|
||||
component: FooterLink,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Standard footer link with arrow icon and hover effects.',
|
||||
},
|
||||
story: { inline: false },
|
||||
},
|
||||
layout: 'centered',
|
||||
},
|
||||
argTypes: {
|
||||
text: {
|
||||
control: 'text',
|
||||
description: 'Link text',
|
||||
},
|
||||
href: {
|
||||
control: 'text',
|
||||
description: 'Link URL',
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Default"
|
||||
args={{
|
||||
text: 'Google Fonts',
|
||||
href: 'https://fonts.google.com',
|
||||
target: '_blank',
|
||||
}}
|
||||
>
|
||||
{#snippet template(args: ComponentProps<typeof FooterLink>)}
|
||||
<FooterLink {...args} />
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Multiple Links">
|
||||
{#snippet template()}
|
||||
<div class="flex gap-4 p-8 bg-neutral-100 dark:bg-neutral-900 rounded-lg">
|
||||
<FooterLink text="Google Fonts" href="https://fonts.google.com" target="_blank" />
|
||||
<FooterLink text="Fontshare" href="https://www.fontshare.com" target="_blank" />
|
||||
<FooterLink text="GitHub" href="https://github.com" target="_blank" />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1,48 @@
|
||||
<!--
|
||||
Component: FooterLink
|
||||
Standard footer link with arrow icon and hover effects.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||
import clsx from 'clsx';
|
||||
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props extends HTMLAnchorAttributes {
|
||||
/**
|
||||
* Link text
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
* Link URL
|
||||
*/
|
||||
href: string;
|
||||
/**
|
||||
* CSS classes
|
||||
*/
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
text,
|
||||
href,
|
||||
class: className,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<a
|
||||
{href}
|
||||
class={clsx(
|
||||
'group inline-flex items-center gap-1 text-2xs font-mono uppercase tracking-wider-mono',
|
||||
'text-neutral-400 hover:text-brand transition-colors',
|
||||
'bg-surface/80 dark:bg-dark-bg/80 backdrop-blur-sm px-2 py-1 pointer-events-auto',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<span>{text}</span>
|
||||
<ArrowUpRightIcon
|
||||
size={10}
|
||||
class="opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200"
|
||||
/>
|
||||
</a>
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
} from '@testing-library/svelte';
|
||||
import FooterLink from './FooterLink.svelte';
|
||||
|
||||
describe('FooterLink', () => {
|
||||
const defaultProps = {
|
||||
text: 'Google Fonts',
|
||||
href: 'https://fonts.google.com',
|
||||
};
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders text content', () => {
|
||||
render(FooterLink, { props: defaultProps });
|
||||
expect(screen.getByText('Google Fonts')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders as an anchor element with correct href', () => {
|
||||
render(FooterLink, { props: defaultProps });
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute('href', 'https://fonts.google.com');
|
||||
});
|
||||
|
||||
it('renders the arrow icon', () => {
|
||||
const { container } = render(FooterLink, { props: defaultProps });
|
||||
const icon = container.querySelector('svg');
|
||||
expect(icon).toBeInTheDocument();
|
||||
expect(icon).toHaveClass('lucide-arrow-up-right');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Attributes', () => {
|
||||
it('applies custom CSS classes', () => {
|
||||
render(FooterLink, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
class: 'custom-class',
|
||||
},
|
||||
});
|
||||
expect(screen.getByRole('link')).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('spreads additional anchor attributes', () => {
|
||||
render(FooterLink, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
},
|
||||
});
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link).toHaveAttribute('rel', 'noopener');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -52,6 +52,12 @@ export {
|
||||
*/
|
||||
default as FilterGroup,
|
||||
} from './FilterGroup/FilterGroup.svelte';
|
||||
export {
|
||||
/**
|
||||
* Standard footer link with arrow icon and hover effects
|
||||
*/
|
||||
default as FooterLink,
|
||||
} from './FooterLink/FooterLink.svelte';
|
||||
export {
|
||||
/**
|
||||
* Small text for secondary meta-information
|
||||
|
||||
Reference in New Issue
Block a user