Compare commits

..

23 Commits

Author SHA1 Message Date
Ilia Mashkov
aa4796079a feat(Page): add new Section props for sticky titles
All checks were successful
Workflow / build (pull_request) Successful in 3m11s
Workflow / publish (pull_request) Has been skipped
2026-02-18 17:40:20 +03:00
Ilia Mashkov
f18454f9b3 feat(Layout): change fonts link and remove max-width for main 2026-02-18 17:39:24 +03:00
Ilia Mashkov
e3924d43d8 feat(Section): add a styickyTitle feature and change the section layout 2026-02-18 17:36:38 +03:00
Ilia Mashkov
0f6a4d6587 chore: add/delete imports/exports 2026-02-18 17:35:53 +03:00
Ilia Mashkov
8f4faa3328 feat(Input): create index file with type exports 2026-02-18 17:35:26 +03:00
Ilia Mashkov
5867028be6 feat(app): add variable value for mono font 2026-02-18 17:34:47 +03:00
Ilia Mashkov
b8d019b824 feat(ComparisonSlider): add labels 2026-02-18 17:03:44 +03:00
Ilia Mashkov
45ed0d5601 fix(Footnote): use classes every time 2026-02-18 17:03:17 +03:00
Ilia Mashkov
9f91fed692 feat(Input): tweak styles 2026-02-18 17:02:32 +03:00
Ilia Mashkov
201280093f feat(ComparisonSlider): change color for selected font in font list 2026-02-18 17:01:57 +03:00
Ilia Mashkov
55b27973a2 feat(ComparisonSlider): add selected fonts name for mobile controls and labels everywhere 2026-02-18 17:00:25 +03:00
Ilia Mashkov
5fa79e06e9 feat(ComparisonSlider): slightly tweak styles 2026-02-18 16:59:46 +03:00
Ilia Mashkov
ee0749e828 feat(ComparisonSlider): slightly tweak styles 2026-02-18 16:59:31 +03:00
Ilia Mashkov
5dae5fb7ea feat(ComparisonSlider): increase minimal height for large screens 2026-02-18 16:58:31 +03:00
Ilia Mashkov
20f65ee396 feat(FontSampler): slight font style tweaks for font name 2026-02-18 16:57:52 +03:00
Ilia Mashkov
010b8ad04b feat(FontSearch): make filters open by default 2026-02-18 16:57:03 +03:00
Ilia Mashkov
ce1dcd92ab feat(Label): create shared Label component 2026-02-18 16:56:26 +03:00
Ilia Mashkov
ce609728c3 feat(SidebarMenu): tweak styles 2026-02-18 16:55:57 +03:00
Ilia Mashkov
147df04c22 feat(Slider): tweak styles for a knob and add slider label 2026-02-18 16:55:11 +03:00
Ilia Mashkov
f356851d97 chore: remove lenis package 2026-02-18 16:53:40 +03:00
Ilia Mashkov
411dbfefcb feat(ComparisonSlider): rotate icon for the mobile and slightly tweak styles 2026-02-18 16:52:50 +03:00
Ilia Mashkov
a65d692139 feat(app): style default scrollbar 2026-02-18 11:18:54 +03:00
Ilia Mashkov
3330f13228 fix(SearchBar): restore proper padding 2026-02-18 11:18:17 +03:00
28 changed files with 473 additions and 317 deletions

View File

@@ -67,7 +67,6 @@
"vitest-browser-svelte": "^2.0.1" "vitest-browser-svelte": "^2.0.1"
}, },
"dependencies": { "dependencies": {
"@tanstack/svelte-query": "^6.0.14", "@tanstack/svelte-query": "^6.0.14"
"lenis": "^1.3.17"
} }
} }

View File

