Compare commits
23 Commits
ad6e1da292
...
aa4796079a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa4796079a | ||
|
|
f18454f9b3 | ||
|
|
e3924d43d8 | ||
|
|
0f6a4d6587 | ||
|
|
8f4faa3328 | ||
|
|
5867028be6 | ||
|
|
b8d019b824 | ||
|
|
45ed0d5601 | ||
|
|
9f91fed692 | ||
|
|
201280093f | ||
|
|
55b27973a2 | ||
|
|
5fa79e06e9 | ||
|
|
ee0749e828 | ||
|
|
5dae5fb7ea | ||
|
|
20f65ee396 | ||
|
|
010b8ad04b | ||
|
|
ce1dcd92ab | ||
|
|
ce609728c3 | ||
|
|
147df04c22 | ||
|
|
f356851d97 | ||
|
|
411dbfefcb | ||
|
|
a65d692139 | ||
|
|
3330f13228 |
@@ -67,7 +67,6 @@
|
||||
"vitest-browser-svelte": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/svelte-query": "^6.0.14",
|
||||
"lenis": "^1.3.17"
|
||||
"@tanstack/svelte-query": "^6.0.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
--gradient-from: oklch(0.98 0.002 286.32);
|
||||
--gradient-via: oklch(1 0 0);
|
||||
--gradient-to: oklch(0.98 0.002 286.32);
|
||||
|
||||
--font-mono: 'Major Mono Display';
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -165,6 +167,7 @@
|
||||
--color-gradient-from: var(--gradient-from);
|
||||
--color-gradient-via: var(--gradient-via);
|
||||
--color-gradient-to: var(--gradient-to);
|
||||
--font-mono: var(--font-mono);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -222,3 +225,82 @@
|
||||
.barlow {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -55,30 +55,36 @@ onMount(async () => {
|
||||
<link rel="icon" href={GD} />
|
||||
|
||||
<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.gstatic.com" crossorigin="anonymous">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
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
|
||||
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"
|
||||
onload={(e => ((e.currentTarget as HTMLLinkElement).media = 'all'))}
|
||||
>
|
||||
/>
|
||||
<noscript>
|
||||
<link
|
||||
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>
|
||||
<title>
|
||||
Compare Typography & Typefaces | GlyphDiff
|
||||
</title>
|
||||
<title>Compare Typography & Typefaces | GlyphDiff</title>
|
||||
</svelte:head>
|
||||
|
||||
<ResponsiveProvider>
|
||||
@@ -88,7 +94,7 @@ onMount(async () => {
|
||||
</header>
|
||||
|
||||
<!-- <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>
|
||||
{#if fontsReady}
|
||||
{@render children?.()}
|
||||
|
||||
@@ -36,12 +36,7 @@ interface Props {
|
||||
letterSpacing?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
font,
|
||||
text = $bindable(),
|
||||
index = 0,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
let { font, text = $bindable(), index = 0, ...restProps }: Props = $props();
|
||||
|
||||
const fontWeight = $derived(controlManager.weight);
|
||||
const fontSize = $derived(controlManager.renderedSize);
|
||||
@@ -66,9 +61,9 @@ const letterSpacing = $derived(controlManager.spacing);
|
||||
typeface_{String(index).padStart(3, '0')}
|
||||
</Footnote>
|
||||
<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}
|
||||
</Footnote>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
@@ -86,11 +81,11 @@ const letterSpacing = $derived(controlManager.spacing);
|
||||
<div class="p-4 sm:p-5 md:p-8 relative z-10">
|
||||
<FontApplicator {font} weight={fontWeight}>
|
||||
<ContentEditable
|
||||
bind:text={text}
|
||||
bind:text
|
||||
{...restProps}
|
||||
fontSize={fontSize}
|
||||
lineHeight={lineHeight}
|
||||
letterSpacing={letterSpacing}
|
||||
{fontSize}
|
||||
{lineHeight}
|
||||
{letterSpacing}
|
||||
/>
|
||||
</FontApplicator>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Drawer,
|
||||
IconButton,
|
||||
} from '$shared/ui';
|
||||
import { Label } from '$shared/ui';
|
||||
import SlidersIcon from '@lucide/svelte/icons/sliders-vertical';
|
||||
import { getContext } from 'svelte';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
@@ -72,7 +73,11 @@ $effect(() => {
|
||||
</script>
|
||||
|
||||
<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' }}
|
||||
out:send={{ key: 'panel' }}
|
||||
>
|
||||
@@ -86,11 +91,17 @@ $effect(() => {
|
||||
</IconButton>
|
||||
{/snippet}
|
||||
{#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)}
|
||||
<ComboControlV2
|
||||
control={control.instance}
|
||||
orientation="horizontal"
|
||||
label={control.controlLabel}
|
||||
reduced
|
||||
/>
|
||||
{/each}
|
||||
@@ -112,6 +123,7 @@ $effect(() => {
|
||||
decreaseLabel={control.decreaseLabel}
|
||||
controlLabel={control.controlLabel}
|
||||
orientation="vertical"
|
||||
showScale={false}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { scrollBreadcrumbsStore } from '$entities/Breadcrumb';
|
||||
import type { ResponsiveManager } from '$shared/lib';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import {
|
||||
Logo,
|
||||
Section,
|
||||
@@ -15,13 +17,17 @@ import CodeIcon from '@lucide/svelte/icons/code';
|
||||
import EyeIcon from '@lucide/svelte/icons/eye';
|
||||
import LineSquiggleIcon from '@lucide/svelte/icons/line-squiggle';
|
||||
import ScanSearchIcon from '@lucide/svelte/icons/search';
|
||||
import type { Snippet } from 'svelte';
|
||||
import {
|
||||
type Snippet,
|
||||
getContext,
|
||||
} from 'svelte';
|
||||
import { cubicIn } from 'svelte/easing';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let searchContainer: HTMLElement;
|
||||
|
||||
let isExpanded = $state(false);
|
||||
let isExpanded = $state(true);
|
||||
const responsive = getContext<ResponsiveManager>('responsive');
|
||||
|
||||
function handleTitleStatusChanged(
|
||||
index: number,
|
||||
@@ -39,39 +45,37 @@ function handleTitleStatusChanged(
|
||||
scrollBreadcrumbsStore.remove(index);
|
||||
};
|
||||
}
|
||||
|
||||
// $effect(() => {
|
||||
// appliedFontsManager.touch(
|
||||
// selectedFontsStore.all.map(font => ({
|
||||
// slug: font.id,
|
||||
// weight: controlManager.weight,
|
||||
// })),
|
||||
// );
|
||||
// });
|
||||
</script>
|
||||
|
||||
<!-- Font List -->
|
||||
<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 }}
|
||||
>
|
||||
<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 })}
|
||||
<CodeIcon class={className} />
|
||||
{/snippet}
|
||||
{#snippet description({ className })}
|
||||
<span class={className}>
|
||||
Project_Codename
|
||||
</span>
|
||||
<span class={className}> Project_Codename </span>
|
||||
{/snippet}
|
||||
{#snippet content({ className })}
|
||||
<div class={cn(className, 'col-start-0 col-span-2')}>
|
||||
<Logo />
|
||||
</div>
|
||||
{/snippet}
|
||||
<Logo />
|
||||
</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}
|
||||
id="optical_comparator"
|
||||
onTitleStatusChange={handleTitleStatusChanged}
|
||||
stickyTitle={responsive.isDesktopLarge}
|
||||
stickyOffset="4rem"
|
||||
>
|
||||
{#snippet icon({ className })}
|
||||
<EyeIcon class={className} />
|
||||
@@ -81,14 +85,20 @@ function handleTitleStatusChanged(
|
||||
Optical<br />Comparator
|
||||
</h1>
|
||||
{/snippet}
|
||||
<ComparisonSlider />
|
||||
{#snippet content({ className })}
|
||||
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
||||
<ComparisonSlider />
|
||||
</div>
|
||||
{/snippet}
|
||||
</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}
|
||||
id="query_module"
|
||||
onTitleStatusChange={handleTitleStatusChanged}
|
||||
stickyTitle={responsive.isDesktopLarge}
|
||||
stickyOffset="4rem"
|
||||
>
|
||||
{#snippet icon({ className })}
|
||||
<ScanSearchIcon class={className} />
|
||||
@@ -98,14 +108,20 @@ function handleTitleStatusChanged(
|
||||
Query<br />Module
|
||||
</h2>
|
||||
{/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
|
||||
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}
|
||||
id="sample_set"
|
||||
onTitleStatusChange={handleTitleStatusChanged}
|
||||
stickyTitle={responsive.isDesktopLarge}
|
||||
stickyOffset="4rem"
|
||||
>
|
||||
{#snippet icon({ className })}
|
||||
<LineSquiggleIcon class={className} />
|
||||
@@ -115,18 +131,22 @@ function handleTitleStatusChanged(
|
||||
Sample<br />Set
|
||||
</h2>
|
||||
{/snippet}
|
||||
<SampleList />
|
||||
{#snippet content({ className })}
|
||||
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
||||
<SampleList />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
/* Tells the browser to skip rendering off-screen content */
|
||||
content-visibility: auto;
|
||||
/* Helps the browser reserve space without calculating everything */
|
||||
contain-intrinsic-size: 1px 1000px;
|
||||
/* Tells the browser to skip rendering off-screen content */
|
||||
content-visibility: auto;
|
||||
/* Helps the browser reserve space without calculating everything */
|
||||
contain-intrinsic-size: 1px 1000px;
|
||||
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -44,12 +44,6 @@ export {
|
||||
responsiveManager,
|
||||
} from './createResponsiveManager/createResponsiveManager.svelte';
|
||||
|
||||
export {
|
||||
createLenisContext,
|
||||
getLenisContext,
|
||||
setLenisContext,
|
||||
} from './createScrollContext/createScrollContext.svelte';
|
||||
|
||||
export {
|
||||
createPerspectiveManager,
|
||||
type PerspectiveManager,
|
||||
|
||||
@@ -6,7 +6,6 @@ export {
|
||||
createDebouncedState,
|
||||
createEntityStore,
|
||||
createFilter,
|
||||
createLenisContext,
|
||||
createPersistentStore,
|
||||
createPerspectiveManager,
|
||||
createResponsiveManager,
|
||||
@@ -16,14 +15,12 @@ export {
|
||||
type EntityStore,
|
||||
type Filter,
|
||||
type FilterModel,
|
||||
getLenisContext,
|
||||
type LineData,
|
||||
type PersistentStore,
|
||||
type PerspectiveManager,
|
||||
type Property,
|
||||
type ResponsiveManager,
|
||||
responsiveManager,
|
||||
setLenisContext,
|
||||
type TypographyControl,
|
||||
type VirtualItem,
|
||||
type Virtualizer,
|
||||
|
||||
@@ -98,11 +98,11 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
||||
function calculateScale(index: number): number | string {
|
||||
const calculate = () =>
|
||||
orientation === 'horizontal'
|
||||
? (control.min + (index * (control.max - control.min) / 4))
|
||||
: (control.max - (index * (control.max - control.min) / 4));
|
||||
? control.min + (index * (control.max - control.min)) / 4
|
||||
: control.max - (index * (control.max - control.min)) / 4;
|
||||
return Number.isInteger(control.step)
|
||||
? Math.round(calculate())
|
||||
: (calculate()).toFixed(2);
|
||||
: calculate().toFixed(2);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -111,7 +111,9 @@ function calculateScale(index: number): number | string {
|
||||
class={cn(
|
||||
'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,
|
||||
)}
|
||||
>
|
||||
@@ -120,7 +122,9 @@ function calculateScale(index: number): number | string {
|
||||
<div
|
||||
class={cn(
|
||||
'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}
|
||||
@@ -133,7 +137,12 @@ function calculateScale(index: number): number | string {
|
||||
<span class="font-mono text-[0.375rem] text-text-muted tabular-nums">
|
||||
{calculateScale(i)}
|
||||
</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>
|
||||
{/each}
|
||||
@@ -146,6 +155,7 @@ function calculateScale(index: number): number | string {
|
||||
min={control.min}
|
||||
max={control.max}
|
||||
step={control.step}
|
||||
{label}
|
||||
{orientation}
|
||||
/>
|
||||
</div>
|
||||
@@ -162,16 +172,6 @@ function calculateScale(index: number): number | string {
|
||||
variant="ghost"
|
||||
/>
|
||||
{/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>
|
||||
{/snippet}
|
||||
|
||||
|
||||
@@ -16,16 +16,22 @@ interface 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>
|
||||
|
||||
{#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}
|
||||
<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()}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -27,19 +27,19 @@ export const inputVariants = tv({
|
||||
'h-9 sm:h-10 md:h-11 rounded-lg',
|
||||
'px-3 sm:px-3.5 md:px-4',
|
||||
'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: [
|
||||
'h-10 sm:h-12 md:h-14 rounded-xl',
|
||||
'px-3.5 sm:px-4 md:px-5',
|
||||
'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: [
|
||||
'h-12 sm:h-14 md:h-16 rounded-2xl',
|
||||
'px-4 sm:px-5 md:px-6',
|
||||
'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>
|
||||
|
||||
<BaseInput
|
||||
bind:value={value}
|
||||
bind:value
|
||||
class={cn(inputVariants({ variant, size }), className)}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
13
src/shared/ui/Input/index.ts
Normal file
13
src/shared/ui/Input/index.ts
Normal 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,
|
||||
};
|
||||
45
src/shared/ui/Label/Label.svelte
Normal file
45
src/shared/ui/Label/Label.svelte
Normal 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>
|
||||
@@ -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">
|
||||
<AsteriskIcon class="size-3 sm:size-4 stroke-gray-400 stroke-[1.5]" />
|
||||
</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>
|
||||
|
||||
@@ -56,13 +56,40 @@ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
|
||||
/**
|
||||
* 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>();
|
||||
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
|
||||
let isScrolledPast = $state(false);
|
||||
@@ -72,18 +99,21 @@ $effect(() => {
|
||||
return;
|
||||
}
|
||||
let cleanup: ((index: number) => void) | undefined;
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
const entry = entries[0];
|
||||
const isPast = !entry.isIntersecting && entry.boundingClientRect.top < 0;
|
||||
const observer = new IntersectionObserver(
|
||||
entries => {
|
||||
const entry = entries[0];
|
||||
const isPast = !entry.isIntersecting && entry.boundingClientRect.top < 0;
|
||||
|
||||
if (isPast !== isScrolledPast) {
|
||||
isScrolledPast = isPast;
|
||||
cleanup = onTitleStatusChange?.(index, isPast, title, id);
|
||||
}
|
||||
}, {
|
||||
// Set threshold to 0 to trigger exactly when the last pixel leaves
|
||||
threshold: 0,
|
||||
});
|
||||
if (isPast !== isScrolledPast) {
|
||||
isScrolledPast = isPast;
|
||||
cleanup = onTitleStatusChange?.(index, isPast, title, id);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Set threshold to 0 to trigger exactly when the last pixel leaves
|
||||
threshold: 0,
|
||||
},
|
||||
);
|
||||
|
||||
observer.observe(titleContainer);
|
||||
return () => {
|
||||
@@ -94,20 +124,32 @@ $effect(() => {
|
||||
</script>
|
||||
|
||||
<section
|
||||
id={id}
|
||||
{id}
|
||||
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,
|
||||
)}
|
||||
in: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">
|
||||
{#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>
|
||||
{/if}
|
||||
|
||||
{#if description}
|
||||
<Footnote>
|
||||
{#snippet render({ class: className })}
|
||||
@@ -129,5 +171,9 @@ $effect(() => {
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{@render children?.()}
|
||||
{@render content?.({
|
||||
className: stickyTitle
|
||||
? 'row-start-2 col-start-2'
|
||||
: 'row-start-2 col-start-2',
|
||||
})}
|
||||
</section>
|
||||
|
||||
@@ -73,7 +73,7 @@ function handleClick(event: MouseEvent) {
|
||||
{@render action?.()}
|
||||
{#if visible}
|
||||
<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 }}
|
||||
out:fade={{ duration: 150, easing: cubicOut }}
|
||||
>
|
||||
|
||||
@@ -9,28 +9,43 @@ import {
|
||||
type SliderRootProps,
|
||||
} from 'bits-ui';
|
||||
|
||||
type Props = Omit<SliderRootProps, 'type' | 'onValueChange' | 'onValueCommit'> & {
|
||||
/**
|
||||
* Slider value, numeric.
|
||||
*/
|
||||
value: number;
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
type Props =
|
||||
& Omit<
|
||||
SliderRootProps,
|
||||
'type' | 'onValueChange' | 'onValueCommit'
|
||||
>
|
||||
& {
|
||||
/**
|
||||
* Slider value, numeric.
|
||||
*/
|
||||
value: number;
|
||||
/**
|
||||
* Optional label displayed inline on the track before the filled range.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* 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>
|
||||
|
||||
<Slider.Root
|
||||
bind:value={value}
|
||||
bind:value
|
||||
class={cn(
|
||||
'relative flex h-full w-6 touch-none select-none items-center justify-center',
|
||||
orientation === 'horizontal' ? 'w-48 h-6' : 'w-6 h-48',
|
||||
@@ -41,13 +56,23 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
|
||||
{...rest}
|
||||
>
|
||||
{#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
|
||||
{...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
|
||||
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
|
||||
@@ -56,27 +81,32 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
|
||||
'group/thumb relative block',
|
||||
'size-2',
|
||||
orientation === 'horizontal' ? '-top-1' : '-left-1',
|
||||
'rounded-sm',
|
||||
'rounded-full',
|
||||
'bg-foreground',
|
||||
// Glow shadow
|
||||
'shadow-[0_0_6px_rgba(0,0,0,0.4)]',
|
||||
// Smooth transitions only for size/position
|
||||
'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: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: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',
|
||||
'cursor-grab active:cursor-grabbing',
|
||||
)}
|
||||
>
|
||||
<!-- Soft glow on hover -->
|
||||
<div
|
||||
class="
|
||||
absolute inset-0 rounded-sm
|
||||
absolute inset-0 rounded-full
|
||||
bg-background-20
|
||||
opacity-0 group-hover/thumb:opacity-100
|
||||
transition-opacity duration-200
|
||||
@@ -84,11 +114,12 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Value label -->
|
||||
<span
|
||||
class={cn(
|
||||
'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',
|
||||
'bg-foreground/90 backdrop-blur-sm',
|
||||
'font-mono text-[0.625rem] font-medium text-background',
|
||||
|
||||
@@ -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?.()}
|
||||
@@ -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 ComboControl } from './ComboControl/ComboControl.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 Footnote } from './Footnote/Footnote.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 Logo } from './Logo/Logo.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 Skeleton } from './Skeleton/Skeleton.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';
|
||||
|
||||
type InputProps = ComponentProps<typeof Input>;
|
||||
type InputSize = InputProps['size'];
|
||||
type InputVariant = InputProps['variant'];
|
||||
|
||||
export {
|
||||
Input,
|
||||
type InputProps,
|
||||
type InputSize,
|
||||
type InputVariant,
|
||||
};
|
||||
|
||||
@@ -205,7 +205,7 @@ const isInSettingsMode = $derived(perspective.isBack);
|
||||
relative w-full flex justify-center items-center
|
||||
perspective-distant perspective-origin-center transform-3d
|
||||
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
|
||||
border border-border-muted
|
||||
shadow-[inset_2px_0_8px_rgba(0,0,0,0.05)]
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getFontUrl } from '$entities/Font/lib';
|
||||
import type { ResponsiveManager } from '$shared/lib';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import { SidebarMenu } from '$shared/ui';
|
||||
import { Label } from '$shared/ui';
|
||||
import Drawer from '$shared/ui/Drawer/Drawer.svelte';
|
||||
import { comparisonStore } from '$widgets/ComparisonSlider/model';
|
||||
import { getContext } from 'svelte';
|
||||
@@ -71,19 +72,29 @@ $effect(() => {
|
||||
{#if responsive.isMobile}
|
||||
<Drawer>
|
||||
{#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} />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#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]')}>
|
||||
<Label class="mb-2" text="Available Fonts" align="center" />
|
||||
|
||||
<div class="h-full overflow-hidden">
|
||||
<FontList />
|
||||
</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>
|
||||
<div class="mr-4 sm:mr-6 flex-shrink-0">
|
||||
<div class="mx-4 flex-shrink-0">
|
||||
<TypographyControls />
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,7 +103,7 @@ $effect(() => {
|
||||
{:else}
|
||||
<SidebarMenu
|
||||
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',
|
||||
className,
|
||||
)}
|
||||
@@ -102,15 +113,17 @@ $effect(() => {
|
||||
>
|
||||
{#snippet action()}
|
||||
<!-- 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} />
|
||||
</div>
|
||||
{/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 />
|
||||
</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">
|
||||
<TypographyControls />
|
||||
</div>
|
||||
|
||||
@@ -132,7 +132,11 @@ function isFontB(font: UnifiedFont): boolean {
|
||||
</svg>
|
||||
{/snippet}
|
||||
|
||||
{#snippet brackets(renderLeft?: boolean, renderRight?: boolean, className?: string)}
|
||||
{#snippet brackets(
|
||||
renderLeft?: boolean,
|
||||
renderRight?: boolean,
|
||||
className?: string,
|
||||
)}
|
||||
{#if renderLeft}
|
||||
{@render leftBrackets(className)}
|
||||
{/if}
|
||||
@@ -156,7 +160,7 @@ function isFontB(font: UnifiedFont): boolean {
|
||||
{@const handleSelectFontA = () => selectFontA(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
|
||||
class={cn(
|
||||
'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">
|
||||
<span
|
||||
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
|
||||
? 'opacity-100 font-bold'
|
||||
: 'opacity-30 group-hover:opacity-100',
|
||||
isSelectedB && 'text-indigo-500',
|
||||
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} ---
|
||||
@@ -186,14 +191,22 @@ function isFontB(font: UnifiedFont): boolean {
|
||||
onclick={handleSelectFontB}
|
||||
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
|
||||
onclick={handleSelectFontA}
|
||||
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>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -22,7 +22,7 @@ let { sliderPos, isDragging }: Props = $props();
|
||||
|
||||
<div
|
||||
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
|
||||
'translate-z-0',
|
||||
// Only transition left when NOT dragging
|
||||
|
||||
@@ -36,16 +36,25 @@ const fontB = $derived(comparisonStore.fontB);
|
||||
stroke-width="2"
|
||||
stroke-linecap="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" />
|
||||
{#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 }}
|
||||
d="m9 9 6 6"
|
||||
/>
|
||||
{: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 }}
|
||||
d="M8 12h8"
|
||||
/>
|
||||
@@ -62,13 +71,18 @@ const fontB = $derived(comparisonStore.fontB);
|
||||
'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="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}
|
||||
</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}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
Simplified version for static positioning in settings mode.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import {
|
||||
ComboControlV2,
|
||||
Input,
|
||||
@@ -20,30 +19,35 @@ const typography = $derived(comparisonStore.typography);
|
||||
size="sm"
|
||||
label="Text"
|
||||
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 -->
|
||||
{#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
|
||||
control={typography.weightControl}
|
||||
orientation="horizontal"
|
||||
class="sm:py-0"
|
||||
class="sm:py-0 sm:px-0 mb-5 sm:mb-1.5"
|
||||
label="font weight"
|
||||
showScale={false}
|
||||
reduced
|
||||
/>
|
||||
|
||||
<ComboControlV2
|
||||
control={typography.sizeControl}
|
||||
orientation="horizontal"
|
||||
class="sm:py-0"
|
||||
class="sm:py-0 sm:px-0 mb-5 sm:mb-1.5"
|
||||
label="font size"
|
||||
showScale={false}
|
||||
reduced
|
||||
/>
|
||||
|
||||
<ComboControlV2
|
||||
control={typography.heightControl}
|
||||
orientation="horizontal"
|
||||
class="sm:py-0"
|
||||
class="sm:py-0 sm:px-0"
|
||||
label="line height"
|
||||
showScale={false}
|
||||
reduced
|
||||
/>
|
||||
|
||||
@@ -33,7 +33,7 @@ interface Props {
|
||||
showFilters?: boolean;
|
||||
}
|
||||
|
||||
let { showFilters = $bindable(false) }: Props = $props();
|
||||
let { showFilters = $bindable(true) }: Props = $props();
|
||||
|
||||
onMount(() => {
|
||||
/**
|
||||
|
||||
19
yarn.lock
19
yarn.lock
@@ -2459,7 +2459,6 @@ __metadata:
|
||||
dprint: "npm:^0.50.2"
|
||||
jsdom: "npm:^27.4.0"
|
||||
lefthook: "npm:^2.0.13"
|
||||
lenis: "npm:^1.3.17"
|
||||
oxlint: "npm:^1.35.0"
|
||||
playwright: "npm:^1.57.0"
|
||||
storybook: "npm:^10.1.11"
|
||||
@@ -2850,24 +2849,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.30.2
|
||||
resolution: "lightningcss-android-arm64@npm:1.30.2"
|
||||
|
||||
Reference in New Issue
Block a user