Compare commits

...

8 Commits

Author SHA1 Message Date
Ilia Mashkov
ef48d9815c feat(Page): add Section wrappers to page widgets 2026-01-30 17:46:21 +03:00
Ilia Mashkov
818dfdb55e feat(Layout): increase bottom gap for TypographyMenu 2026-01-30 17:45:08 +03:00
Ilia Mashkov
42e1271647 feat(ContentEditable): add comments 2026-01-30 17:44:18 +03:00
Ilia Mashkov
8ef9226dd2 feat(IconButton): slightlyu change the style 2026-01-30 17:43:46 +03:00
Ilia Mashkov
f0c0a9de45 feat(ComparisonSlider): move in/out animation to Section component 2026-01-30 17:43:19 +03:00
Ilia Mashkov
730eba138d feat(FontSampler): refactor styles of FontSampler component 2026-01-30 17:42:06 +03:00
Ilia Mashkov
18f265974e chore: add import/export 2026-01-30 17:40:26 +03:00
Ilia Mashkov
705723b009 feat(Section): create a section wrapper for a page 2026-01-30 17:40:11 +03:00
9 changed files with 168 additions and 40 deletions

View File

@@ -41,7 +41,7 @@ let { children }: Props = $props();
<header></header> <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-4 py-6 md:px-8 lg:py-10 relative"> <main class="flex-1 h-full w-full max-w-6xl mx-auto px-4 pt-6 pb-10 md:px-8 lg:pt-10 lg:pb-20 relative">
<TooltipProvider> <TooltipProvider>
<TypographyMenu /> <TypographyMenu />
{@render children?.()} {@render children?.()}

View File

