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"
},
"dependencies": {
"@tanstack/svelte-query": "^6.0.14",
"lenis": "^1.3.17"
"@tanstack/svelte-query": "^6.0.14"
}
}

View File

@@ -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;
}

View File

@@ -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?.()}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

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,
} from './createResponsiveManager/createResponsiveManager.svelte';
export {
createLenisContext,
getLenisContext,
setLenisContext,
} from './createScrollContext/createScrollContext.svelte';
export {
createPerspectiveManager,
type PerspectiveManager,

View File

@@ -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,

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}
/>

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">
<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>

View File

@@ -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>

View File

@@ -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 }}
>

View File

@@ -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',

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 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,
};

View File

@@ -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)]

View File

@@ -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>

View File

@@ -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}

View File

@@ -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

View File

@@ -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>

View File

@@ -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
/>

View File

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

View File

@@ -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"