feat(shared): add cn utility for tailwind-aware class merging #38
@@ -1,53 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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,12 +52,6 @@ export {
|
|||||||
*/
|
*/
|
||||||
default as FilterGroup,
|
default as FilterGroup,
|
||||||
} from './FilterGroup/FilterGroup.svelte';
|
} from './FilterGroup/FilterGroup.svelte';
|
||||||
export {
|
|
||||||
/**
|
|
||||||
* Standard footer link with arrow icon and hover effects
|
|
||||||
*/
|
|
||||||
default as FooterLink,
|
|
||||||
} from './FooterLink/FooterLink.svelte';
|
|
||||||
export {
|
export {
|
||||||
/**
|
/**
|
||||||
* Small text for secondary meta-information
|
* Small text for secondary meta-information
|
||||||
@@ -76,6 +70,12 @@ export {
|
|||||||
*/
|
*/
|
||||||
default as Label,
|
default as Label,
|
||||||
} from './Label/Label.svelte';
|
} from './Label/Label.svelte';
|
||||||
|
export {
|
||||||
|
/**
|
||||||
|
* Styled link with optional icon
|
||||||
|
*/
|
||||||
|
default as Link,
|
||||||
|
} from './Link/Link.svelte';
|
||||||
export {
|
export {
|
||||||
/**
|
/**
|
||||||
* Full-page or component-level progress spinner
|
* Full-page or component-level progress spinner
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/lib';
|
import { cn } from '$shared/lib';
|
||||||
import type { ResponsiveManager } from '$shared/lib/helpers';
|
import type { ResponsiveManager } from '$shared/lib/helpers';
|
||||||
import { FooterLink } from '$shared/ui';
|
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
|
import FooterLink from './FooterLink.svelte';
|
||||||
|
|
||||||
const responsive = getContext<ResponsiveManager>('responsive');
|
const responsive = getContext<ResponsiveManager>('responsive');
|
||||||
const isVertical = $derived(responsive?.isDesktop || responsive?.isDesktopLarge);
|
const isVertical = $derived(responsive?.isDesktop || responsive?.isDesktopLarge);
|
||||||
@@ -41,6 +41,7 @@ const currentYear = new Date().getFullYear();
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class={cn('border border-subtle', isVertical ? 'text-2xs' : 'text-4xs')}
|
class={cn('border border-subtle', isVertical ? 'text-2xs' : 'text-4xs')}
|
||||||
|
iconClass={isVertical ? 'rotate-90' : ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
+17
-10
@@ -1,9 +1,11 @@
|
|||||||
<!--
|
<!--
|
||||||
Component: FooterLink
|
Component: FooterLink
|
||||||
Standard footer link with arrow icon and hover effects.
|
Specific footer link implementation that uses the generic Link component
|
||||||
|
and adds the default arrow icon.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/lib';
|
import { cn } from '$shared/lib';
|
||||||
|
import { Link } from '$shared/ui';
|
||||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||||
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
@@ -12,6 +14,10 @@ interface Props extends HTMLAnchorAttributes {
|
|||||||
* Link text
|
* Link text
|
||||||
*/
|
*/
|
||||||
text: string;
|
text: string;
|
||||||
|
/**
|
||||||
|
* CSS classes for the default icon
|
||||||
|
*/
|
||||||
|
iconClass?: string;
|
||||||
/**
|
/**
|
||||||
* Link URL
|
* Link URL
|
||||||
*/
|
*/
|
||||||
@@ -24,25 +30,26 @@ interface Props extends HTMLAnchorAttributes {
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
text,
|
text,
|
||||||
|
iconClass,
|
||||||
href,
|
href,
|
||||||
class: className,
|
class: className,
|
||||||
...rest
|
...rest
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<Link
|
||||||
{href}
|
{href}
|
||||||
class={cn(
|
class={className}
|
||||||
'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}
|
{...rest}
|
||||||
>
|
>
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
|
{#snippet icon()}
|
||||||
<ArrowUpRightIcon
|
<ArrowUpRightIcon
|
||||||
size={10}
|
size={10}
|
||||||
class="fill-body group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200"
|
class={cn(
|
||||||
|
'fill-body group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200',
|
||||||
|
iconClass,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</a>
|
{/snippet}
|
||||||
|
</Link>
|
||||||
Reference in New Issue
Block a user