Merge pull request 'feat(shared): add cn utility for tailwind-aware class merging' (#38) from feature/minor-improvements into main
Reviewed-on: #38
This commit was merged in pull request #38.
This commit is contained in:
@@ -219,6 +219,11 @@
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--color-brand);
|
||||||
|
color: var(--swiss-white);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
font-family: "Karla", system-ui, -apple-system, "Segoe UI", Inter, Roboto, Arial, sans-serif;
|
font-family: "Karla", system-ui, -apple-system, "Segoe UI", Inter, Roboto, Arial, sans-serif;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
import { themeManager } from '$features/ChangeAppTheme';
|
import { themeManager } from '$features/ChangeAppTheme';
|
||||||
import G from '$shared/assets/G.svg';
|
import G from '$shared/assets/G.svg';
|
||||||
import { ResponsiveProvider } from '$shared/lib';
|
import { ResponsiveProvider } from '$shared/lib';
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import { Footer } from '$widgets/Footer';
|
import { Footer } from '$widgets/Footer';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type Snippet,
|
type Snippet,
|
||||||
@@ -73,7 +73,7 @@ onDestroy(() => themeManager.destroy());
|
|||||||
<ResponsiveProvider>
|
<ResponsiveProvider>
|
||||||
<div
|
<div
|
||||||
id="app-root"
|
id="app-root"
|
||||||
class={clsx(
|
class={cn(
|
||||||
'min-h-screen w-auto flex flex-col bg-surface dark:bg-dark-bg relative',
|
'min-h-screen w-auto flex flex-col bg-surface dark:bg-dark-bg relative',
|
||||||
theme === 'dark' ? 'dark' : '',
|
theme === 'dark' ? 'dark' : '',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
Shows the skeleton snippet while loading; falls back to system font if no skeleton is provided.
|
Shows the skeleton snippet while loading; falls back to system font if no skeleton is provided.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_WEIGHT,
|
DEFAULT_FONT_WEIGHT,
|
||||||
@@ -61,7 +61,7 @@ const shouldReveal = $derived(status === 'loaded' || status === 'error');
|
|||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
style:font-family={shouldReveal ? `'${font.name}'` : 'system-ui, -apple-system, sans-serif'}
|
style:font-family={shouldReveal ? `'${font.name}'` : 'system-ui, -apple-system, sans-serif'}
|
||||||
class={clsx(className)}
|
class={cn(className)}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fontStore } from '$entities/Font';
|
import { fontStore } from '$entities/Font';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import { Button } from '$shared/ui';
|
import { Button } from '$shared/ui';
|
||||||
import { Label } from '$shared/ui';
|
import { Label } from '$shared/ui';
|
||||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||||
import clsx from 'clsx';
|
|
||||||
import {
|
import {
|
||||||
getContext,
|
getContext,
|
||||||
untrack,
|
untrack,
|
||||||
@@ -45,7 +45,7 @@ function handleReset() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'flex flex-col md:flex-row justify-between items-start md:items-center',
|
'flex flex-col md:flex-row justify-between items-start md:items-center',
|
||||||
'gap-1 md:gap-6',
|
'gap-1 md:gap-6',
|
||||||
'pt-6 mt-6 md:pt-8 md:mt-8',
|
'pt-6 mt-6 md:pt-8 md:mt-8',
|
||||||
@@ -77,7 +77,7 @@ function handleReset() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size={isMobileOrTabletPortrait ? 'xs' : 'sm'}
|
size={isMobileOrTabletPortrait ? 'xs' : 'sm'}
|
||||||
onclick={handleReset}
|
onclick={handleReset}
|
||||||
class={clsx('group font-mono tracking-widest text-neutral-400', isMobileOrTabletPortrait && 'px-0')}
|
class={cn('group font-mono tracking-widest text-neutral-400', isMobileOrTabletPortrait && 'px-0')}
|
||||||
iconPosition="left"
|
iconPosition="left"
|
||||||
>
|
>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
MULTIPLIER_S,
|
MULTIPLIER_S,
|
||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ComboControl,
|
ComboControl,
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
import Settings2Icon from '@lucide/svelte/icons/settings-2';
|
import Settings2Icon from '@lucide/svelte/icons/settings-2';
|
||||||
import XIcon from '@lucide/svelte/icons/x';
|
import XIcon from '@lucide/svelte/icons/x';
|
||||||
import { Popover } from 'bits-ui';
|
import { Popover } from 'bits-ui';
|
||||||
import clsx from 'clsx';
|
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
@@ -88,7 +88,7 @@ $effect(() => {
|
|||||||
side="top"
|
side="top"
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
class={clsx(
|
class={cn(
|
||||||
'z-50 w-72',
|
'z-50 w-72',
|
||||||
'bg-surface dark:bg-dark-card',
|
'bg-surface dark:bg-dark-card',
|
||||||
'border border-subtle',
|
'border border-subtle',
|
||||||
@@ -142,11 +142,11 @@ $effect(() => {
|
|||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class={clsx('w-full md:w-auto', className)}
|
class={cn('w-full md:w-auto', className)}
|
||||||
transition:fly={{ y: 100, duration: 200, easing: cubicOut }}
|
transition:fly={{ y: 100, duration: 200, easing: cubicOut }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'flex items-center gap-1 md:gap-2 p-1.5 md:p-2',
|
'flex items-center gap-1 md:gap-2 p-1.5 md:p-2',
|
||||||
'bg-surface/95 dark:bg-dark-bg/95 backdrop-blur-xl',
|
'bg-surface/95 dark:bg-dark-bg/95 backdrop-blur-xl',
|
||||||
'border border-subtle',
|
'border border-subtle',
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export {
|
|||||||
export {
|
export {
|
||||||
buildQueryString,
|
buildQueryString,
|
||||||
clampNumber,
|
clampNumber,
|
||||||
|
cn,
|
||||||
debounce,
|
debounce,
|
||||||
getDecimalPlaces,
|
getDecimalPlaces,
|
||||||
roundToStepPrecision,
|
roundToStepPrecision,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
correctly via the HTML element's class attribute.
|
correctly via the HTML element's class attribute.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import type {
|
import type {
|
||||||
Component,
|
Component,
|
||||||
Snippet,
|
Snippet,
|
||||||
@@ -32,7 +32,7 @@ let { icon: Icon, class: className, attrs = {} }: Props = $props();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if Icon}
|
{#if Icon}
|
||||||
{@const __iconClass__ = clsx('size-4', className)}
|
{@const __iconClass__ = cn('size-4', className)}
|
||||||
<!-- Render icon component dynamically with class prop -->
|
<!-- Render icon component dynamically with class prop -->
|
||||||
<Icon
|
<Icon
|
||||||
class={__iconClass__}
|
class={__iconClass__}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
} from 'vitest';
|
||||||
|
import { cn } from './cn';
|
||||||
|
|
||||||
|
describe('cn utility', () => {
|
||||||
|
it('should merge classes with clsx', () => {
|
||||||
|
expect(cn('class1', 'class2')).toBe('class1 class2');
|
||||||
|
expect(cn('class1', { class2: true, class3: false })).toBe('class1 class2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve tailwind specificity conflicts', () => {
|
||||||
|
// text-neutral-400 vs text-brand (text-brand should win)
|
||||||
|
expect(cn('text-neutral-400', 'text-brand')).toBe('text-brand');
|
||||||
|
|
||||||
|
// p-4 vs p-2
|
||||||
|
expect(cn('p-4', 'p-2')).toBe('p-2');
|
||||||
|
|
||||||
|
// dark mode classes should be handled correctly too
|
||||||
|
expect(cn('text-neutral-400 dark:text-neutral-400', 'text-brand dark:text-brand')).toBe(
|
||||||
|
'text-brand dark:text-brand',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined and null inputs', () => {
|
||||||
|
expect(cn('class1', undefined, null, 'class2')).toBe('class1 class2');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {
|
||||||
|
type ClassValue,
|
||||||
|
clsx,
|
||||||
|
} from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for merging Tailwind classes with clsx and tailwind-merge.
|
||||||
|
* This resolves specificity conflicts between Tailwind classes.
|
||||||
|
*/
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ export {
|
|||||||
type QueryParamValue,
|
type QueryParamValue,
|
||||||
} from './buildQueryString/buildQueryString';
|
} from './buildQueryString/buildQueryString';
|
||||||
export { clampNumber } from './clampNumber/clampNumber';
|
export { clampNumber } from './clampNumber/clampNumber';
|
||||||
|
export { cn } from './cn';
|
||||||
export { debounce } from './debounce/debounce';
|
export { debounce } from './debounce/debounce';
|
||||||
export { getDecimalPlaces } from './getDecimalPlaces/getDecimalPlaces';
|
export { getDecimalPlaces } from './getDecimalPlaces/getDecimalPlaces';
|
||||||
export { getSkeletonWidth } from './getSkeletonWidth/getSkeletonWidth';
|
export { getSkeletonWidth } from './getSkeletonWidth/getSkeletonWidth';
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
Pill badge with border and optional status dot.
|
Pill badge with border and optional status dot.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import {
|
import {
|
||||||
type LabelSize,
|
type LabelSize,
|
||||||
labelSizeConfig,
|
labelSizeConfig,
|
||||||
} from '$shared/ui/Label/config';
|
} from '$shared/ui/Label/config';
|
||||||
import clsx from 'clsx';
|
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ let {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class={clsx(
|
class={cn(
|
||||||
'inline-flex items-center gap-1 px-2 py-0.5 border rounded-full',
|
'inline-flex items-center gap-1 px-2 py-0.5 border rounded-full',
|
||||||
'font-mono uppercase tracking-wide',
|
'font-mono uppercase tracking-wide',
|
||||||
labelSizeConfig[size],
|
labelSizeConfig[size],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
design-system button. Uppercase, zero border-radius, Space Grotesk.
|
design-system button. Uppercase, zero border-radius, Space Grotesk.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||||
import type {
|
import type {
|
||||||
@@ -71,7 +71,7 @@ let {
|
|||||||
const isIconOnly = $derived(!!icon && !children);
|
const isIconOnly = $derived(!!icon && !children);
|
||||||
|
|
||||||
const variantStyles: Record<ButtonVariant, string> = {
|
const variantStyles: Record<ButtonVariant, string> = {
|
||||||
primary: clsx(
|
primary: cn(
|
||||||
'bg-swiss-red text-white',
|
'bg-swiss-red text-white',
|
||||||
'hover:bg-swiss-red/90',
|
'hover:bg-swiss-red/90',
|
||||||
'active:bg-swiss-red/80',
|
'active:bg-swiss-red/80',
|
||||||
@@ -87,7 +87,7 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
'disabled:transform-none',
|
'disabled:transform-none',
|
||||||
),
|
),
|
||||||
secondary: clsx(
|
secondary: cn(
|
||||||
'bg-surface dark:bg-paper',
|
'bg-surface dark:bg-paper',
|
||||||
'text-swiss-black dark:text-neutral-200',
|
'text-swiss-black dark:text-neutral-200',
|
||||||
'border border-black/10 dark:border-white/10',
|
'border border-black/10 dark:border-white/10',
|
||||||
@@ -98,7 +98,7 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
),
|
),
|
||||||
outline: clsx(
|
outline: cn(
|
||||||
'bg-transparent',
|
'bg-transparent',
|
||||||
'text-swiss-black dark:text-neutral-200',
|
'text-swiss-black dark:text-neutral-200',
|
||||||
'border border-black/20 dark:border-white/20',
|
'border border-black/20 dark:border-white/20',
|
||||||
@@ -109,7 +109,7 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
),
|
),
|
||||||
ghost: clsx(
|
ghost: cn(
|
||||||
'bg-transparent',
|
'bg-transparent',
|
||||||
'text-secondary',
|
'text-secondary',
|
||||||
'border border-transparent',
|
'border border-transparent',
|
||||||
@@ -119,7 +119,7 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
),
|
),
|
||||||
icon: clsx(
|
icon: cn(
|
||||||
'bg-surface dark:bg-dark-bg',
|
'bg-surface dark:bg-dark-bg',
|
||||||
'text-secondary',
|
'text-secondary',
|
||||||
'border border-transparent',
|
'border border-transparent',
|
||||||
@@ -130,8 +130,8 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||||||
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
'disabled:text-neutral-400 dark:disabled:text-neutral-600',
|
||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
),
|
),
|
||||||
tertiary: clsx(
|
tertiary: cn(
|
||||||
// Font override — must come after base in clsx() to win via tailwind-merge
|
// Font override — must come after base in cn() to win via tailwind-merge
|
||||||
'font-secondary font-medium normal-case tracking-normal',
|
'font-secondary font-medium normal-case tracking-normal',
|
||||||
// Inactive state
|
// Inactive state
|
||||||
'bg-transparent',
|
'bg-transparent',
|
||||||
@@ -168,14 +168,13 @@ const iconSizeStyles: Record<ButtonSize, string> = {
|
|||||||
|
|
||||||
const activeStyles: Partial<Record<ButtonVariant, string>> = {
|
const activeStyles: Partial<Record<ButtonVariant, string>> = {
|
||||||
secondary: 'bg-paper dark:bg-paper shadow-sm border-black/20 dark:border-white/20',
|
secondary: 'bg-paper dark:bg-paper shadow-sm border-black/20 dark:border-white/20',
|
||||||
tertiary:
|
tertiary: 'bg-paper dark:bg-dark-card border-black/10 dark:border-white/10 shadow-sm text-brand dark:text-brand',
|
||||||
'bg-paper dark:bg-dark-card border-black/10 dark:border-white/10 shadow-sm text-neutral-900 dark:text-neutral-100',
|
|
||||||
ghost: 'bg-transparent dark:bg-transparent text-brand dark:text-brand',
|
ghost: 'bg-transparent dark:bg-transparent text-brand dark:text-brand',
|
||||||
outline: 'bg-surface dark:bg-paper border-brand',
|
outline: 'bg-surface dark:bg-paper border-brand',
|
||||||
icon: 'bg-paper dark:bg-paper text-brand border-subtle',
|
icon: 'bg-paper dark:bg-paper text-brand border-subtle',
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = $derived(clsx(
|
const classes = $derived(cn(
|
||||||
// Base
|
// Base
|
||||||
'inline-flex items-center justify-center',
|
'inline-flex items-center justify-center',
|
||||||
'font-primary font-bold tracking-tight uppercase',
|
'font-primary font-bold tracking-tight uppercase',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
Use for segmented controls, view toggles, or any mutually exclusive button set.
|
Use for segmented controls, view toggles, or any mutually exclusive button set.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ let { children, class: className, ...rest }: Props = $props();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'flex items-center gap-1 p-1',
|
'flex items-center gap-1 p-1',
|
||||||
'bg-surface dark:bg-dark-bg',
|
'bg-surface dark:bg-dark-bg',
|
||||||
'border border-subtle',
|
'border border-subtle',
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TypographyControl } from '$shared/lib';
|
import type { TypographyControl } from '$shared/lib';
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import { Slider } from '$shared/ui';
|
import { Slider } from '$shared/ui';
|
||||||
import { Button } from '$shared/ui/Button';
|
import { Button } from '$shared/ui/Button';
|
||||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||||
import { Popover } from 'bits-ui';
|
import { Popover } from 'bits-ui';
|
||||||
import clsx from 'clsx';
|
|
||||||
import TechText from '../TechText/TechText.svelte';
|
import TechText from '../TechText/TechText.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -78,7 +78,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
|
|||||||
-->
|
-->
|
||||||
{#if reduced}
|
{#if reduced}
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'flex gap-4 items-end w-full',
|
'flex gap-4 items-end w-full',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@@ -98,7 +98,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
|
|||||||
|
|
||||||
<!-- ── FULL MODE ──────────────────────────────────────────────────────────────── -->
|
<!-- ── FULL MODE ──────────────────────────────────────────────────────────────── -->
|
||||||
{:else}
|
{:else}
|
||||||
<div class={clsx('flex items-center px-1 relative', className)}>
|
<div class={cn('flex items-center px-1 relative', className)}>
|
||||||
<!-- Decrease button -->
|
<!-- Decrease button -->
|
||||||
<Button
|
<Button
|
||||||
variant="icon"
|
variant="icon"
|
||||||
@@ -119,7 +119,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
|
|||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
class={clsx(
|
class={cn(
|
||||||
'flex flex-col items-center justify-center w-14 py-1',
|
'flex flex-col items-center justify-center w-14 py-1',
|
||||||
'select-none rounded-none transition-all duration-150',
|
'select-none rounded-none transition-all duration-150',
|
||||||
'border border-transparent',
|
'border border-transparent',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Labeled container for form controls
|
Labeled container for form controls
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,7 +24,7 @@ interface Props {
|
|||||||
const { label, children, class: className }: Props = $props();
|
const { label, children, class: className }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={clsx('flex flex-col gap-3 py-6 border-b border-subtle last:border-0', className)}>
|
<div class={cn('flex flex-col gap-3 py-6 border-b border-subtle last:border-0', className)}>
|
||||||
<div class="flex justify-between items-center text-xs font-primary font-bold tracking-tight text-neutral-900 dark:text-neutral-100 uppercase leading-none">
|
<div class="flex justify-between items-center text-xs font-primary font-bold tracking-tight text-neutral-900 dark:text-neutral-100 uppercase leading-none">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
1px separator line, horizontal or vertical.
|
1px separator line, horizontal or vertical.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +24,7 @@ let {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'bg-black/10 dark:bg-white/10',
|
'bg-black/10 dark:bg-white/10',
|
||||||
orientation === 'horizontal' ? 'w-full h-px' : 'w-px h-full',
|
orientation === 'horizontal' ? 'w-full h-px' : 'w-px h-full',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Filter } from '$shared/lib';
|
import type { Filter } from '$shared/lib';
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import { Button } from '$shared/ui';
|
import { Button } from '$shared/ui';
|
||||||
import { Label } from '$shared/ui';
|
import { Label } from '$shared/ui';
|
||||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||||
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
||||||
import clsx from 'clsx';
|
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import {
|
import {
|
||||||
draw,
|
draw,
|
||||||
@@ -68,7 +68,7 @@ $effect(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class={clsx('flex flex-col', className)}>
|
<div class={cn('flex flex-col', className)}>
|
||||||
<Label
|
<Label
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
Provides classes for styling footnotes
|
Provides classes for styling footnotes
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -26,14 +26,14 @@ const { children, class: className, render }: Props = $props();
|
|||||||
|
|
||||||
{#if render}
|
{#if render}
|
||||||
{@render render({
|
{@render render({
|
||||||
class: clsx(
|
class: cn(
|
||||||
'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft',
|
'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft',
|
||||||
className,
|
className,
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
{:else if children}
|
{:else if children}
|
||||||
<span
|
<span
|
||||||
class={clsx(
|
class={cn(
|
||||||
'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft',
|
'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
design-system input. Zero border-radius, Space Grotesk, precise states.
|
design-system input. Zero border-radius, Space Grotesk, precise states.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import XIcon from '@lucide/svelte/icons/x';
|
import XIcon from '@lucide/svelte/icons/x';
|
||||||
import clsx from 'clsx';
|
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||||
@@ -90,7 +90,7 @@ const hasRightSlot = $derived(!!rightIcon || showClearButton);
|
|||||||
const cfg = $derived(inputSizeConfig[size]);
|
const cfg = $derived(inputSizeConfig[size]);
|
||||||
const styles = $derived(inputVariantConfig[variant]);
|
const styles = $derived(inputVariantConfig[variant]);
|
||||||
|
|
||||||
const inputClasses = $derived(clsx(
|
const inputClasses = $derived(cn(
|
||||||
'font-primary rounded-none outline-none transition-all duration-200',
|
'font-primary rounded-none outline-none transition-all duration-200',
|
||||||
'text-neutral-900 dark:text-neutral-100',
|
'text-neutral-900 dark:text-neutral-100',
|
||||||
'placeholder:text-neutral-400 dark:placeholder:text-neutral-600',
|
'placeholder:text-neutral-400 dark:placeholder:text-neutral-600',
|
||||||
@@ -107,8 +107,8 @@ const inputClasses = $derived(clsx(
|
|||||||
));
|
));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={clsx('flex flex-col gap-1', fullWidth && 'w-full')}>
|
<div class={cn('flex flex-col gap-1', fullWidth && 'w-full')}>
|
||||||
<div class={clsx('relative group', fullWidth && 'w-full')}>
|
<div class={cn('relative group', fullWidth && 'w-full')}>
|
||||||
<!-- Left icon slot -->
|
<!-- Left icon slot -->
|
||||||
{#if leftIcon}
|
{#if leftIcon}
|
||||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-600 pointer-events-none z-10 flex items-center">
|
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-600 pointer-events-none z-10 flex items-center">
|
||||||
@@ -147,7 +147,7 @@ const inputClasses = $derived(clsx(
|
|||||||
<!-- Helper / error text -->
|
<!-- Helper / error text -->
|
||||||
{#if helperText}
|
{#if helperText}
|
||||||
<span
|
<span
|
||||||
class={clsx(
|
class={cn(
|
||||||
'text-2xs font-mono tracking-wide px-1',
|
'text-2xs font-mono tracking-wide px-1',
|
||||||
error ? 'text-brand ' : 'text-secondary',
|
error ? 'text-brand ' : 'text-secondary',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Inline monospace label. The base primitive for all micrographic text.
|
Inline monospace label. The base primitive for all micrographic text.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import {
|
import {
|
||||||
type LabelFont,
|
type LabelFont,
|
||||||
@@ -72,7 +72,7 @@ let {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class={clsx(
|
class={cn(
|
||||||
'font-mono tracking-widest leading-none',
|
'font-mono tracking-widest leading-none',
|
||||||
'inline-flex items-center gap-1.5',
|
'inline-flex items-center gap-1.5',
|
||||||
font === 'primary' && 'font-primary tracking-tight',
|
font === 'primary' && 'font-primary tracking-tight',
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<script module lang="ts">
|
||||||
|
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import Link from './Link.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Shared/Link',
|
||||||
|
component: Link,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
'Styled link component based on the footer link design. Supports optional icon snippet and standard anchor attributes.',
|
||||||
|
},
|
||||||
|
story: { inline: false },
|
||||||
|
},
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
href: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Link URL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Default"
|
||||||
|
args={{
|
||||||
|
href: 'https://fonts.google.com',
|
||||||
|
target: '_blank',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#snippet template(args: ComponentProps<typeof Link>)}
|
||||||
|
<Link {...args}>
|
||||||
|
<span>Google Fonts</span>
|
||||||
|
</Link>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="With Icon"
|
||||||
|
args={{
|
||||||
|
href: 'https://fonts.google.com',
|
||||||
|
target: '_blank',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#snippet template(args: ComponentProps<typeof Link>)}
|
||||||
|
<Link {...args}>
|
||||||
|
<span>Google Fonts</span>
|
||||||
|
{#snippet icon()}
|
||||||
|
<ArrowUpRightIcon
|
||||||
|
size={10}
|
||||||
|
class="fill-body group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200"
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</Link>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Multiple Links">
|
||||||
|
{#snippet template()}
|
||||||
|
<div class="flex gap-4 p-8 bg-neutral-100 dark:bg-neutral-900 rounded-lg">
|
||||||
|
<Link href="https://fonts.google.com" target="_blank">
|
||||||
|
<span>Google Fonts</span>
|
||||||
|
{#snippet icon()}
|
||||||
|
<ArrowUpRightIcon
|
||||||
|
size={10}
|
||||||
|
class="fill-body group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200"
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</Link>
|
||||||
|
<Link href="https://www.fontshare.com" target="_blank">
|
||||||
|
<span>Fontshare</span>
|
||||||
|
{#snippet icon()}
|
||||||
|
<ArrowUpRightIcon
|
||||||
|
size={10}
|
||||||
|
class="fill-body group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200"
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</Link>
|
||||||
|
<Link href="https://github.com" target="_blank">
|
||||||
|
<span>GitHub</span>
|
||||||
|
{#snippet icon()}
|
||||||
|
<ArrowUpRightIcon
|
||||||
|
size={10}
|
||||||
|
class="fill-body group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200"
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Story>
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
<!--
|
<!--
|
||||||
Component: FooterLink
|
Component: Link
|
||||||
Standard footer link with arrow icon and hover effects.
|
A styled link component based on the footer link design.
|
||||||
|
Supports optional icon snippet and standard anchor attributes.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
import { cn } from '$shared/lib';
|
||||||
import clsx from 'clsx';
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
interface Props extends HTMLAnchorAttributes {
|
interface Props extends HTMLAnchorAttributes {
|
||||||
/**
|
/**
|
||||||
* Link text
|
* Link content
|
||||||
*/
|
*/
|
||||||
text: string;
|
children?: Snippet;
|
||||||
/**
|
/**
|
||||||
* Link URL
|
* Optional icon snippet
|
||||||
*/
|
*/
|
||||||
href: string;
|
icon?: Snippet;
|
||||||
/**
|
/**
|
||||||
* CSS classes
|
* CSS classes
|
||||||
*/
|
*/
|
||||||
@@ -23,16 +24,15 @@ interface Props extends HTMLAnchorAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
text,
|
children,
|
||||||
href,
|
icon,
|
||||||
class: className,
|
class: className,
|
||||||
...rest
|
...rest
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
{href}
|
class={cn(
|
||||||
class={clsx(
|
|
||||||
'group inline-flex items-center gap-1 text-2xs font-mono uppercase tracking-wider-mono',
|
'group inline-flex items-center gap-1 text-2xs font-mono uppercase tracking-wider-mono',
|
||||||
'text-neutral-400 hover:text-brand transition-colors',
|
'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',
|
'bg-surface/80 dark:bg-dark-bg/80 backdrop-blur-sm px-2 py-1 pointer-events-auto',
|
||||||
@@ -40,9 +40,6 @@ let {
|
|||||||
)}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<span>{text}</span>
|
{@render children?.()}
|
||||||
<ArrowUpRightIcon
|
{@render icon?.()}
|
||||||
size={10}
|
|
||||||
class="opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
+39
-10
@@ -2,29 +2,58 @@ import {
|
|||||||
render,
|
render,
|
||||||
screen,
|
screen,
|
||||||
} from '@testing-library/svelte';
|
} from '@testing-library/svelte';
|
||||||
import FooterLink from './FooterLink.svelte';
|
import { createRawSnippet } from 'svelte';
|
||||||
|
import Link from './Link.svelte';
|
||||||
|
|
||||||
describe('FooterLink', () => {
|
/**
|
||||||
|
* Helper to create a plain text snippet
|
||||||
|
*/
|
||||||
|
function textSnippet(text: string) {
|
||||||
|
return createRawSnippet(() => ({
|
||||||
|
render: () => `<span>${text}</span>`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create an icon snippet
|
||||||
|
*/
|
||||||
|
function iconSnippet() {
|
||||||
|
return createRawSnippet(() => ({
|
||||||
|
render: () => `<svg class="lucide-arrow-up-right"></svg>`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Link', () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
text: 'Google Fonts',
|
|
||||||
href: 'https://fonts.google.com',
|
href: 'https://fonts.google.com',
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('renders text content', () => {
|
it('renders text content via children snippet', () => {
|
||||||
render(FooterLink, { props: defaultProps });
|
render(Link, {
|
||||||
|
props: {
|
||||||
|
...defaultProps,
|
||||||
|
children: textSnippet('Google Fonts'),
|
||||||
|
},
|
||||||
|
});
|
||||||
expect(screen.getByText('Google Fonts')).toBeInTheDocument();
|
expect(screen.getByText('Google Fonts')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders as an anchor element with correct href', () => {
|
it('renders as an anchor element with correct href', () => {
|
||||||
render(FooterLink, { props: defaultProps });
|
render(Link, { props: defaultProps });
|
||||||
const link = screen.getByRole('link');
|
const link = screen.getByRole('link');
|
||||||
expect(link).toBeInTheDocument();
|
expect(link).toBeInTheDocument();
|
||||||
expect(link).toHaveAttribute('href', 'https://fonts.google.com');
|
expect(link).toHaveAttribute('href', 'https://fonts.google.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the arrow icon', () => {
|
it('renders the icon when provided via snippet', () => {
|
||||||
const { container } = render(FooterLink, { props: defaultProps });
|
const { container } = render(Link, {
|
||||||
|
props: {
|
||||||
|
...defaultProps,
|
||||||
|
children: textSnippet('Google Fonts'),
|
||||||
|
icon: iconSnippet(),
|
||||||
|
},
|
||||||
|
});
|
||||||
const icon = container.querySelector('svg');
|
const icon = container.querySelector('svg');
|
||||||
expect(icon).toBeInTheDocument();
|
expect(icon).toBeInTheDocument();
|
||||||
expect(icon).toHaveClass('lucide-arrow-up-right');
|
expect(icon).toHaveClass('lucide-arrow-up-right');
|
||||||
@@ -33,7 +62,7 @@ describe('FooterLink', () => {
|
|||||||
|
|
||||||
describe('Attributes', () => {
|
describe('Attributes', () => {
|
||||||
it('applies custom CSS classes', () => {
|
it('applies custom CSS classes', () => {
|
||||||
render(FooterLink, {
|
render(Link, {
|
||||||
props: {
|
props: {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
class: 'custom-class',
|
class: 'custom-class',
|
||||||
@@ -43,7 +72,7 @@ describe('FooterLink', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('spreads additional anchor attributes', () => {
|
it('spreads additional anchor attributes', () => {
|
||||||
render(FooterLink, {
|
render(Link, {
|
||||||
props: {
|
props: {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
Project logo with apropriate styles
|
Project logo with apropriate styles
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import { Badge } from '$shared/ui';
|
import { Badge } from '$shared/ui';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -18,7 +18,7 @@ const { class: className }: Props = $props();
|
|||||||
const title = 'GLYPHDIFF';
|
const title = 'GLYPHDIFF';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={clsx('flex items-center gap-2 md:gap-3 select-none', className)}>
|
<div class={cn('flex items-center gap-2 md:gap-3 select-none', className)}>
|
||||||
<h1 class="font-logo font-extrabold text-base md:text-xl tracking-tight text-swiss-black dark:text-neutral-200">
|
<h1 class="font-logo font-extrabold text-base md:text-xl tracking-tight text-swiss-black dark:text-neutral-200">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PerspectiveManager } from '$shared/lib';
|
import type { PerspectiveManager } from '$shared/lib';
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import { type Snippet } from 'svelte';
|
import { type Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -73,7 +73,7 @@ const isVisible = $derived(manager.isFront);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx('will-change-transform', className)}
|
class={cn('will-change-transform', className)}
|
||||||
style:transform-style="preserve-3d"
|
style:transform-style="preserve-3d"
|
||||||
style:transform={style?.transform}
|
style:transform={style?.transform}
|
||||||
style:filter={style?.filter}
|
style:filter={style?.filter}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
Numbered section heading with optional subtitle and pulse dot.
|
Numbered section heading with optional subtitle and pulse dot.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import { Label } from '$shared/ui';
|
import { Label } from '$shared/ui';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +41,7 @@ let {
|
|||||||
const indexStr = $derived(String(index).padStart(2, '0'));
|
const indexStr = $derived(String(index).padStart(2, '0'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={clsx('flex items-center gap-3 md:gap-4 mb-2', className)}>
|
<div class={cn('flex items-center gap-3 md:gap-4 mb-2', className)}>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if pulse}
|
{#if pulse}
|
||||||
<span class="w-1.5 h-1.5 bg-brand rounded-full animate-pulse"></span>
|
<span class="w-1.5 h-1.5 bg-brand rounded-full animate-pulse"></span>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
A horizontal separator line used to visually separate sections within a page.
|
A horizontal separator line used to visually separate sections within a page.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -15,4 +15,4 @@ interface Props {
|
|||||||
const { class: className = '' }: Props = $props();
|
const { class: className = '' }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={clsx('w-full h-px bg-swiss-black/5 dark:bg-white/10 my-8 md:my-12', className)}></div>
|
<div class={cn('w-full h-px bg-swiss-black/5 dark:bg-white/10 my-8 md:my-12', className)}></div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
@@ -79,7 +79,7 @@ function close() {
|
|||||||
The inner div stays w-80 so Sidebar layout never reflows mid-animation.
|
The inner div stays w-80 so Sidebar layout never reflows mid-animation.
|
||||||
-->
|
-->
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'shrink-0 z-30 h-full relative',
|
'shrink-0 z-30 h-full relative',
|
||||||
'overflow-hidden',
|
'overflow-hidden',
|
||||||
'will-change-[width]',
|
'will-change-[width]',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Generic loading placeholder with shimmer animation.
|
Generic loading placeholder with shimmer animation.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
@@ -18,7 +18,7 @@ let { class: className, animate = true, ...rest }: Props = $props();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'rounded-md bg-background-subtle/50 backdrop-blur-sm',
|
'rounded-md bg-background-subtle/50 backdrop-blur-sm',
|
||||||
animate && 'animate-pulse',
|
animate && 'animate-pulse',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
A single key:value pair in Space Mono. Optional trailing divider.
|
A single key:value pair in Space Mono. Optional trailing divider.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import { Label } from '$shared/ui';
|
import { Label } from '$shared/ui';
|
||||||
import clsx from 'clsx';
|
|
||||||
import type { ComponentProps } from 'svelte';
|
import type { ComponentProps } from 'svelte';
|
||||||
|
|
||||||
interface Props extends Pick<ComponentProps<typeof Label>, 'variant'> {
|
interface Props extends Pick<ComponentProps<typeof Label>, 'variant'> {
|
||||||
@@ -36,7 +36,7 @@ let {
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={clsx('flex items-center gap-1', className)}>
|
<div class={cn('flex items-center gap-1', className)}>
|
||||||
<Label variant="muted" size="xs">{label}:</Label>
|
<Label variant="muted" size="xs">{label}:</Label>
|
||||||
<Label {variant} size="xs" bold>{value}</Label>
|
<Label {variant} size="xs" bold>{value}</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
Renders multiple Stat components in a row with auto-separators.
|
Renders multiple Stat components in a row with auto-separators.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import { Stat } from '$shared/ui';
|
import { Stat } from '$shared/ui';
|
||||||
import clsx from 'clsx';
|
|
||||||
import type { ComponentProps } from 'svelte';
|
import type { ComponentProps } from 'svelte';
|
||||||
|
|
||||||
interface StatItem extends Partial<Pick<ComponentProps<typeof Stat>, 'variant'>> {
|
interface StatItem extends Partial<Pick<ComponentProps<typeof Stat>, 'variant'>> {
|
||||||
@@ -26,7 +26,7 @@ interface Props {
|
|||||||
let { stats, class: className }: Props = $props();
|
let { stats, class: className }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={clsx('flex items-center gap-4', className)}>
|
<div class={cn('flex items-center gap-4', className)}>
|
||||||
{#each stats as stat, i}
|
{#each stats as stat, i}
|
||||||
<Stat
|
<Stat
|
||||||
label={stat.label}
|
label={stat.label}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
Monospace <code> element for technical values, measurements, identifiers.
|
Monospace <code> element for technical values, measurements, identifiers.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import {
|
import {
|
||||||
type LabelSize,
|
type LabelSize,
|
||||||
type LabelVariant,
|
type LabelVariant,
|
||||||
labelSizeConfig,
|
labelSizeConfig,
|
||||||
labelVariantConfig,
|
labelVariantConfig,
|
||||||
} from '$shared/ui/Label/config';
|
} from '$shared/ui/Label/config';
|
||||||
import clsx from 'clsx';
|
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -42,7 +42,7 @@ let {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<code
|
<code
|
||||||
class={clsx(
|
class={cn(
|
||||||
'font-mono tracking-tight tabular-nums',
|
'font-mono tracking-tight tabular-nums',
|
||||||
labelSizeConfig[size],
|
labelSizeConfig[size],
|
||||||
labelVariantConfig[variant],
|
labelVariantConfig[variant],
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts" generics="T">
|
<script lang="ts" generics="T">
|
||||||
import { createVirtualizer } from '$shared/lib';
|
import { createVirtualizer } from '$shared/lib';
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import { throttle } from '$shared/lib/utils';
|
import { throttle } from '$shared/lib/utils';
|
||||||
import clsx from 'clsx';
|
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
@@ -324,13 +324,13 @@ $effect(() => {
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#if useWindowScroll}
|
{#if useWindowScroll}
|
||||||
<div class={clsx('relative w-full', className)} bind:this={viewportRef} {...rest}>
|
<div class={cn('relative w-full', className)} bind:this={viewportRef} {...rest}>
|
||||||
{@render content()}
|
{@render content()}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
bind:this={viewportRef}
|
bind:this={viewportRef}
|
||||||
class={clsx(
|
class={cn(
|
||||||
'relative overflow-y-auto overflow-x-hidden',
|
'relative overflow-y-auto overflow-x-hidden',
|
||||||
'rounded-md bg-background',
|
'rounded-md bg-background',
|
||||||
'w-full',
|
'w-full',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { typographySettingsStore } from '$features/SetupFont';
|
import { typographySettingsStore } from '$features/SetupFont';
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import { comparisonStore } from '../../model';
|
import { comparisonStore } from '../../model';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -53,7 +53,7 @@ $effect(() => {
|
|||||||
>
|
>
|
||||||
{#each [0, 1] as s (s)}
|
{#each [0, 1] as s (s)}
|
||||||
<span
|
<span
|
||||||
class={clsx(
|
class={cn(
|
||||||
'char-inner',
|
'char-inner',
|
||||||
'transition-colors duration-300',
|
'transition-colors duration-300',
|
||||||
isPast
|
isPast
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ThemeSwitch } from '$features/ChangeAppTheme';
|
import { ThemeSwitch } from '$features/ChangeAppTheme';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import {
|
import {
|
||||||
Divider,
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
} from '$shared/ui';
|
} from '$shared/ui';
|
||||||
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
|
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
|
||||||
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
|
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
|
||||||
import clsx from 'clsx';
|
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { comparisonStore } from '../../model';
|
import { comparisonStore } from '../../model';
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ const fontBName = $derived(comparisonStore.fontB?.name ?? '');
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
class={clsx(
|
class={cn(
|
||||||
'flex items-center justify-between',
|
'flex items-center justify-between',
|
||||||
'px-4 md:px-8 py-4 md:py-6',
|
'px-4 md:px-8 py-4 md:py-6',
|
||||||
'h-16 md:h-20 z-20',
|
'h-16 md:h-20 z-20',
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
Content (font list, controls) is injected via snippets.
|
Content (font list, controls) is injected via snippets.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import {
|
import {
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Label,
|
Label,
|
||||||
ToggleButton,
|
ToggleButton,
|
||||||
} from '$shared/ui';
|
} from '$shared/ui';
|
||||||
import clsx from 'clsx';
|
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import {
|
import {
|
||||||
type Side,
|
type Side,
|
||||||
@@ -40,7 +40,7 @@ let {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'flex flex-col h-full',
|
'flex flex-col h-full',
|
||||||
'w-80',
|
'w-80',
|
||||||
'bg-surface dark:bg-dark-bg',
|
'bg-surface dark:bg-dark-bg',
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import {
|
|||||||
type ResponsiveManager,
|
type ResponsiveManager,
|
||||||
debounce,
|
debounce,
|
||||||
} from '$shared/lib';
|
} from '$shared/lib';
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import {
|
import {
|
||||||
CharacterComparisonEngine,
|
CharacterComparisonEngine,
|
||||||
} from '$shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte';
|
} from '$shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte';
|
||||||
import { Loader } from '$shared/ui';
|
import { Loader } from '$shared/ui';
|
||||||
import clsx from 'clsx';
|
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { Spring } from 'svelte/motion';
|
import { Spring } from 'svelte/motion';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
@@ -61,6 +61,26 @@ const comparisonEngine = new CharacterComparisonEngine();
|
|||||||
|
|
||||||
let layoutResult = $state<ReturnType<typeof comparisonEngine.layout>>({ lines: [], totalHeight: 0 });
|
let layoutResult = $state<ReturnType<typeof comparisonEngine.layout>>({ lines: [], totalHeight: 0 });
|
||||||
|
|
||||||
|
// Track container width changes (window resize, sidebar toggle, etc.)
|
||||||
|
$effect(() => {
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(entries => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
// Use borderBoxSize if available, fallback to contentRect
|
||||||
|
const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
|
||||||
|
if (width > 0) {
|
||||||
|
containerWidth = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(container);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
const sliderSpring = new Spring(50, {
|
const sliderSpring = new Spring(50, {
|
||||||
stiffness: 0.2,
|
stiffness: 0.2,
|
||||||
damping: 0.7,
|
damping: 0.7,
|
||||||
@@ -124,25 +144,25 @@ $effect(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Layout effect — depends on content, settings AND containerWidth
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const _text = comparisonStore.text;
|
const _text = comparisonStore.text;
|
||||||
const _weight = typography.weight;
|
const _weight = typography.weight;
|
||||||
const _size = typography.renderedSize;
|
const _size = typography.renderedSize;
|
||||||
const _height = typography.height;
|
const _height = typography.height;
|
||||||
const _spacing = typography.spacing;
|
const _spacing = typography.spacing;
|
||||||
|
const _width = containerWidth;
|
||||||
|
const _isMobile = isMobile;
|
||||||
|
|
||||||
if (container && fontA && fontB) {
|
if (container && fontA && fontB && _width > 0) {
|
||||||
// PRETEXT API strings: "weight sizepx family"
|
// PRETEXT API strings: "weight sizepx family"
|
||||||
const fontAStr = getPretextFontString(_weight, _size, fontA.name);
|
const fontAStr = getPretextFontString(_weight, _size, fontA.name);
|
||||||
const fontBStr = getPretextFontString(_weight, _size, fontB.name);
|
const fontBStr = getPretextFontString(_weight, _size, fontB.name);
|
||||||
|
|
||||||
// Use offsetWidth to avoid transform scaling issues
|
const padding = _isMobile ? 48 : 96;
|
||||||
const width = container.offsetWidth;
|
const availableWidth = Math.max(0, _width - padding);
|
||||||
const padding = isMobile ? 48 : 96;
|
|
||||||
const availableWidth = width - padding;
|
|
||||||
const lineHeight = _size * _height;
|
const lineHeight = _size * _height;
|
||||||
|
|
||||||
containerWidth = width;
|
|
||||||
layoutResult = comparisonEngine.layout(
|
layoutResult = comparisonEngine.layout(
|
||||||
_text,
|
_text,
|
||||||
fontAStr,
|
fontAStr,
|
||||||
@@ -155,30 +175,6 @@ $effect(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const handleResize = () => {
|
|
||||||
if (container && fontA && fontB) {
|
|
||||||
const width = container.offsetWidth;
|
|
||||||
const padding = isMobile ? 48 : 96;
|
|
||||||
containerWidth = width;
|
|
||||||
layoutResult = comparisonEngine.layout(
|
|
||||||
comparisonStore.text,
|
|
||||||
getPretextFontString(typography.weight, typography.renderedSize, fontA.name),
|
|
||||||
getPretextFontString(typography.weight, typography.renderedSize, fontB.name),
|
|
||||||
width - padding,
|
|
||||||
typography.renderedSize * typography.height,
|
|
||||||
typography.spacing,
|
|
||||||
typography.renderedSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dynamic backgroundSize based on isMobile — can't express this in Tailwind.
|
// Dynamic backgroundSize based on isMobile — can't express this in Tailwind.
|
||||||
// Color is set to currentColor so it respects dark mode via text color.
|
// Color is set to currentColor so it respects dark mode via text color.
|
||||||
const gridStyle = $derived(
|
const gridStyle = $derived(
|
||||||
@@ -198,10 +194,10 @@ const scaleClass = $derived(
|
|||||||
Outer flex container — fills parent.
|
Outer flex container — fills parent.
|
||||||
The paper div inside scales down when the sidebar opens on desktop.
|
The paper div inside scales down when the sidebar opens on desktop.
|
||||||
-->
|
-->
|
||||||
<div class={clsx('flex-1 relative flex items-center justify-center p-0 overflow-hidden bg-surface dark:bg-dark-bg', className)}>
|
<div class={cn('flex-1 relative flex items-center justify-center p-0 overflow-hidden bg-surface dark:bg-dark-bg', className)}>
|
||||||
<!-- Paper surface -->
|
<!-- Paper surface -->
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'w-full h-full flex flex-col items-center justify-center relative',
|
'w-full h-full flex flex-col items-center justify-center relative',
|
||||||
'bg-paper dark:bg-dark-card',
|
'bg-paper dark:bg-dark-card',
|
||||||
'shadow-2xl shadow-black/5 dark:shadow-black/20',
|
'shadow-2xl shadow-black/5 dark:shadow-black/20',
|
||||||
@@ -270,11 +266,11 @@ const scaleClass = $derived(
|
|||||||
|
|
||||||
<TypographyMenu
|
<TypographyMenu
|
||||||
bind:open={isTypographyMenuOpen}
|
bind:open={isTypographyMenuOpen}
|
||||||
class={clsx(
|
class={cn(
|
||||||
'absolute z-50',
|
'absolute z-10',
|
||||||
responsive.isMobileOrTablet
|
responsive.isMobileOrTablet
|
||||||
? 'bottom-4 right-4 -translate-1/2'
|
? 'bottom-0 right-0 -translate-1/2'
|
||||||
: 'bottom-5 left-1/2 right-[unset] -translate-x-1/2',
|
: 'bottom-2.5 left-1/2 -translate-x-1/2',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
1px red vertical rule with square handles at top and bottom.
|
1px red vertical rule with square handles at top and bottom.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import clsx from 'clsx';
|
import { cn } from '$shared/lib';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ let { sliderPos, isDragging }: Props = $props();
|
|||||||
>
|
>
|
||||||
<!-- Top handle -->
|
<!-- Top handle -->
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'w-5 h-5 md:w-6 md:h-6',
|
'w-5 h-5 md:w-6 md:h-6',
|
||||||
'-ml-2.5 md:-ml-3',
|
'-ml-2.5 md:-ml-3',
|
||||||
'mt-2 md:mt-4',
|
'mt-2 md:mt-4',
|
||||||
@@ -47,7 +47,7 @@ let { sliderPos, isDragging }: Props = $props();
|
|||||||
|
|
||||||
<!-- Bottom handle -->
|
<!-- Bottom handle -->
|
||||||
<div
|
<div
|
||||||
class={clsx(
|
class={cn(
|
||||||
'w-5 h-5 md:w-6 md:h-6',
|
'w-5 h-5 md:w-6 md:h-6',
|
||||||
'-ml-2.5 md:-ml-3',
|
'-ml-2.5 md:-ml-3',
|
||||||
'mb-2 md:mb-4',
|
'mb-2 md:mb-4',
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { NavigationWrapper } from '$entities/Breadcrumb';
|
import { NavigationWrapper } from '$entities/Breadcrumb';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import { Section } from '$shared/ui';
|
import { Section } from '$shared/ui';
|
||||||
import clsx from 'clsx';
|
|
||||||
import {
|
import {
|
||||||
getContext,
|
getContext,
|
||||||
untrack,
|
untrack,
|
||||||
@@ -38,7 +38,7 @@ $effect(() => {
|
|||||||
headerAction={registerAction}
|
headerAction={registerAction}
|
||||||
>
|
>
|
||||||
{#snippet content({ className })}
|
{#snippet content({ className })}
|
||||||
<div class={clsx(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
||||||
<FontSearch bind:showFilters={isExpanded} />
|
<FontSearch bind:showFilters={isExpanded} />
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default as Footer } from './ui/Footer.svelte';
|
export { default as Footer } from './ui/Footer/Footer.svelte';
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
<!--
|
|
||||||
Widget: Footer
|
|
||||||
Application footer with project information and portfolio link.
|
|
||||||
Visible only on desktop screens.
|
|
||||||
-->
|
|
||||||
<script lang="ts">
|
|
||||||
import type { ResponsiveManager } from '$shared/lib/helpers';
|
|
||||||
import { FooterLink } from '$shared/ui';
|
|
||||||
import { getContext } from 'svelte';
|
|
||||||
|
|
||||||
const responsive = getContext<ResponsiveManager>('responsive');
|
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if responsive?.isDesktop || responsive?.isDesktopLarge}
|
|
||||||
<footer class="fixed bottom-5 right-5 z-50 flex flex-col items-end gap-1 pointer-events-none">
|
|
||||||
<!-- Portfolio Link (Vertical) -->
|
|
||||||
<div class="pointer-events-auto">
|
|
||||||
<FooterLink
|
|
||||||
text="allmy.work"
|
|
||||||
href="https://allmy.work/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="border border-subtle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Project Name (Horizontal) -->
|
|
||||||
<div class="pointer-events-auto flex items-center gap-2 bg-surface/80 dark:bg-dark-bg/80 backdrop-blur-sm px-3 py-1 border border-subtle">
|
|
||||||
<div class="w-1.5 h-1.5 bg-brand"></div>
|
|
||||||
<span class="text-2xs font-mono uppercase tracking-wider-mono text-neutral-500 dark:text-neutral-400">
|
|
||||||
GlyphDiff © 2025 — {currentYear}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
{/if}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<!--
|
||||||
|
Widget: Footer
|
||||||
|
Application footer with project information and portfolio link.
|
||||||
|
Visible only on desktop screens.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
|
import type { ResponsiveManager } from '$shared/lib/helpers';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import FooterLink from '../FooterLink/FooterLink.svelte';
|
||||||
|
|
||||||
|
const responsive = getContext<ResponsiveManager>('responsive');
|
||||||
|
const isVertical = $derived(responsive?.isDesktop || responsive?.isDesktopLarge);
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer
|
||||||
|
class={cn(
|
||||||
|
'fixed z-10 flex flex-row items-end gap-1 pointer-events-none',
|
||||||
|
isVertical ? 'bottom-2.5 right-2.5 [writing-mode:vertical-rl] rotate-180' : 'bottom-4 left-4',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<!-- Project Name (Horizontal) -->
|
||||||
|
{#if isVertical}
|
||||||
|
<div class="pointer-events-auto items-center gap-2 bg-surface/80 dark:bg-dark-bg/80 backdrop-blur-sm px-3 py-1 border border-subtle">
|
||||||
|
<div class="w-1.5 h-1.5 bg-brand"></div>
|
||||||
|
<span class="text-2xs font-mono uppercase tracking-wider-mono text-neutral-500 dark:text-neutral-400">
|
||||||
|
GlyphDiff © 2025 — {currentYear}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Portfolio Link (Vertical) -->
|
||||||
|
<div class="pointer-events-auto">
|
||||||
|
<FooterLink
|
||||||
|
text="allmy.work"
|
||||||
|
href="https://allmy.work/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class={cn('border border-subtle', isVertical ? 'text-2xs' : 'text-4xs')}
|
||||||
|
iconClass={isVertical ? 'rotate-90' : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<!--
|
||||||
|
Component: FooterLink
|
||||||
|
Specific footer link implementation that uses the generic Link component
|
||||||
|
and adds the default arrow icon.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
|
import { Link } from '$shared/ui';
|
||||||
|
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||||
|
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
interface Props extends HTMLAnchorAttributes {
|
||||||
|
/**
|
||||||
|
* Link text
|
||||||
|
*/
|
||||||
|
text: string;
|
||||||
|
/**
|
||||||
|
* CSS classes for the default icon
|
||||||
|
*/
|
||||||
|
iconClass?: string;
|
||||||
|
/**
|
||||||
|
* Link URL
|
||||||
|
*/
|
||||||
|
href: string;
|
||||||
|
/**
|
||||||
|
* CSS classes
|
||||||
|
*/
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
text,
|
||||||
|
iconClass,
|
||||||
|
href,
|
||||||
|
class: className,
|
||||||
|
...rest
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
{href}
|
||||||
|
class={className}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<span>{text}</span>
|
||||||
|
{#snippet icon()}
|
||||||
|
<ArrowUpRightIcon
|
||||||
|
size={10}
|
||||||
|
class={cn(
|
||||||
|
'fill-body group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200',
|
||||||
|
iconClass,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</Link>
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
import { NavigationWrapper } from '$entities/Breadcrumb';
|
import { NavigationWrapper } from '$entities/Breadcrumb';
|
||||||
import { fontStore } from '$entities/Font';
|
import { fontStore } from '$entities/Font';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
|
import { cn } from '$shared/lib';
|
||||||
import {
|
import {
|
||||||
Label,
|
Label,
|
||||||
Section,
|
Section,
|
||||||
} from '$shared/ui';
|
} from '$shared/ui';
|
||||||
import clsx from 'clsx';
|
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { layoutManager } from '../../model';
|
import { layoutManager } from '../../model';
|
||||||
import LayoutSwitch from '../LayoutSwitch/LayoutSwitch.svelte';
|
import LayoutSwitch from '../LayoutSwitch/LayoutSwitch.svelte';
|
||||||
@@ -50,7 +50,7 @@ const responsive = getContext<ResponsiveManager>('responsive');
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet content({ className })}
|
{#snippet content({ className })}
|
||||||
<div class={clsx(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
||||||
<SampleList />
|
<SampleList />
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
Reference in New Issue
Block a user