@@ -9,10 +9,10 @@ import { displayedFontsStore } from '../../model';
import FontSampler from '../FontSampler/FontSampler.svelte'; import FontSampler from '../FontSampler/FontSampler.svelte';
</script> </script>
<div class="grid gap-2 grid-cols-[repeat(auto-fit,minmax(500px,1fr))]"> <div class="grid gap-2 grid-cols-[repeat(auto-fit,minmax(500px,1fr))] will-change-tranform transition-transform content">
{#each displayedFontsStore.fonts as font (font.id)} {#each displayedFontsStore.fonts as font, index (font.id)}
<div animate:flip={{ delay: 0, duration: 400, easing: quintOut }}> <div animate:flip={{ delay: 0, duration: 400, easing: quintOut }}>
<FontSampler font={font} bind:text={displayedFontsStore.text} /> <FontSampler font={font} bind:text={displayedFontsStore.text} index={index} />
</div> </div>
{/each} {/each}
</div> </div>

View File

@@ -9,12 +9,11 @@ import {
selectedFontsStore, selectedFontsStore,
} from '$entities/Font'; } from '$entities/Font';
import { controlManager } from '$features/SetupFont'; import { controlManager } from '$features/SetupFont';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
ContentEditable, ContentEditable,
IconButton, IconButton,
} from '$shared/ui'; } from '$shared/ui';
import MinusIcon from '@lucide/svelte/icons/minus'; import XIcon from '@lucide/svelte/icons/x';
interface Props { interface Props {
/** /**
@@ -25,6 +24,10 @@ interface Props {
* Text to display * Text to display
*/ */
text: string; text: string;
/**
* Index of the font sampler
*/
index?: number;
/** /**
* Font settings * Font settings
*/ */
@@ -36,6 +39,7 @@ interface Props {
let { let {
font, font,
text = $bindable(), text = $bindable(),
index = 0,
...restProps ...restProps
}: Props = $props(); }: Props = $props();
@@ -51,23 +55,37 @@ function removeSample() {
<div <div
class=" class="
w-full h-full rounded-xl w-full h-full rounded-2xl
bg-white border border-slate-200 flex flex-col
shadow-sm dark:border-slate-800 dark:bg-slate-950 backdrop-blur-md bg-white/80
border border-gray-300/50
shadow-[0_1px_3px_rgba(0,0,0,0.04)]
relative overflow-hidden
" "
style:font-weight={fontWeight} style:font-weight={fontWeight}
> >
<div class="mx-3 p-1.5 pr-0 border-b border-slate-200 flex items-center justify-between"> <div class="px-6 py-3 border-b border-gray-200/60 flex items-center justify-between">
<span class="text-[0.5rem] font-mono uppercase tracking-widest text-slate-900"> <div class="flex items-center gap-2.5">
<span class="font-mono text-[9px] uppercase tracking-[0.25em] text-gray-500 font-medium">
typeface_{String(index).padStart(3, '0')}
</span>
<div class="w-px h-2.5 bg-gray-300/60"></div>
<span class="font-mono text-[10px] tracking-[0.15em] font-bold uppercase text-gray-900">
{font.name} {font.name}
</span> </span>
<IconButton onclick={removeSample}> </div>
<IconButton
onclick={removeSample}
class="w-5 h-5 rounded-full hover:bg-transparent flex items-center justify-center transition-colors group translate-x-1/2 cursor-pointer"
>
{#snippet icon({ className })} {#snippet icon({ className })}
<MinusIcon class={cn(className, 'stroke-red-500 group-hover:stroke-red-500')} /> <XIcon class={className} />
{/snippet} {/snippet}
</IconButton> </IconButton>
</div> </div>
<div class="p-6">
<div class="p-8 relative z-10">
<FontApplicator id={font.id} name={font.name}> <FontApplicator id={font.id} name={font.name}>
<ContentEditable <ContentEditable
bind:text={text} bind:text={text}
@@ -78,4 +96,22 @@ function removeSample() {
/> />
</FontApplicator> </FontApplicator>
</div> </div>
<div class="px-6 py-2 border-t border-gray-200/40 w-full flex gap-4 bg-gray-50/30 mt-auto">
<span class="text-[8px] font-mono text-gray-400 uppercase tracking-wider ml-auto">
SZ:{fontSize}PX
</span>
<div class="w-px h-2.5 self-center bg-gray-300/40"></div>
<span class="text-[8px] font-mono text-gray-400 uppercase tracking-wider">
WGT:{fontWeight}
</span>
<div class="w-px h-2.5 self-center bg-gray-300/40"></div>
<span class="text-[8px] font-mono text-gray-400 uppercase tracking-wider">
LH:{lineHeight?.toFixed(2)}
</span>
<div class="w-px h-2.5 self-center bg-gray-300/40"></div>
<span class="text-[8px] font-mono text-gray-400 uppercase tracking-wider">
LTR:{letterSpacing}
</span>
</div>
</div> </div>

View File

@@ -1,21 +1,18 @@
<!--
Component: Page
Description: The main page component of the application.
-->
<script lang="ts"> <script lang="ts">
import { appliedFontsManager } from '$entities/Font'; import { appliedFontsManager } from '$entities/Font';
import { displayedFontsStore } from '$features/DisplayFont'; import { displayedFontsStore } from '$features/DisplayFont';
import FontDisplay from '$features/DisplayFont/ui/FontDisplay/FontDisplay.svelte'; import FontDisplay from '$features/DisplayFont/ui/FontDisplay/FontDisplay.svelte';
import { controlManager } from '$features/SetupFont'; import { controlManager } from '$features/SetupFont';
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Section } from '$shared/ui';
import ComparisonSlider from '$widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.svelte'; import ComparisonSlider from '$widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.svelte';
import { FontSearch } from '$widgets/FontSearch'; import { FontSearch } from '$widgets/FontSearch';
import { cubicOut } from 'svelte/easing'; import LineSquiggleIcon from '@lucide/svelte/icons/line-squiggle';
import { Spring } from 'svelte/motion'; import ScanEyeIcon from '@lucide/svelte/icons/scan-eye';
import type {
SlideParams,
TransitionConfig,
} from 'svelte/transition';
/**
* Page Component
*/
let searchContainer: HTMLElement; let searchContainer: HTMLElement;
@@ -53,13 +50,33 @@ $effect(() => {
</div> </div>
{/key} {/key}
<div class="my-6"> {#if displayedFontsStore.fonts.length > 1}
<Section class="my-12 gap-8" index={1}>
{#snippet icon({ className })}
<ScanEyeIcon class={className} />
{/snippet}
{#snippet title({ className })}
<h1 class={className}>
Optical<br>Comparator
</h1>
{/snippet}
<ComparisonSlider /> <ComparisonSlider />
</div> </Section>
{/if}
<div class="will-change-tranform transition-transform content my-2"> {#if displayedFontsStore.hasAnyFonts}
<Section class="my-12 gap-8" index={2}>
{#snippet icon({ className })}
<LineSquiggleIcon class={className} />
{/snippet}
{#snippet title({ className })}
<h2 class={className}>
Sample<br>Set
</h2>
{/snippet}
<FontDisplay /> <FontDisplay />
</div> </Section>
{/if}
</div> </div>
<style> <style>

View File

@@ -5,14 +5,20 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
/** /**
* Visible text * Visible text (bindable)
*/ */
text: string; text: string;
/** /**
* Font settings * Font size in pixels
*/ */
fontSize?: number; fontSize?: number;
/**
* Line height
*/
lineHeight?: number; lineHeight?: number;
/**
* Letter spacing in pixels
*/
letterSpacing?: number; letterSpacing?: number;
} }
@@ -53,7 +59,7 @@ function handleInput(e: Event) {
w-full min-h-[1.2em] outline-none transition-all duration-200 w-full min-h-[1.2em] outline-none transition-all duration-200
empty:before:content-[attr(data-placeholder)] empty:before:text-slate-400 empty:before:content-[attr(data-placeholder)] empty:before:text-slate-400
selection:bg-indigo-100 selection:text-indigo-900 selection:bg-indigo-100 selection:text-indigo-900
caret-indigo-500 caret-indigo-500 focus:outline-none
" "
style:font-size="{fontSize}px" style:font-size="{fontSize}px"
style:line-height={lineHeight} style:line-height={lineHeight}

View File

@@ -43,7 +43,7 @@ let { rotation = 'clockwise', icon, ...rest }: Props = $props();
> >
{@render icon({ {@render icon({
className: cn( className: cn(
'size-4 transition-all duration-200 stroke-slate-600/50 group-hover:stroke-indigo-500 group-hover:scale-110 group-hover:stroke-3 group-active:scale-90 group-disabled:stroke-transparent', 'size-4 transition-all duration-200 stroke-[1.5] stroke-gray-500 group-hover:stroke-gray-900 group-hover:scale-110 group-hover:stroke-3 group-active:scale-90 group-disabled:stroke-transparent',
rotation === 'clockwise' ? 'group-active:rotate-6' : 'group-active:-rotate-6', rotation === 'clockwise' ? 'group-active:rotate-6' : 'group-active:-rotate-6',
), ),
})} })}

View File

@@ -0,0 +1,70 @@
<!--
Component: Section
Provides a container for a page widget with snippets for a title
-->
<script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import type { Snippet } from 'svelte';
import { cubicOut } from 'svelte/easing';
import type { HTMLAttributes } from 'svelte/elements';
import {
type FlyParams,
fly,
} from 'svelte/transition';
interface Props extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
/**
* Additional CSS classes to apply to the section container.
*/
class?: string;
/**
* Snippet for a title itself
*/
title?: Snippet<[{ className?: string }]>;
/**
* Snippet for a title icon
*/
icon?: Snippet<[{ className?: string }]>;
/**
* Index of the section
*/
index?: number;
/**
* Snippet for the section content
*/
children?: Snippet;
}
const { class: className, title, icon, index, children }: Props = $props();
const flyParams: FlyParams = { y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 };
</script>
<section
class={cn(
'flex flex-col',
className,
)}
in:fly={flyParams}
out:fly={flyParams}
>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3 opacity-60">
{#if icon}
{@render icon({ className: 'size-4 stroke-gray-900 stroke-1' })}
<div class="w-px h-3 bg-gray-400/50"></div>
{/if}
{#if typeof index === 'number'}
<span class="font-mono text-[10px] uppercase tracking-[0.2em] text-gray-600">
Component_{String(index).padStart(3, '0')}
</span>
{/if}
</div>
{#if title}
{@render title({ className: 'text-5xl md:text-6xl font-semibold tracking-tighter text-gray-900 leading-[0.9]' })}
{/if}
</div>
{@render children?.()}
</section>

View File

@@ -11,6 +11,7 @@ import ContentEditable from './ContentEditable/ContentEditable.svelte';
import ExpandableWrapper from './ExpandableWrapper/ExpandableWrapper.svelte'; import ExpandableWrapper from './ExpandableWrapper/ExpandableWrapper.svelte';
import IconButton from './IconButton/IconButton.svelte'; import IconButton from './IconButton/IconButton.svelte';
import SearchBar from './SearchBar/SearchBar.svelte'; import SearchBar from './SearchBar/SearchBar.svelte';
import Section from './Section/Section.svelte';
import VirtualList from './VirtualList/VirtualList.svelte'; import VirtualList from './VirtualList/VirtualList.svelte';
export { export {
@@ -21,5 +22,6 @@ export {
ExpandableWrapper, ExpandableWrapper,
IconButton, IconButton,
SearchBar, SearchBar,
Section,
VirtualList, VirtualList,
}; };

View File

@@ -16,9 +16,7 @@ import {
createTypographyControl, createTypographyControl,
} from '$shared/lib'; } from '$shared/lib';
import type { LineData } from '$shared/lib'; import type { LineData } from '$shared/lib';
import { cubicOut } from 'svelte/easing';
import { Spring } from 'svelte/motion'; import { Spring } from 'svelte/motion';
import { fly } from 'svelte/transition';
import CharacterSlot from './components/CharacterSlot.svelte'; import CharacterSlot from './components/CharacterSlot.svelte';
import ControlsWrapper from './components/ControlsWrapper.svelte'; import ControlsWrapper from './components/ControlsWrapper.svelte';
import Labels from './components/Labels.svelte'; import Labels from './components/Labels.svelte';
@@ -180,7 +178,7 @@ $effect(() => {
aria-label="Font comparison slider" aria-label="Font comparison slider"
onpointerdown={startDragging} onpointerdown={startDragging}
class=" class="
group relative w-full py-16 px-0 sm:py-24 sm:px-0 overflow-hidden group relative w-full py-16 px-24 sm:py-24 sm:px-24 overflow-hidden
rounded-[2.5rem] rounded-[2.5rem]
select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center
backdrop-blur-lg bg-gradient-to-br from-gray-100/70 via-white/50 to-gray-100/60 backdrop-blur-lg bg-gradient-to-br from-gray-100/70 via-white/50 to-gray-100/60
@@ -190,7 +188,6 @@ $effect(() => {
before:bg-gradient-to-br before:from-black/5 before:via-black/2 before:to-transparent before:bg-gradient-to-br before:from-black/5 before:via-black/2 before:to-transparent
before:-z-10 before:blur-sm before:-z-10 before:blur-sm
" "
in:fly={{ y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 }}
> >
<!-- Text Rendering Container --> <!-- Text Rendering Container -->
<div <div