@@ -57,6 +57,8 @@
--gradient-from: oklch(0.98 0.002 286.32); --gradient-from: oklch(0.98 0.002 286.32);
--gradient-via: oklch(1 0 0); --gradient-via: oklch(1 0 0);
--gradient-to: oklch(0.98 0.002 286.32); --gradient-to: oklch(0.98 0.002 286.32);
--font-mono: 'Major Mono Display';
} }
.dark { .dark {
@@ -165,6 +167,7 @@
--color-gradient-from: var(--gradient-from); --color-gradient-from: var(--gradient-from);
--color-gradient-via: var(--gradient-via); --color-gradient-via: var(--gradient-via);
--color-gradient-to: var(--gradient-to); --color-gradient-to: var(--gradient-to);
--font-mono: var(--font-mono);
} }
@layer base { @layer base {
@@ -222,3 +225,82 @@
.barlow { .barlow {
font-family: "Barlow", system-ui, Inter, Roboto, "Segoe UI", Arial, sans-serif; font-family: "Barlow", system-ui, Inter, Roboto, "Segoe UI", Arial, sans-serif;
} }
* {
scrollbar-width: thin;
scrollbar-color: hsl(0 0% 70% / 0.4) transparent;
}
.dark * {
scrollbar-color: hsl(0 0% 40% / 0.5) transparent;
}
/* ---- Webkit / Blink ---- */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(0 0% 70% / 0);
border-radius: 3px;
transition: background 0.2s ease;
}
/* Show thumb when container is hovered or actively scrolling */
:hover > ::-webkit-scrollbar-thumb,
::-webkit-scrollbar-thumb:hover,
*:hover::-webkit-scrollbar-thumb {
background: hsl(0 0% 70% / 0.4);
}
::-webkit-scrollbar-thumb:hover {
background: hsl(0 0% 50% / 0.6);
}
::-webkit-scrollbar-thumb:active {
background: hsl(0 0% 40% / 0.8);
}
::-webkit-scrollbar-corner {
background: transparent;
}
/* Dark mode */
.dark ::-webkit-scrollbar-thumb {
background: hsl(0 0% 40% / 0);
}
.dark :hover > ::-webkit-scrollbar-thumb,
.dark ::-webkit-scrollbar-thumb:hover,
.dark *:hover::-webkit-scrollbar-thumb {
background: hsl(0 0% 40% / 0.5);
}
.dark ::-webkit-scrollbar-thumb:hover {
background: hsl(0 0% 55% / 0.6);
}
.dark ::-webkit-scrollbar-thumb:active {
background: hsl(0 0% 65% / 0.7);
}
/* ---- Behavior ---- */
* {
scroll-behavior: smooth;
scrollbar-gutter: stable;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
body {
overscroll-behavior-y: none;
}

View File

@@ -55,30 +55,36 @@ onMount(async () => {
<link rel="icon" href={GD} /> <link rel="icon" href={GD} />
<link rel="preconnect" href="https://api.fontshare.com" /> <link rel="preconnect" href="https://api.fontshare.com" />
<link rel="preconnect" href="https://cdn.fontshare.com" crossorigin="anonymous" /> <link
rel="preconnect"
href="https://cdn.fontshare.com"
crossorigin="anonymous"
/>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous"> <link
rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin="anonymous"
/>
<link <link
rel="preload" rel="preload"
as="style" as="style"
href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;1,100;1,200&family=Karla:wght@200..800&display=swap" href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;1,100;1,200&family=Karla:wght@200..800&family=Major+Mono+Display&display=swap"
> />
<link <link
rel="stylesheet" rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;1,100;1,200&family=Karla:wght@200..800&display=swap" href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;1,100;1,200&family=Karla:wght@200..800&family=Major+Mono+Display&display=swap"
media="print" media="print"
onload={(e => ((e.currentTarget as HTMLLinkElement).media = 'all'))} onload={(e => ((e.currentTarget as HTMLLinkElement).media = 'all'))}
> />
<noscript> <noscript>
<link <link
rel="stylesheet" rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;1,100;1,200&family=Karla:wght@200..800&display=swap" href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;1,100;1,200&family=Karla:wght@200..800&family=Major+Mono+Display&display=swap"
> />
</noscript> </noscript>
<title> <title>Compare Typography & Typefaces | GlyphDiff</title>
Compare Typography & Typefaces | GlyphDiff
</title>
</svelte:head> </svelte:head>
<ResponsiveProvider> <ResponsiveProvider>
@@ -88,7 +94,7 @@ onMount(async () => {
</header> </header>
<!-- <ScrollArea class="h-screen w-screen"> --> <!-- <ScrollArea class="h-screen w-screen"> -->
<main class="flex-1 h-full w-full max-w-6xl mx-auto px-0 pt-0 pb-10 sm:px-6 sm:pt-8 sm:pb-12 md:px-8 md:pt-10 md:pb-16 lg:px-10 lg:pt-12 lg:pb-20 xl:px-16 relative overflow-x-hidden"> <main class="flex-1 w-full mx-auto px-4 pt-0 pb-10 sm:px-6 sm:pt-8 sm:pb-12 md:px-8 md:pt-10 md:pb-16 lg:px-10 lg:pt-12 lg:pb-20 xl:px-16 relative">
<TooltipProvider> <TooltipProvider>
{#if fontsReady} {#if fontsReady}
{@render children?.()} {@render children?.()}

View File

@@ -36,12 +36,7 @@ interface Props {
letterSpacing?: number; letterSpacing?: number;
} }
let { let { font, text = $bindable(), index = 0, ...restProps }: Props = $props();
font,
text = $bindable(),
index = 0,
...restProps
}: Props = $props();
const fontWeight = $derived(controlManager.weight); const fontWeight = $derived(controlManager.weight);
const fontSize = $derived(controlManager.renderedSize); const fontSize = $derived(controlManager.renderedSize);
@@ -66,9 +61,9 @@ const letterSpacing = $derived(controlManager.spacing);
typeface_{String(index).padStart(3, '0')} typeface_{String(index).padStart(3, '0')}
</Footnote> </Footnote>
<div class="w-px h-2 sm:h-2.5 bg-border-subtle"></div> <div class="w-px h-2 sm:h-2.5 bg-border-subtle"></div>
<Footnote class="tracking-[0.15em] font-bold text-foreground"> <div class="font-bold text-foreground">
{font.name} {font.name}
</Footnote> </div>
</div> </div>
<!-- <!--
@@ -86,11 +81,11 @@ const letterSpacing = $derived(controlManager.spacing);
<div class="p-4 sm:p-5 md:p-8 relative z-10"> <div class="p-4 sm:p-5 md:p-8 relative z-10">
<FontApplicator {font} weight={fontWeight}> <FontApplicator {font} weight={fontWeight}>
<ContentEditable <ContentEditable
bind:text={text} bind:text
{...restProps} {...restProps}
fontSize={fontSize} {fontSize}
lineHeight={lineHeight} {lineHeight}
letterSpacing={letterSpacing} {letterSpacing}
/> />
</FontApplicator> </FontApplicator>
</div> </div>

View File

@@ -15,6 +15,7 @@ import {
Drawer, Drawer,
IconButton, IconButton,
} from '$shared/ui'; } from '$shared/ui';
import { Label } from '$shared/ui';
import SlidersIcon from '@lucide/svelte/icons/sliders-vertical'; import SlidersIcon from '@lucide/svelte/icons/sliders-vertical';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
@@ -72,7 +73,11 @@ $effect(() => {
</script> </script>
<div <div
class={cn('w-auto max-screen z-10 flex justify-center', hidden && 'hidden', className)} class={cn(
'w-auto max-screen z-10 flex justify-center',
hidden && 'hidden',
className,
)}
in:receive={{ key: 'panel' }} in:receive={{ key: 'panel' }}
out:send={{ key: 'panel' }} out:send={{ key: 'panel' }}
> >
@@ -86,11 +91,17 @@ $effect(() => {
</IconButton> </IconButton>
{/snippet} {/snippet}
{#snippet content({ className })} {#snippet content({ className })}
<div class={cn(className, 'flex flex-col gap-6')}> <Label
class="mt-6 mb-12 px-2"
text="Typography Controls"
align="center"
/>
<div class={cn(className, 'flex flex-col gap-8')}>
{#each controlManager.controls as control (control.id)} {#each controlManager.controls as control (control.id)}
<ComboControlV2 <ComboControlV2
control={control.instance} control={control.instance}
orientation="horizontal" orientation="horizontal"
label={control.controlLabel}
reduced reduced
/> />
{/each} {/each}
@@ -112,6 +123,7 @@ $effect(() => {
decreaseLabel={control.decreaseLabel} decreaseLabel={control.decreaseLabel}
controlLabel={control.controlLabel} controlLabel={control.controlLabel}
orientation="vertical" orientation="vertical"
showScale={false}
/> />
{/each} {/each}
</div> </div>

View File

@@ -4,6 +4,8 @@
--> -->
<script lang="ts"> <script lang="ts">
import { scrollBreadcrumbsStore } from '$entities/Breadcrumb'; import { scrollBreadcrumbsStore } from '$entities/Breadcrumb';
import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
Logo, Logo,
Section, Section,
@@ -15,13 +17,17 @@ import CodeIcon from '@lucide/svelte/icons/code';
import EyeIcon from '@lucide/svelte/icons/eye'; import EyeIcon from '@lucide/svelte/icons/eye';
import LineSquiggleIcon from '@lucide/svelte/icons/line-squiggle'; import LineSquiggleIcon from '@lucide/svelte/icons/line-squiggle';
import ScanSearchIcon from '@lucide/svelte/icons/search'; import ScanSearchIcon from '@lucide/svelte/icons/search';
import type { Snippet } from 'svelte'; import {
type Snippet,
getContext,
} from 'svelte';
import { cubicIn } from 'svelte/easing'; import { cubicIn } from 'svelte/easing';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
let searchContainer: HTMLElement; let searchContainer: HTMLElement;
let isExpanded = $state(false); let isExpanded = $state(true);
const responsive = getContext<ResponsiveManager>('responsive');
function handleTitleStatusChanged( function handleTitleStatusChanged(
index: number, index: number,
@@ -39,39 +45,37 @@ function handleTitleStatusChanged(
scrollBreadcrumbsStore.remove(index); scrollBreadcrumbsStore.remove(index);
}; };
} }
// $effect(() => {
// appliedFontsManager.touch(
// selectedFontsStore.all.map(font => ({
// slug: font.id,
// weight: controlManager.weight,
// })),
// );
// });
</script> </script>
<!-- Font List --> <!-- Font List -->
<div <div
class="p-2 sm:p-3 md:p-4 h-full flex flex-col gap-3 sm:gap-4" class="p-2 sm:p-3 md:p-4 h-full grid gap-3 sm:gap-4 grid-cols-[max-content_1fr]"
in:fade={{ duration: 500, delay: 150, easing: cubicIn }} in:fade={{ duration: 500, delay: 150, easing: cubicIn }}
> >
<Section class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-8" onTitleStatusChange={handleTitleStatusChanged}> <Section
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-8"
onTitleStatusChange={handleTitleStatusChanged}
>
{#snippet icon({ className })} {#snippet icon({ className })}
<CodeIcon class={className} /> <CodeIcon class={className} />
{/snippet} {/snippet}
{#snippet description({ className })} {#snippet description({ className })}
<span class={className}> <span class={className}> Project_Codename </span>
Project_Codename {/snippet}
</span> {#snippet content({ className })}
<div class={cn(className, 'col-start-0 col-span-2')}>
<Logo />
</div>
{/snippet} {/snippet}
<Logo />
</Section> </Section>
<Section <Section
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-8" class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-x-12 sm:gap-y-8"
index={1} index={1}
id="optical_comparator" id="optical_comparator"
onTitleStatusChange={handleTitleStatusChanged} onTitleStatusChange={handleTitleStatusChanged}
stickyTitle={responsive.isDesktopLarge}
stickyOffset="4rem"
> >
{#snippet icon({ className })} {#snippet icon({ className })}
<EyeIcon class={className} /> <EyeIcon class={className} />
@@ -81,14 +85,20 @@ function handleTitleStatusChanged(
Optical<br />Comparator Optical<br />Comparator
</h1> </h1>
{/snippet} {/snippet}
<ComparisonSlider /> {#snippet content({ className })}
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
<ComparisonSlider />
</div>
{/snippet}
</Section> </Section>
<Section <Section
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-8" class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-x-12 sm:gap-y-8"
index={2} index={2}
id="query_module" id="query_module"
onTitleStatusChange={handleTitleStatusChanged} onTitleStatusChange={handleTitleStatusChanged}
stickyTitle={responsive.isDesktopLarge}
stickyOffset="4rem"
> >
{#snippet icon({ className })} {#snippet icon({ className })}
<ScanSearchIcon class={className} /> <ScanSearchIcon class={className} />
@@ -98,14 +108,20 @@ function handleTitleStatusChanged(
Query<br />Module Query<br />Module
</h2> </h2>
{/snippet} {/snippet}
<FontSearch bind:showFilters={isExpanded} /> {#snippet content({ className })}
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
<FontSearch bind:showFilters={isExpanded} />
</div>
{/snippet}
</Section> </Section>
<Section <Section
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-8" class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-x-12 sm:gap-y-8"
index={3} index={3}
id="sample_set" id="sample_set"
onTitleStatusChange={handleTitleStatusChanged} onTitleStatusChange={handleTitleStatusChanged}
stickyTitle={responsive.isDesktopLarge}
stickyOffset="4rem"
> >
{#snippet icon({ className })} {#snippet icon({ className })}
<LineSquiggleIcon class={className} /> <LineSquiggleIcon class={className} />
@@ -115,18 +131,22 @@ function handleTitleStatusChanged(
Sample<br />Set Sample<br />Set
</h2> </h2>
{/snippet} {/snippet}
<SampleList /> {#snippet content({ className })}
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
<SampleList />
</div>
{/snippet}
</Section> </Section>
</div> </div>
<style> <style>
.content { .content {
/* Tells the browser to skip rendering off-screen content */ /* Tells the browser to skip rendering off-screen content */
content-visibility: auto; content-visibility: auto;
/* Helps the browser reserve space without calculating everything */ /* Helps the browser reserve space without calculating everything */
contain-intrinsic-size: 1px 1000px; contain-intrinsic-size: 1px 1000px;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
</style> </style>

View File

@@ -1,32 +0,0 @@
import Lenis from 'lenis';
import {
getContext,
setContext,
} from 'svelte';
const LENIS_KEY = Symbol('lenis');
export function createLenisContext() {
let lenis = $state<Lenis | null>(null);
return {
get lenis() {
return lenis;
},
setLenis(instance: Lenis) {
lenis = instance;
},
destroyLenis() {
lenis?.destroy();
lenis = null;
},
};
}
export function setLenisContext(context: ReturnType<typeof createLenisContext>) {
setContext(LENIS_KEY, context);
}
export function getLenisContext() {
return getContext<ReturnType<typeof createLenisContext>>(LENIS_KEY);
}

View File

@@ -44,12 +44,6 @@ export {
responsiveManager, responsiveManager,
} from './createResponsiveManager/createResponsiveManager.svelte'; } from './createResponsiveManager/createResponsiveManager.svelte';
export {
createLenisContext,
getLenisContext,
setLenisContext,
} from './createScrollContext/createScrollContext.svelte';
export { export {
createPerspectiveManager, createPerspectiveManager,
type PerspectiveManager, type PerspectiveManager,

View File

@@ -6,7 +6,6 @@ export {
createDebouncedState, createDebouncedState,
createEntityStore, createEntityStore,
createFilter, createFilter,
createLenisContext,
createPersistentStore, createPersistentStore,
createPerspectiveManager, createPerspectiveManager,
createResponsiveManager, createResponsiveManager,
@@ -16,14 +15,12 @@ export {
type EntityStore, type EntityStore,
type Filter, type Filter,
type FilterModel, type FilterModel,
getLenisContext,
type LineData, type LineData,
type PersistentStore, type PersistentStore,
type PerspectiveManager, type PerspectiveManager,
type Property, type Property,
type ResponsiveManager, type ResponsiveManager,
responsiveManager, responsiveManager,
setLenisContext,
type TypographyControl, type TypographyControl,
type VirtualItem, type VirtualItem,
type Virtualizer, type Virtualizer,

View File

@@ -98,11 +98,11 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
function calculateScale(index: number): number | string { function calculateScale(index: number): number | string {
const calculate = () => const calculate = () =>
orientation === 'horizontal' orientation === 'horizontal'
? (control.min + (index * (control.max - control.min) / 4)) ? control.min + (index * (control.max - control.min)) / 4
: (control.max - (index * (control.max - control.min) / 4)); : control.max - (index * (control.max - control.min)) / 4;
return Number.isInteger(control.step) return Number.isInteger(control.step)
? Math.round(calculate()) ? Math.round(calculate())
: (calculate()).toFixed(2); : calculate().toFixed(2);
} }
</script> </script>
@@ -111,7 +111,9 @@ function calculateScale(index: number): number | string {
class={cn( class={cn(
'flex gap-4 sm:py-4 sm:px-1 rounded-xl transition-all duration-300', 'flex gap-4 sm:py-4 sm:px-1 rounded-xl transition-all duration-300',
'', '',
orientation === 'horizontal' ? 'flex-row items-end w-full' : 'flex-col items-center h-full', orientation === 'horizontal'
? 'flex-row items-end w-full'
: 'flex-col items-center h-full',
className, className,
)} )}
> >
@@ -120,7 +122,9 @@ function calculateScale(index: number): number | string {
<div <div
class={cn( class={cn(
'absolute flex justify-between', 'absolute flex justify-between',
orientation === 'horizontal' ? 'flex-row w-full -top-5 px-0.5' : 'flex-col h-full -left-5 py-0.5', orientation === 'horizontal'
? 'flex-row w-full -top-8 px-0.5'
: 'flex-col h-full -left-5 py-0.5',
)} )}
> >
{#each Array(5) as _, i} {#each Array(5) as _, i}
@@ -133,7 +137,12 @@ function calculateScale(index: number): number | string {
<span class="font-mono text-[0.375rem] text-text-muted tabular-nums"> <span class="font-mono text-[0.375rem] text-text-muted tabular-nums">
{calculateScale(i)} {calculateScale(i)}
</span> </span>
<div class={cn('bg-border-muted', orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1')}> <div
class={cn(
'bg-border-muted',
orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1',
)}
>
</div> </div>
</div> </div>
{/each} {/each}
@@ -146,6 +155,7 @@ function calculateScale(index: number): number | string {
min={control.min} min={control.min}
max={control.max} max={control.max}
step={control.step} step={control.step}
{label}
{orientation} {orientation}
/> />
</div> </div>
@@ -162,16 +172,6 @@ function calculateScale(index: number): number | string {
variant="ghost" variant="ghost"
/> />
{/if} {/if}
{#if label}
<div class="flex items-center gap-2 opacity-70">
<div class="w-1 h-1 rounded-full bg-foreground"></div>
<div class="w-px h-2 bg-text-muted/50"></div>
<span class="font-mono text-[8px] uppercase tracking-[0.2em] text-text-subtle font-medium">
{label}
</span>
</div>
{/if}
</div> </div>
{/snippet} {/snippet}

View File

@@ -16,16 +16,22 @@ interface Props {
} }
const { children, class: className, render }: Props = $props(); const { children, class: className, render }: Props = $props();
const baseClasses =
'font-mono text-[0.5625rem] sm:text-[0.625rem] uppercase tracking-[0.2em] text-text-soft opacity-60';
const combinedClasses = cn(baseClasses, className);
</script> </script>
{#if render} {#if render}
{@render render({ class: combinedClasses })} {@render render({
class: cn(
'font-mono text-[0.5625rem] sm:text-[0.625rem] lowercase tracking-[0.2em] text-text-soft',
className,
),
})}
{:else if children} {:else if children}
<span class={combinedClasses}> <span
class={cn(
'font-mono text-[0.5625rem] sm:text-[0.625rem] lowercase tracking-[0.2em] text-text-soft',
className,
)}
>
{@render children()} {@render children()}
</span> </span>
{/if} {/if}

View File

@@ -27,19 +27,19 @@ export const inputVariants = tv({
'h-9 sm:h-10 md:h-11 rounded-lg', 'h-9 sm:h-10 md:h-11 rounded-lg',
'px-3 sm:px-3.5 md:px-4', 'px-3 sm:px-3.5 md:px-4',
'text-xs sm:text-sm md:text-base', 'text-xs sm:text-sm md:text-base',
'placeholder:text-xs sm:placeholder:text-sm md:placeholder:text-base', 'placeholder:text-[0.75rem] sm:placeholder:text-sm md:placeholder:text-base',
], ],
md: [ md: [
'h-10 sm:h-12 md:h-14 rounded-xl', 'h-10 sm:h-12 md:h-14 rounded-xl',
'px-3.5 sm:px-4 md:px-5', 'px-3.5 sm:px-4 md:px-5',
'text-sm sm:text-base md:text-lg', 'text-sm sm:text-base md:text-lg',
'placeholder:text-xs sm:placeholder:text-sm md:placeholder:text-base', 'placeholder:text-[0.75rem] sm:placeholder:text-sm md:placeholder:text-base',
], ],
lg: [ lg: [
'h-12 sm:h-14 md:h-16 rounded-2xl', 'h-12 sm:h-14 md:h-16 rounded-2xl',
'px-4 sm:px-5 md:px-6', 'px-4 sm:px-5 md:px-6',
'text-sm sm:text-base md:text-lg', 'text-sm sm:text-base md:text-lg',
'placeholder:text-xs sm:placeholder:text-sm md:placeholder:text-base', 'placeholder:text-[0.75rem] sm:placeholder:text-sm md:placeholder:text-base',
], ],
}, },
}, },
@@ -84,7 +84,7 @@ let {
</script> </script>
<BaseInput <BaseInput
bind:value={value} bind:value
class={cn(inputVariants({ variant, size }), className)} class={cn(inputVariants({ variant, size }), className)}
{...rest} {...rest}
/> />

View File

@@ -0,0 +1,13 @@
import type { ComponentProps } from 'svelte';
import Input from './Input.svelte';
type InputProps = ComponentProps<typeof Input>;
type InputSize = InputProps['size'];
type InputVariant = InputProps['variant'];
export {
Input,
type InputProps,
type InputSize,
type InputVariant,
};

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
interface Props {
text?: string;
align?: 'left' | 'right' | 'center';
size?: 'sm' | 'md' | 'lg';
onlyText?: boolean;
class?: string;
}
const {
text,
align = 'left',
size = 'md',
onlyText = false,
class: className,
}: Props = $props();
</script>
<div
class={cn(
'grid grid-rows-1 gap-2 items-center w-auto',
align === 'left' && 'grid-cols-[max-content_1fr]',
align === 'center' && 'grid-cols-[1fr_max-content_1fr]',
align === 'right' && 'grig-cols-[1fr_max-content]',
className,
)}
>
{#if align !== 'left'}
<div class={cn('h-px w-full bg-gray-400/50', onlyText && 'bg-transparent')}></div>
{/if}
<div
class={cn(
'text-gray-400 uppercase',
size === 'sm' && 'text-[0.5rem]',
size === 'md' && 'text-[0.625rem]',
size === 'lg' && 'text-[0.75rem]',
)}
>
{text}
</div>
{#if align !== 'right'}
<div class={cn('h-px w-full bg-gray-400/50', onlyText && 'bg-transparent')}></div>
{/if}
</div>

View File

@@ -35,5 +35,10 @@ let {
<div class="absolute left-4 sm:left-5 top-1/2 -translate-y-1/2 pointer-events-none z-10"> <div class="absolute left-4 sm:left-5 top-1/2 -translate-y-1/2 pointer-events-none z-10">
<AsteriskIcon class="size-3 sm:size-4 stroke-gray-400 stroke-[1.5]" /> <AsteriskIcon class="size-3 sm:size-4 stroke-gray-400 stroke-[1.5]" />
</div> </div>
<Input {id} class={cn('pl-11 sm:pl-14', className)} bind:value={value} placeholder={placeholder} /> <Input
{id}
class={cn('pl-11 sm:pl-14 md:pl-14 lg:pl-14', className)}
bind:value
{placeholder}
/>
</div> </div>

View File

@@ -56,13 +56,40 @@ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
/** /**
* Snippet for the section content * Snippet for the section content
*/ */
children?: Snippet; content?: Snippet<[{ className?: string }]>;
/**
* When true, the title stays fixed in view while
* scrolling through the section content.
*/
stickyTitle?: boolean;
/**
* Top offset for sticky title (e.g. header height).
* @default '0px'
*/
stickyOffset?: string;
} }
const { class: className, title, icon, description, index = 0, onTitleStatusChange, id, children }: Props = $props(); const {
class: className,
title,
icon,
description,
index = 0,
onTitleStatusChange,
id,
content,
stickyTitle = false,
stickyOffset = '0px',
}: Props = $props();
let titleContainer = $state<HTMLElement>(); let titleContainer = $state<HTMLElement>();
const flyParams: FlyParams = { y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 }; const flyParams: FlyParams = {
y: 0,
x: -50,
duration: 300,
easing: cubicOut,
opacity: 0.2,
};
// Track if the user has actually scrolled away from view // Track if the user has actually scrolled away from view
let isScrolledPast = $state(false); let isScrolledPast = $state(false);
@@ -72,18 +99,21 @@ $effect(() => {
return; return;
} }
let cleanup: ((index: number) => void) | undefined; let cleanup: ((index: number) => void) | undefined;
const observer = new IntersectionObserver(entries => { const observer = new IntersectionObserver(
const entry = entries[0]; entries => {
const isPast = !entry.isIntersecting && entry.boundingClientRect.top < 0; const entry = entries[0];
const isPast = !entry.isIntersecting && entry.boundingClientRect.top < 0;
if (isPast !== isScrolledPast) { if (isPast !== isScrolledPast) {
isScrolledPast = isPast; isScrolledPast = isPast;
cleanup = onTitleStatusChange?.(index, isPast, title, id); cleanup = onTitleStatusChange?.(index, isPast, title, id);
} }
}, { },
// Set threshold to 0 to trigger exactly when the last pixel leaves {
threshold: 0, // Set threshold to 0 to trigger exactly when the last pixel leaves
}); threshold: 0,
},
);
observer.observe(titleContainer); observer.observe(titleContainer);
return () => { return () => {
@@ -94,20 +124,32 @@ $effect(() => {
</script> </script>
<section <section
id={id} {id}
class={cn( class={cn(
'flex flex-col', 'col-span-2 grid grid-cols-subgrid',
stickyTitle ? 'gap-x-6 sm:gap-x-8 md:gap-x-10 lg:gap-x-12' : 'grid-rows-[max-content_1fr]',
className, className,
)} )}
in:fly={flyParams} in:fly={flyParams}
out:fly={flyParams} out:fly={flyParams}
> >
<div class="flex flex-col gap-2 sm:gap-3" bind:this={titleContainer}> <div
bind:this={titleContainer}
class={cn(
'flex flex-col gap-2 sm:gap-3',
stickyTitle && 'self-start',
)}
style:position={stickyTitle ? 'sticky' : undefined}
style:top={stickyTitle ? stickyOffset : undefined}
>
<div class="flex items-center gap-2 sm:gap-3"> <div class="flex items-center gap-2 sm:gap-3">
{#if icon} {#if icon}
{@render icon({ className: 'size-3 sm:size-4 stroke-foreground stroke-1 opacity-60' })} {@render icon({
className: 'size-3 sm:size-4 stroke-foreground stroke-1 opacity-60',
})}
<div class="w-px h-2.5 sm:h-3 bg-border-subtle"></div> <div class="w-px h-2.5 sm:h-3 bg-border-subtle"></div>
{/if} {/if}
{#if description} {#if description}
<Footnote> <Footnote>
{#snippet render({ class: className })} {#snippet render({ class: className })}
@@ -129,5 +171,9 @@ $effect(() => {
{/if} {/if}
</div> </div>
{@render children?.()} {@render content?.({
className: stickyTitle
? 'row-start-2 col-start-2'
: 'row-start-2 col-start-2',
})}
</section> </section>

View File

@@ -73,7 +73,7 @@ function handleClick(event: MouseEvent) {
{@render action?.()} {@render action?.()}
{#if visible} {#if visible}
<div <div
class="relative z-20 h-full w-auto flex flex-col gap-4" class="relative z-20 h-full w-auto flex flex-col"
in:fade={{ duration: 300, delay: 400, easing: cubicOut }} in:fade={{ duration: 300, delay: 400, easing: cubicOut }}
out:fade={{ duration: 150, easing: cubicOut }} out:fade={{ duration: 150, easing: cubicOut }}
> >

View File

@@ -9,28 +9,43 @@ import {
type SliderRootProps, type SliderRootProps,
} from 'bits-ui'; } from 'bits-ui';
type Props = Omit<SliderRootProps, 'type' | 'onValueChange' | 'onValueCommit'> & { type Props =
/** & Omit<
* Slider value, numeric. SliderRootProps,
*/ 'type' | 'onValueChange' | 'onValueCommit'
value: number; >
/** & {
* A callback function called when the value changes. /**
* @param newValue - number * Slider value, numeric.
*/ */
onValueChange?: (newValue: number) => void; value: number;
/** /**
* A callback function called when the user stops dragging the thumb and the value is committed. * Optional label displayed inline on the track before the filled range.
* @param newValue - number */
*/ label?: string;
onValueCommit?: (newValue: number) => void; /**
}; * A callback function called when the value changes.
* @param newValue - number
*/
onValueChange?: (newValue: number) => void;
/**
* A callback function called when the user stops dragging the thumb and the value is committed.
* @param newValue - number
*/
onValueCommit?: (newValue: number) => void;
};
let { value = $bindable(), orientation = 'horizontal', class: className, ...rest }: Props = $props(); let {
value = $bindable(),
orientation = 'horizontal',
class: className,
label,
...rest
}: Props = $props();
</script> </script>
<Slider.Root <Slider.Root
bind:value={value} bind:value
class={cn( class={cn(
'relative flex h-full w-6 touch-none select-none items-center justify-center', 'relative flex h-full w-6 touch-none select-none items-center justify-center',
orientation === 'horizontal' ? 'w-48 h-6' : 'w-6 h-48', orientation === 'horizontal' ? 'w-48 h-6' : 'w-6 h-48',
@@ -41,13 +56,23 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
{...rest} {...rest}
> >
{#snippet children(props)} {#snippet children(props)}
{#if label && orientation === 'horizontal'}
<span class="absolute top-0 left-0 -translate-y-1/2 text-[0.5rem] uppercase text-gray-400">
{label}
</span>
{/if}
<span <span
{...props} {...props}
class={cn('relative bg-background-muted rounded-full', orientation === 'horizontal' ? 'w-full h-px' : 'h-full w-px')} class={cn(
'relative bg-background-muted rounded-full',
orientation === 'horizontal' ? 'w-full h-px' : 'h-full w-px',
)}
> >
<!-- Filled range with NO transition -->
<Slider.Range <Slider.Range
class={cn('absolute bg-foreground rounded-full', orientation === 'horizontal' ? 'h-full' : 'w-full')} class={cn(
'absolute bg-foreground rounded-full',
orientation === 'horizontal' ? 'h-full' : 'w-full',
)}
/> />
<Slider.Thumb <Slider.Thumb
@@ -56,27 +81,32 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
'group/thumb relative block', 'group/thumb relative block',
'size-2', 'size-2',
orientation === 'horizontal' ? '-top-1' : '-left-1', orientation === 'horizontal' ? '-top-1' : '-left-1',
'rounded-sm', 'rounded-full',
'bg-foreground', 'bg-foreground',
// Glow shadow // Glow shadow
'shadow-[0_0_6px_rgba(0,0,0,0.4)]', 'shadow-[0_0_6px_rgba(0,0,0,0.4)]',
// Smooth transitions only for size/position // Smooth transitions only for size/position
'duration-200 ease-out', 'duration-200 ease-out',
orientation === 'horizontal' ? 'transition-[height,top,left,box-shadow]' : 'transition-[width,top,left,box-shadow]', orientation === 'horizontal'
? 'transition-[height,top,left,box-shadow]'
: 'transition-[width,top,left,box-shadow]',
// Hover: bigger glow // Hover: bigger glow
'hover:shadow-[0_0_10px_rgba(0,0,0,0.5)]', 'hover:shadow-[0_0_10px_rgba(0,0,0,0.5)]',
orientation === 'horizontal' ? 'hover:size-3 hover:-top-[5.5px]' : 'hover:size-3 hover:-left-[5.5px]', orientation === 'horizontal'
? 'hover:size-3 hover:-top-[5.5px]'
: 'hover:size-3 hover:-left-[5.5px]',
// Active: smaller glow // Active: smaller glow
'active:shadow-[0_0_4px_rgba(0,0,0,0.3)]', 'active:shadow-[0_0_4px_rgba(0,0,0,0.3)]',
orientation === 'horizontal' ? 'active:h-2.5 active:-top-[4.5px]' : 'active:w-2.5 active:-left-[4.5px]', orientation === 'horizontal'
? 'active:h-2.5 active:-top-[4.5px]'
: 'active:w-2.5 active:-left-[4.5px]',
'focus:outline-none', 'focus:outline-none',
'cursor-grab active:cursor-grabbing', 'cursor-grab active:cursor-grabbing',
)} )}
> >
<!-- Soft glow on hover -->
<div <div
class=" class="
absolute inset-0 rounded-sm absolute inset-0 rounded-full
bg-background-20 bg-background-20
opacity-0 group-hover/thumb:opacity-100 opacity-0 group-hover/thumb:opacity-100
transition-opacity duration-200 transition-opacity duration-200
@@ -84,11 +114,12 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
> >
</div> </div>
<!-- Value label -->
<span <span
class={cn( class={cn(
'absolute', 'absolute',
orientation === 'horizontal' ? '-top-8 left-1/2 -translate-x-1/2' : 'left-5 top-1/2 -translate-y-1/2', orientation === 'horizontal'
? '-top-8 left-1/2 -translate-x-1/2'
: 'left-5 top-1/2 -translate-y-1/2',
'px-1.5 py-0.5 rounded-md', 'px-1.5 py-0.5 rounded-md',
'bg-foreground/90 backdrop-blur-sm', 'bg-foreground/90 backdrop-blur-sm',
'font-mono text-[0.625rem] font-medium text-background', 'font-mono text-[0.625rem] font-medium text-background',

View File

@@ -1,78 +0,0 @@
<script lang="ts">
import {
createLenisContext,
setLenisContext,
} from '$shared/lib';
import Lenis from 'lenis';
import type { LenisOptions } from 'lenis';
import { onMount } from 'svelte';
interface Props {
children?: import('svelte').Snippet;
// Lenis options - all optional with sensible defaults
duration?: number;
easing?: (t: number) => number;
smoothWheel?: boolean;
wheelMultiplier?: number;
touchMultiplier?: number;
infinite?: boolean;
orientation?: 'vertical' | 'horizontal';
gestureOrientation?: 'vertical' | 'horizontal' | 'both';
}
let {
children,
duration = 1.2,
easing = t => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
smoothWheel = true,
wheelMultiplier = 1,
touchMultiplier = 2,
infinite = false,
orientation = 'vertical',
gestureOrientation = 'vertical',
}: Props = $props();
const lenisContext = createLenisContext();
setLenisContext(lenisContext);
onMount(() => {
const lenisOptions: LenisOptions = {
duration,
easing,
smoothWheel,
wheelMultiplier,
touchMultiplier,
infinite,
orientation,
gestureOrientation,
// Prevent jitter with virtual scroll
prevent: (node: HTMLElement) => {
// Don't smooth scroll inside elements with data-lenis-prevent
return node.hasAttribute('data-lenis-prevent');
},
};
const lenis = new Lenis(lenisOptions);
lenisContext.setLenis(lenis);
// RAF loop
function raf(time: number) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
// Expose to window for debugging (only in dev)
if (import.meta.env?.DEV) {
(window as any).lenis = lenis;
}
return () => {
lenisContext.destroyLenis();
};
});
</script>
{@render children?.()}

View File

@@ -1,6 +1,3 @@
import type { ComponentProps } from 'svelte';
import Input from './Input/Input.svelte';
export { default as CheckboxFilter } from './CheckboxFilter/CheckboxFilter.svelte'; export { default as CheckboxFilter } from './CheckboxFilter/CheckboxFilter.svelte';
export { default as ComboControl } from './ComboControl/ComboControl.svelte'; export { default as ComboControl } from './ComboControl/ComboControl.svelte';
export { default as ComboControlV2 } from './ComboControlV2/ComboControlV2.svelte'; export { default as ComboControlV2 } from './ComboControlV2/ComboControlV2.svelte';
@@ -9,7 +6,12 @@ export { default as Drawer } from './Drawer/Drawer.svelte';
export { default as ExpandableWrapper } from './ExpandableWrapper/ExpandableWrapper.svelte'; export { default as ExpandableWrapper } from './ExpandableWrapper/ExpandableWrapper.svelte';
export { default as Footnote } from './Footnote/Footnote.svelte'; export { default as Footnote } from './Footnote/Footnote.svelte';
export { default as IconButton } from './IconButton/IconButton.svelte'; export { default as IconButton } from './IconButton/IconButton.svelte';
export {
Input,
type InputSize,
type InputVariant,
} from './Input';
export { default as Label } from './Label/Label.svelte';
export { default as Loader } from './Loader/Loader.svelte'; export { default as Loader } from './Loader/Loader.svelte';
export { default as Logo } from './Logo/Logo.svelte'; export { default as Logo } from './Logo/Logo.svelte';
export { default as PerspectivePlan } from './PerspectivePlan/PerspectivePlan.svelte'; export { default as PerspectivePlan } from './PerspectivePlan/PerspectivePlan.svelte';
@@ -18,16 +20,4 @@ export { default as Section } from './Section/Section.svelte';
export { default as SidebarMenu } from './SidebarMenu/SidebarMenu.svelte'; export { default as SidebarMenu } from './SidebarMenu/SidebarMenu.svelte';
export { default as Skeleton } from './Skeleton/Skeleton.svelte'; export { default as Skeleton } from './Skeleton/Skeleton.svelte';
export { default as Slider } from './Slider/Slider.svelte'; export { default as Slider } from './Slider/Slider.svelte';
export { default as SmoothScroll } from './SmoothScroll/SmoothScroll.svelte';
export { default as VirtualList } from './VirtualList/VirtualList.svelte'; export { default as VirtualList } from './VirtualList/VirtualList.svelte';
type InputProps = ComponentProps<typeof Input>;
type InputSize = InputProps['size'];
type InputVariant = InputProps['variant'];
export {
Input,
type InputProps,
type InputSize,
type InputVariant,
};

View File

@@ -205,7 +205,7 @@ const isInSettingsMode = $derived(perspective.isBack);
relative w-full flex justify-center items-center relative w-full flex justify-center items-center
perspective-distant perspective-origin-center transform-3d perspective-distant perspective-origin-center transform-3d
rounded-xl sm:rounded-2xl md:rounded-[2.5rem] rounded-xl sm:rounded-2xl md:rounded-[2.5rem]
min-h-72 sm:min-h-96 min-h-72 sm:min-h-96 lg:min-h-128
backdrop-blur-lg bg-linear-to-br from-gray-200/40 via-white/80 to-gray-100/60 backdrop-blur-lg bg-linear-to-br from-gray-200/40 via-white/80 to-gray-100/60
border border-border-muted border border-border-muted
shadow-[inset_2px_0_8px_rgba(0,0,0,0.05)] shadow-[inset_2px_0_8px_rgba(0,0,0,0.05)]

View File

@@ -11,6 +11,7 @@ import { getFontUrl } from '$entities/Font/lib';
import type { ResponsiveManager } from '$shared/lib'; import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { SidebarMenu } from '$shared/ui'; import { SidebarMenu } from '$shared/ui';
import { Label } from '$shared/ui';
import Drawer from '$shared/ui/Drawer/Drawer.svelte'; import Drawer from '$shared/ui/Drawer/Drawer.svelte';
import { comparisonStore } from '$widgets/ComparisonSlider/model'; import { comparisonStore } from '$widgets/ComparisonSlider/model';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
@@ -71,19 +72,29 @@ $effect(() => {
{#if responsive.isMobile} {#if responsive.isMobile}
<Drawer> <Drawer>
{#snippet trigger({ onClick })} {#snippet trigger({ onClick })}
<div class={cn('absolute bottom-2 inset-x-0 z-50')}> <div class={cn('absolute bottom-0.5 left-1/2 -translate-x-1/2 z-50')}>
<ToggleMenuButton bind:isActive={visible} {onClick} /> <ToggleMenuButton bind:isActive={visible} {onClick} />
</div> </div>
{/snippet} {/snippet}
{#snippet content({ className })} {#snippet content({ className })}
<div class="w-full pt-4 grid grid-cols-[1fr_min-content_1fr] gap-2 items-center justify-center">
<div class="uppercase text-indigo-500 ml-auto font-semibold tracking-tight text-[0.825rem] whitespace-nowrap">
{fontB?.name ?? 'typeface_01'}
</div>
<div class="w-px h-2.5 bg-gray-400/50"></div>
<div class="uppercase text-neutral-950 mr-auto font-semibold tracking-tight text-[0.825rem] whitespace-nowrap">
{fontA?.name ?? 'typeface_02'}
</div>
</div>
<div class={cn(className, 'flex flex-col gap-2 h-[60vh]')}> <div class={cn(className, 'flex flex-col gap-2 h-[60vh]')}>
<Label class="mb-2" text="Available Fonts" align="center" />
<div class="h-full overflow-hidden"> <div class="h-full overflow-hidden">
<FontList /> <FontList />
</div> </div>
<Label class="mb-2" text="Typography Controls" align="center" />
<div class="relative flex w-auto border-b border-gray-400/50 px-2 ml-4 mr-8 lg:mr-10 flex-shrink-0"> <div class="mx-4 flex-shrink-0">
</div>
<div class="mr-4 sm:mr-6 flex-shrink-0">
<TypographyControls /> <TypographyControls />
</div> </div>
</div> </div>
@@ -92,7 +103,7 @@ $effect(() => {
{:else} {:else}
<SidebarMenu <SidebarMenu
class={cn( class={cn(
'w-96 flex flex-col h-full pl-4 lg:pl-6 py-4 sm:py-6 sm:pt-12 gap-4 sm:gap-6 pointer-events-auto overflow-hidden', 'w-96 flex flex-col h-full pl-4 lg:pl-6 py-4 sm:py-6 sm:pt-12 gap-0 sm:gap-0 pointer-events-auto overflow-hidden',
'relative h-full transition-all duration-700 ease-out', 'relative h-full transition-all duration-700 ease-out',
className, className,
)} )}
@@ -102,15 +113,17 @@ $effect(() => {
> >
{#snippet action()} {#snippet action()}
<!-- Always-visible mode switch --> <!-- Always-visible mode switch -->
<div class={cn('absolute top-4 left-0 z-50', visible && 'w-full')}> <div class={cn('absolute top-2 left-0 z-50', visible && 'w-full')}>
<ToggleMenuButton bind:isActive={visible} onClick={handleToggle} /> <ToggleMenuButton bind:isActive={visible} onClick={handleToggle} />
</div> </div>
{/snippet} {/snippet}
<div class="h-2/3 overflow-hidden"> <Label class="mb-2 mr-4 lg:mr-6" text="Available Fonts" align="left" />
<div class="mb-2 h-2/3 overflow-hidden">
<FontList /> <FontList />
</div> </div>
<Label class="mb-2 mr-4 lg:mr-6" text="Typography Controls" align="left" />
<div class="relative flex w-auto border-b border-gray-400/50 px-2 ml-4 mr-8 lg:mr-10"></div>
<div class="mr-4 sm:mr-6"> <div class="mr-4 sm:mr-6">
<TypographyControls /> <TypographyControls />
</div> </div>

View File

@@ -132,7 +132,11 @@ function isFontB(font: UnifiedFont): boolean {
</svg> </svg>
{/snippet} {/snippet}
{#snippet brackets(renderLeft?: boolean, renderRight?: boolean, className?: string)} {#snippet brackets(
renderLeft?: boolean,
renderRight?: boolean,
className?: string,
)}
{#if renderLeft} {#if renderLeft}
{@render leftBrackets(className)} {@render leftBrackets(className)}
{/if} {/if}
@@ -156,7 +160,7 @@ function isFontB(font: UnifiedFont): boolean {
{@const handleSelectFontA = () => selectFontA(font)} {@const handleSelectFontA = () => selectFontA(font)}
{@const handleSelectFontB = () => selectFontB(font)} {@const handleSelectFontB = () => selectFontB(font)}
<div class="group relative flex w-auto h-[36px] border-b border-black/[0.03] overflow-hidden mr-4 lg:mr-6"> <div class="group relative flex w-auto h-[36px] border-b border-black/[0.03] overflow-hidden sm:mr-4 lg:mr-6">
<div <div
class={cn( class={cn(
'absolute inset-0 flex items-center justify-center z-20 pointer-events-none transition-all duration-500 cubic-bezier-out', 'absolute inset-0 flex items-center justify-center z-20 pointer-events-none transition-all duration-500 cubic-bezier-out',
@@ -168,13 +172,14 @@ function isFontB(font: UnifiedFont): boolean {
<div class="relative flex items-center px-6"> <div class="relative flex items-center px-6">
<span <span
class={cn( class={cn(
'font-mono text-[10px] sm:text-[11px] uppercase tracking-tighter select-none transition-all duration-300', 'text-[0.625rem] sm:text-[0.75rem] tracking-tighter select-none transition-all duration-300',
isEither isEither
? 'opacity-100 font-bold' ? 'opacity-100 font-bold'
: 'opacity-30 group-hover:opacity-100', : 'opacity-30 group-hover:opacity-100',
isSelectedB && 'text-indigo-500', isSelectedB && 'text-indigo-500',
isSelectedA && 'text-normal-950', isSelectedA && 'text-normal-950',
isBoth && 'text-indigo-600', isBoth
&& 'bg-[linear-gradient(to_right,theme(colors.indigo.500)_50%,theme(colors.neutral.950)_50%)] bg-clip-text text-transparent',
)} )}
> >
--- {font.name} --- --- {font.name} ---
@@ -186,14 +191,22 @@ function isFontB(font: UnifiedFont): boolean {
onclick={handleSelectFontB} onclick={handleSelectFontB}
class="flex-1 relative flex items-center justify-between transition-all duration-200 cursor-pointer hover:bg-indigo-500/[0.03]" class="flex-1 relative flex items-center justify-between transition-all duration-200 cursor-pointer hover:bg-indigo-500/[0.03]"
> >
{@render brackets(isSelectedB, isSelectedB && !isBoth, 'stroke-1 size-7 stroke-indigo-600')} {@render brackets(
isSelectedB,
isSelectedB && !isBoth,
'stroke-1 size-7 stroke-indigo-600',
)}
</button> </button>
<button <button
onclick={handleSelectFontA} onclick={handleSelectFontA}
class="flex-1 relative flex items-center justify-end transition-all duration-200 cursor-pointer hover:bg-black/[0.02]" class="flex-1 relative flex items-center justify-end transition-all duration-200 cursor-pointer hover:bg-black/[0.02]"
> >
{@render brackets(isSelectedA && !isBoth, isSelectedA, 'stroke-1 size-7 stroke-normal-950')} {@render brackets(
isSelectedA && !isBoth,
isSelectedA,
'stroke-1 size-7 stroke-normal-950',
)}
</button> </button>
</div> </div>
{/snippet} {/snippet}

View File

@@ -22,7 +22,7 @@ let { sliderPos, isDragging }: Props = $props();
<div <div
class={cn( class={cn(
'absolute inset-y-2 sm:inset-y-4 pointer-events-none -translate-x-1/2 z-50 flex flex-col justify-center items-center', 'absolute top-2 bottom-8 sm:top-4 sm:bottom-4 pointer-events-none -translate-x-1/2 z-50 flex flex-col justify-center items-center',
// Force GPU layer with translateZ // Force GPU layer with translateZ
'translate-z-0', 'translate-z-0',
// Only transition left when NOT dragging // Only transition left when NOT dragging

View File

@@ -36,16 +36,25 @@ const fontB = $derived(comparisonStore.fontB);
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class={cn('lucide lucide-circle-arrow-right-icon lucide-circle-arrow-right', className)} class={cn(
'lucide lucide-circle-arrow-right-icon lucide-circle-arrow-right',
className,
)}
> >
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
{#if isActive} {#if isActive}
<path transition:draw={{ duration: 150, delay: 150, easing: cubicOut }} d="m15 9-6 6" /><path <path
transition:draw={{ duration: 150, delay: 150, easing: cubicOut }}
d="m15 9-6 6"
/><path
transition:draw={{ duration: 150, delay: 150, easing: cubicOut }} transition:draw={{ duration: 150, delay: 150, easing: cubicOut }}
d="m9 9 6 6" d="m9 9 6 6"
/> />
{:else} {:else}
<path transition:draw={{ duration: 150, delay: 150, easing: cubicOut }} d="m12 16 4-4-4-4" /><path <path
transition:draw={{ duration: 150, delay: 150, easing: cubicOut }}
d="m12 16 4-4-4-4"
/><path
transition:draw={{ duration: 150, delay: 150, easing: cubicOut }} transition:draw={{ duration: 150, delay: 150, easing: cubicOut }}
d="M8 12h8" d="M8 12h8"
/> />
@@ -62,13 +71,18 @@ const fontB = $derived(comparisonStore.fontB);
'transition-transform duration-150 active:scale-98', 'transition-transform duration-150 active:scale-98',
)} )}
> >
{@render icon('size-4 stroke-[1.5] stroke-gray-500')} {@render icon(
cn(
'size-4 stroke-[1.5] stroke-gray-500',
!isActive && 'rotate-90 sm:rotate-0',
),
)}
<div class="w-px h-2.5 bg-gray-400/50"></div> <div class="w-px h-2.5 bg-gray-400/50"></div>
<div class="text-xs uppercase transition-all delay-150 group-hover:opacity-100 text-indigo-500 text-right"> <div class="text-[0.75rem] sm:text-xs transition-all delay-150 group-hover:text-semibold text-indigo-500 text-right whitespace-nowrap">
{fontB?.name} {fontB?.name}
</div> </div>
<div class="w-px h-2.5 bg-gray-400/50"></div> <div class="w-px h-2.5 bg-gray-400/50"></div>
<div class="text-xs uppercase transition-all delay-150 group-hover:opacity-100 text-neural-950 text-left"> <div class="text-[0.75rem] sm:text-xs transition-all delay-150 group-hover:text-semibold text-neural-950 text-left whitespace-nowrap">
{fontA?.name} {fontA?.name}
</div> </div>
</button> </button>

View File

@@ -4,7 +4,6 @@
Simplified version for static positioning in settings mode. Simplified version for static positioning in settings mode.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
ComboControlV2, ComboControlV2,
Input, Input,
@@ -20,30 +19,35 @@ const typography = $derived(comparisonStore.typography);
size="sm" size="sm"
label="Text" label="Text"
placeholder="The quick brown fox..." placeholder="The quick brown fox..."
class="w-full px-3 py-2 h-10 rounded-lg border border-border-muted bg-background-60 backdrop-blur-sm mr-4" class="w-full h-10 px-3 py-2 sm:mr-4 mb-8 sm:mb-4 rounded-lg border border-border-muted bg-background-60 backdrop-blur-sm"
/> />
<!-- Typography controls --> <!-- Typography controls -->
{#if typography.weightControl && typography.sizeControl && typography.heightControl} {#if typography.weightControl && typography.sizeControl && typography.heightControl}
<div class="flex flex-col gap-1.5 mt-1.5"> <div class="flex flex-col mt-1.5">
<ComboControlV2 <ComboControlV2
control={typography.weightControl} control={typography.weightControl}
orientation="horizontal" orientation="horizontal"
class="sm:py-0" class="sm:py-0 sm:px-0 mb-5 sm:mb-1.5"
label="font weight"
showScale={false} showScale={false}
reduced reduced
/> />
<ComboControlV2 <ComboControlV2
control={typography.sizeControl} control={typography.sizeControl}
orientation="horizontal" orientation="horizontal"
class="sm:py-0" class="sm:py-0 sm:px-0 mb-5 sm:mb-1.5"
label="font size"
showScale={false} showScale={false}
reduced reduced
/> />
<ComboControlV2 <ComboControlV2
control={typography.heightControl} control={typography.heightControl}
orientation="horizontal" orientation="horizontal"
class="sm:py-0" class="sm:py-0 sm:px-0"
label="line height"
showScale={false} showScale={false}
reduced reduced
/> />

View File

@@ -33,7 +33,7 @@ interface Props {
showFilters?: boolean; showFilters?: boolean;
} }
let { showFilters = $bindable(false) }: Props = $props(); let { showFilters = $bindable(true) }: Props = $props();
onMount(() => { onMount(() => {
/** /**

View File

@@ -2459,7 +2459,6 @@ __metadata:
dprint: "npm:^0.50.2" dprint: "npm:^0.50.2"
jsdom: "npm:^27.4.0" jsdom: "npm:^27.4.0"
lefthook: "npm:^2.0.13" lefthook: "npm:^2.0.13"
lenis: "npm:^1.3.17"
oxlint: "npm:^1.35.0" oxlint: "npm:^1.35.0"
playwright: "npm:^1.57.0" playwright: "npm:^1.57.0"
storybook: "npm:^10.1.11" storybook: "npm:^10.1.11"
@@ -2850,24 +2849,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lenis@npm:^1.3.17":
version: 1.3.17
resolution: "lenis@npm:1.3.17"
peerDependencies:
"@nuxt/kit": ">=3.0.0"
react: ">=17.0.0"
vue: ">=3.0.0"
peerDependenciesMeta:
"@nuxt/kit":
optional: true
react:
optional: true
vue:
optional: true
checksum: 10c0/c268da36d5711677b239c7d173bc52775276df08f86f7f89f305c4e02ba4055d8c50ea69125d16c94bb1e1999ccd95f654237d11c6647dc5fdf63aa90515fbfb
languageName: node
linkType: hard
"lightningcss-android-arm64@npm:1.30.2": "lightningcss-android-arm64@npm:1.30.2":
version: 1.30.2 version: 1.30.2
resolution: "lightningcss-android-arm64@npm:1.30.2" resolution: "lightningcss-android-arm64@npm:1.30.2"