Compare commits
16 Commits
780d76dced
...
359617212d
| Author | SHA1 | Date | |
|---|---|---|---|
| 359617212d | |||
| beff194e5b | |||
| f24c93c105 | |||
| c16ef4acbf | |||
| c91ced3617 | |||
| a48c9bce0c | |||
| 152be85e34 | |||
| b09b89f4fc | |||
| 1a23ec2f28 | |||
| 86ea9cd887 | |||
| 10919a9881 | |||
| 180abd150d | |||
| c4bfb1db56 | |||
| 98a94e91ed | |||
| a1b7f78fc4 | |||
| 41c5ceb848 |
@@ -61,6 +61,7 @@
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vaul-svelte": "^1.0.0-next.7",
|
||||
"vite": "^7.2.6",
|
||||
"vitest": "^4.0.16",
|
||||
"vitest-browser-svelte": "^2.0.1"
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
* - Footer area (currently empty, reserved for future use)
|
||||
*/
|
||||
import { BreadcrumbHeader } from '$entities/Breadcrumb';
|
||||
import { TypographyMenu } from '$features/SetupFont';
|
||||
import favicon from '$shared/assets/favicon.svg';
|
||||
import { ResponsiveProvider } from '$shared/lib';
|
||||
import { ScrollArea } from '$shared/shadcn/ui/scroll-area';
|
||||
import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip';
|
||||
import { TypographyMenu } from '$widgets/TypographySettings';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import SetupFontMenu from './ui/SetupFontMenu.svelte';
|
||||
export {
|
||||
SetupFontMenu,
|
||||
TypographyMenu,
|
||||
} from './ui';
|
||||
|
||||
export {
|
||||
type ControlId,
|
||||
controlManager,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_FONT_WEIGHT,
|
||||
DEFAULT_LETTER_SPACING,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
||||
FONT_SIZE_STEP,
|
||||
FONT_WEIGHT_STEP,
|
||||
LINE_HEIGHT_STEP,
|
||||
@@ -16,4 +21,8 @@ export {
|
||||
MIN_FONT_WEIGHT,
|
||||
MIN_LINE_HEIGHT,
|
||||
} from './model';
|
||||
export { SetupFontMenu };
|
||||
|
||||
export {
|
||||
createTypographyControlManager,
|
||||
type TypographyControlManager,
|
||||
} from './lib';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ControlId } from '$features/SetupFont/model/state/manager.svelte';
|
||||
import {
|
||||
type ControlDataModel,
|
||||
type ControlModel,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
} from '$shared/lib';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import {
|
||||
type ControlId,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_FONT_WEIGHT,
|
||||
DEFAULT_LETTER_SPACING,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '../../model';
|
||||
|
||||
type ControlOnlyFields<T extends string = string> = Omit<ControlModel<T>, keyof ControlDataModel>;
|
||||
|
||||
export interface Control extends ControlOnlyFields<ControlId> {
|
||||
instance: TypographyControl;
|
||||
}
|
||||
@@ -29,11 +30,11 @@ export class TypographyControlManager {
|
||||
constructor(configs: ControlModel<ControlId>[], storage: PersistentStore<TypographySettings>) {
|
||||
this.#storage = storage;
|
||||
|
||||
// 1. Initial Load
|
||||
// Initial Load
|
||||
const saved = storage.value;
|
||||
this.#baseSize = saved.fontSize;
|
||||
|
||||
// 2. Setup Controls
|
||||
// Setup Controls
|
||||
configs.forEach(config => {
|
||||
const initialValue = this.#getInitialValue(config.id, saved);
|
||||
|
||||
@@ -46,7 +47,7 @@ export class TypographyControlManager {
|
||||
});
|
||||
});
|
||||
|
||||
// 3. The Sync Effect (UI -> Storage)
|
||||
// The Sync Effect (UI -> Storage)
|
||||
// We access .value explicitly to ensure Svelte 5 tracks the dependency
|
||||
$effect.root(() => {
|
||||
$effect(() => {
|
||||
@@ -65,7 +66,7 @@ export class TypographyControlManager {
|
||||
};
|
||||
});
|
||||
|
||||
// 4. The Font Size Proxy Effect
|
||||
// The Font Size Proxy Effect
|
||||
// This handles the "Multiplier" logic specifically for the Font Size Control
|
||||
$effect(() => {
|
||||
const ctrl = this.#controls.get('font_size')?.instance;
|
||||
@@ -123,10 +124,32 @@ export class TypographyControlManager {
|
||||
if (ctrl) ctrl.value = val * this.#multiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getters for controls
|
||||
*/
|
||||
get controls() {
|
||||
return Array.from(this.#controls.values());
|
||||
}
|
||||
|
||||
get weightControl() {
|
||||
return this.#controls.get('font_weight')?.instance;
|
||||
}
|
||||
|
||||
get sizeControl() {
|
||||
return this.#controls.get('font_size')?.instance;
|
||||
}
|
||||
|
||||
get heightControl() {
|
||||
return this.#controls.get('line_height')?.instance;
|
||||
}
|
||||
|
||||
get spacingControl() {
|
||||
return this.#controls.get('letter_spacing')?.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getters for values (besides font-size)
|
||||
*/
|
||||
get weight() {
|
||||
return this.#controls.get('font_weight')?.instance.value ?? DEFAULT_FONT_WEIGHT;
|
||||
}
|
||||
@@ -175,10 +198,14 @@ export interface TypographySettings {
|
||||
* Creates a typography control manager that handles a collection of typography controls.
|
||||
*
|
||||
* @param configs - Array of control configurations.
|
||||
* @param storageId - Persistent storage identifier.
|
||||
* @returns - Typography control manager instance.
|
||||
*/
|
||||
export function createTypographyControlManager(configs: ControlModel<ControlId>[]) {
|
||||
const storage = createPersistentStore<TypographySettings>('glyphdiff:typography', {
|
||||
export function createTypographyControlManager(
|
||||
configs: ControlModel<ControlId>[],
|
||||
storageId: string = 'glyphdiff:typography',
|
||||
) {
|
||||
const storage = createPersistentStore<TypographySettings>(storageId, {
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
fontWeight: DEFAULT_FONT_WEIGHT,
|
||||
lineHeight: DEFAULT_LINE_HEIGHT,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { ControlModel } from '$shared/lib';
|
||||
import type { ControlId } from '..';
|
||||
|
||||
/**
|
||||
* Font size constants
|
||||
*/
|
||||
@@ -29,3 +32,50 @@ export const DEFAULT_LETTER_SPACING = 0;
|
||||
export const MIN_LETTER_SPACING = -0.1;
|
||||
export const MAX_LETTER_SPACING = 0.5;
|
||||
export const LETTER_SPACING_STEP = 0.01;
|
||||
|
||||
export const DEFAULT_TYPOGRAPHY_CONTROLS_DATA: ControlModel<ControlId>[] = [
|
||||
{
|
||||
id: 'font_size',
|
||||
value: DEFAULT_FONT_SIZE,
|
||||
max: MAX_FONT_SIZE,
|
||||
min: MIN_FONT_SIZE,
|
||||
step: FONT_SIZE_STEP,
|
||||
|
||||
increaseLabel: 'Increase Font Size',
|
||||
decreaseLabel: 'Decrease Font Size',
|
||||
controlLabel: 'Font Size',
|
||||
},
|
||||
{
|
||||
id: 'font_weight',
|
||||
value: DEFAULT_FONT_WEIGHT,
|
||||
max: MAX_FONT_WEIGHT,
|
||||
min: MIN_FONT_WEIGHT,
|
||||
step: FONT_WEIGHT_STEP,
|
||||
|
||||
increaseLabel: 'Increase Font Weight',
|
||||
decreaseLabel: 'Decrease Font Weight',
|
||||
controlLabel: 'Font Weight',
|
||||
},
|
||||
{
|
||||
id: 'line_height',
|
||||
value: DEFAULT_LINE_HEIGHT,
|
||||
max: MAX_LINE_HEIGHT,
|
||||
min: MIN_LINE_HEIGHT,
|
||||
step: LINE_HEIGHT_STEP,
|
||||
|
||||
increaseLabel: 'Increase Line Height',
|
||||
decreaseLabel: 'Decrease Line Height',
|
||||
controlLabel: 'Line Height',
|
||||
},
|
||||
{
|
||||
id: 'letter_spacing',
|
||||
value: DEFAULT_LETTER_SPACING,
|
||||
max: MAX_LETTER_SPACING,
|
||||
min: MIN_LETTER_SPACING,
|
||||
step: LETTER_SPACING_STEP,
|
||||
|
||||
increaseLabel: 'Increase Letter Spacing',
|
||||
decreaseLabel: 'Decrease Letter Spacing',
|
||||
controlLabel: 'Letter Spacing',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,6 +3,7 @@ export {
|
||||
DEFAULT_FONT_WEIGHT,
|
||||
DEFAULT_LETTER_SPACING,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
||||
FONT_SIZE_STEP,
|
||||
FONT_WEIGHT_STEP,
|
||||
LINE_HEIGHT_STEP,
|
||||
@@ -14,5 +15,7 @@ export {
|
||||
MIN_LINE_HEIGHT,
|
||||
} from './const/const';
|
||||
|
||||
export { type TypographyControlManager } from '../lib';
|
||||
export { controlManager } from './state/manager.svelte';
|
||||
export {
|
||||
type ControlId,
|
||||
controlManager,
|
||||
} from './state/manager.svelte';
|
||||
|
||||
@@ -1,71 +1,6 @@
|
||||
import type { ControlModel } from '$shared/lib';
|
||||
import { createTypographyControlManager } from '../../lib';
|
||||
import {
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_FONT_WEIGHT,
|
||||
DEFAULT_LETTER_SPACING,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
FONT_SIZE_STEP,
|
||||
FONT_WEIGHT_STEP,
|
||||
LETTER_SPACING_STEP,
|
||||
LINE_HEIGHT_STEP,
|
||||
MAX_FONT_SIZE,
|
||||
MAX_FONT_WEIGHT,
|
||||
MAX_LETTER_SPACING,
|
||||
MAX_LINE_HEIGHT,
|
||||
MIN_FONT_SIZE,
|
||||
MIN_FONT_WEIGHT,
|
||||
MIN_LETTER_SPACING,
|
||||
MIN_LINE_HEIGHT,
|
||||
} from '../const/const';
|
||||
import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA } from '../const/const';
|
||||
|
||||
export type ControlId = 'font_size' | 'font_weight' | 'line_height' | 'letter_spacing';
|
||||
|
||||
const controlData: ControlModel<ControlId>[] = [
|
||||
{
|
||||
id: 'font_size',
|
||||
value: DEFAULT_FONT_SIZE,
|
||||
max: MAX_FONT_SIZE,
|
||||
min: MIN_FONT_SIZE,
|
||||
step: FONT_SIZE_STEP,
|
||||
|
||||
increaseLabel: 'Increase Font Size',
|
||||
decreaseLabel: 'Decrease Font Size',
|
||||
controlLabel: 'Font Size',
|
||||
},
|
||||
{
|
||||
id: 'font_weight',
|
||||
value: DEFAULT_FONT_WEIGHT,
|
||||
max: MAX_FONT_WEIGHT,
|
||||
min: MIN_FONT_WEIGHT,
|
||||
step: FONT_WEIGHT_STEP,
|
||||
|
||||
increaseLabel: 'Increase Font Weight',
|
||||
decreaseLabel: 'Decrease Font Weight',
|
||||
controlLabel: 'Font Weight',
|
||||
},
|
||||
{
|
||||
id: 'line_height',
|
||||
value: DEFAULT_LINE_HEIGHT,
|
||||
max: MAX_LINE_HEIGHT,
|
||||
min: MIN_LINE_HEIGHT,
|
||||
step: LINE_HEIGHT_STEP,
|
||||
|
||||
increaseLabel: 'Increase Line Height',
|
||||
decreaseLabel: 'Decrease Line Height',
|
||||
controlLabel: 'Line Height',
|
||||
},
|
||||
{
|
||||
id: 'letter_spacing',
|
||||
value: DEFAULT_LETTER_SPACING,
|
||||
max: MAX_LETTER_SPACING,
|
||||
min: MIN_LETTER_SPACING,
|
||||
step: LETTER_SPACING_STEP,
|
||||
|
||||
increaseLabel: 'Increase Letter Spacing',
|
||||
decreaseLabel: 'Decrease Letter Spacing',
|
||||
controlLabel: 'Letter Spacing',
|
||||
},
|
||||
];
|
||||
|
||||
export const controlManager = createTypographyControlManager(controlData);
|
||||
export const controlManager = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as SetupFontMenu } from './SetupFontMenu.svelte';
|
||||
export { default as TypographyMenu } from './TypographyMenu.svelte';
|
||||
+12
-13
@@ -57,19 +57,18 @@ function handleTitleStatusChanged(index: number, isPast: boolean, title?: Snippe
|
||||
{/snippet}
|
||||
<Logo />
|
||||
</Section>
|
||||
<!--
|
||||
<Section class="my-12 gap-8" index={1} onTitleStatusChange={handleTitleStatusChanged}>
|
||||
{#snippet icon({ className })}
|
||||
<EyeIcon class={className} />
|
||||
{/snippet}
|
||||
{#snippet title({ className })}
|
||||
<h1 class={className}>
|
||||
Optical<br />Comparator
|
||||
</h1>
|
||||
{/snippet}
|
||||
<ComparisonSlider />
|
||||
</Section>
|
||||
-->
|
||||
|
||||
<Section class="my-12 gap-8" index={1} onTitleStatusChange={handleTitleStatusChanged}>
|
||||
{#snippet icon({ className })}
|
||||
<EyeIcon class={className} />
|
||||
{/snippet}
|
||||
{#snippet title({ className })}
|
||||
<h1 class={className}>
|
||||
Optical<br />Comparator
|
||||
</h1>
|
||||
{/snippet}
|
||||
<ComparisonSlider />
|
||||
</Section>
|
||||
|
||||
<Section class="my-4 sm:my-10 md:my-12 gap-6 sm:gap-8" index={2} onTitleStatusChange={handleTitleStatusChanged}>
|
||||
{#snippet icon({ className })}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Close bind:ref data-slot="drawer-close" {...restProps} />
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { WithoutChildrenOrChild } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
import DrawerOverlay from './drawer-overlay.svelte';
|
||||
import DrawerPortal from './drawer-portal.svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: DrawerPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DrawerPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPortal {...portalProps}>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="drawer-content"
|
||||
class={cn(
|
||||
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
|
||||
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
|
||||
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
|
||||
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:end-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-s data-[vaul-drawer-direction=right]:sm:max-w-sm',
|
||||
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:start-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-e data-[vaul-drawer-direction=left]:sm:max-w-sm',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<div class="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block">
|
||||
</div>
|
||||
{@render children?.()}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="drawer-description"
|
||||
class={cn('text-muted-foreground text-sm', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="drawer-footer"
|
||||
class={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type WithElementRef,
|
||||
cn,
|
||||
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="drawer-header"
|
||||
class={cn('flex flex-col gap-1.5 p-4', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let {
|
||||
shouldScaleBackground = true,
|
||||
open = $bindable(false),
|
||||
activeSnapPoint = $bindable(null),
|
||||
...restProps
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="drawer-overlay"
|
||||
class={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let { ...restProps }: DrawerPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DrawerPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="drawer-title"
|
||||
class={cn('text-foreground font-semibold', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Trigger bind:ref data-slot="drawer-trigger" {...restProps} />
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let {
|
||||
shouldScaleBackground = true,
|
||||
open = $bindable(false),
|
||||
activeSnapPoint = $bindable(null),
|
||||
...restProps
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
@@ -0,0 +1,37 @@
|
||||
import Close from './drawer-close.svelte';
|
||||
import Content from './drawer-content.svelte';
|
||||
import Description from './drawer-description.svelte';
|
||||
import Footer from './drawer-footer.svelte';
|
||||
import Header from './drawer-header.svelte';
|
||||
import NestedRoot from './drawer-nested.svelte';
|
||||
import Overlay from './drawer-overlay.svelte';
|
||||
import Portal from './drawer-portal.svelte';
|
||||
import Title from './drawer-title.svelte';
|
||||
import Trigger from './drawer-trigger.svelte';
|
||||
import Root from './drawer.svelte';
|
||||
|
||||
export {
|
||||
Close,
|
||||
Close as DrawerClose,
|
||||
Content,
|
||||
Content as DrawerContent,
|
||||
Description,
|
||||
Description as DrawerDescription,
|
||||
Footer,
|
||||
Footer as DrawerFooter,
|
||||
Header,
|
||||
Header as DrawerHeader,
|
||||
NestedRoot,
|
||||
NestedRoot as DrawerNestedRoot,
|
||||
Overlay,
|
||||
Overlay as DrawerOverlay,
|
||||
Portal,
|
||||
Portal as DrawerPortal,
|
||||
Root,
|
||||
//
|
||||
Root as Drawer,
|
||||
Title,
|
||||
Title as DrawerTitle,
|
||||
Trigger,
|
||||
Trigger as DrawerTrigger,
|
||||
};
|
||||
@@ -7,23 +7,29 @@ import type { TypographyControl } from '$shared/lib';
|
||||
import { Input } from '$shared/shadcn/ui/input';
|
||||
import { Slider } from '$shared/shadcn/ui/slider';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Orientation } from 'bits-ui';
|
||||
import type { ChangeEventHandler } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Typography control instance
|
||||
*/
|
||||
control: TypographyControl;
|
||||
ref?: Snippet;
|
||||
/**
|
||||
* Orientation of the component
|
||||
*/
|
||||
orientation?: Orientation;
|
||||
}
|
||||
|
||||
let {
|
||||
control,
|
||||
ref = $bindable(),
|
||||
orientation = 'vertical',
|
||||
}: Props = $props();
|
||||
|
||||
let sliderValue = $state(Number(control.value));
|
||||
|
||||
$effect(() => {
|
||||
sliderValue = Number(control.value);
|
||||
sliderValue = Number(control?.value);
|
||||
});
|
||||
|
||||
const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
||||
@@ -36,22 +42,9 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
||||
const handleSliderChange = (newValue: number) => {
|
||||
control.value = newValue;
|
||||
};
|
||||
|
||||
// Shared glass button class for consistency
|
||||
// const glassBtnClass = cn(
|
||||
// 'border-none transition-all duration-200',
|
||||
// 'bg-white/10 hover:bg-white/40 active:scale-90',
|
||||
// 'text-slate-900 font-medium',
|
||||
// );
|
||||
|
||||
// const ghostStyle = cn(
|
||||
// 'flex items-center justify-center transition-all duration-300 ease-out',
|
||||
// 'text-slate-900/40 hover:text-slate-950 hover:bg-white/20 active:scale-90',
|
||||
// 'disabled:opacity-10 disabled:pointer-events-none',
|
||||
// );
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class={cn('flex flex-col items-center gap-4 w-full', orientation === 'vertical' ? 'flex-col' : 'flex-row')}>
|
||||
<Input
|
||||
value={control.value}
|
||||
onchange={handleInputChange}
|
||||
@@ -66,7 +59,7 @@ const handleSliderChange = (newValue: number) => {
|
||||
value={sliderValue}
|
||||
onValueChange={handleSliderChange}
|
||||
type="single"
|
||||
orientation="vertical"
|
||||
class="h-30"
|
||||
orientation={orientation}
|
||||
class={cn(orientation === 'vertical' ? 'h-30' : 'w-full')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<!-- Component: Drawer -->
|
||||
<script lang="ts">
|
||||
import { Button } from '$shared/shadcn/ui/button';
|
||||
import {
|
||||
Content as DrawerContent,
|
||||
Footer as DrawerFooter,
|
||||
Header as DrawerHeader,
|
||||
Root as DrawerRoot,
|
||||
Trigger as DrawerTrigger,
|
||||
} from '$shared/shadcn/ui/drawer';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
isOpen?: boolean;
|
||||
trigger?: Snippet<[{ isOpen: boolean; onClick: () => void }]>;
|
||||
content?: Snippet<[{ isOpen: boolean }]>;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
let { isOpen = $bindable(false), trigger, content, contentClassName }: Props = $props();
|
||||
|
||||
function handleClick() {
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DrawerRoot bind:open={isOpen}>
|
||||
<DrawerTrigger>
|
||||
{#if trigger}
|
||||
{@render trigger({ isOpen, onClick: handleClick })}
|
||||
{:else}
|
||||
<Button onclick={handleClick}>
|
||||
Open
|
||||
</Button>
|
||||
{/if}
|
||||
</DrawerTrigger>
|
||||
<DrawerContent class={cn('min-h-60', contentClassName)}>
|
||||
{@render content?.({ isOpen })}
|
||||
</DrawerContent>
|
||||
</DrawerRoot>
|
||||
@@ -173,7 +173,7 @@ $effect(() => {
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
'relative p-2 rounded-2xl border transition-all duration-250 ease-out flex flex-col gap-1.5 backdrop-blur-lg',
|
||||
'relative p-0.5 sm:p-2 rounded-lg sm:rounded-2xl border transition-all duration-250 ease-out flex flex-col gap-1.5 backdrop-blur-lg',
|
||||
expanded
|
||||
? 'bg-white/5 border-indigo-400/40 shadow-[0_30px_70px_-10px_rgba(99,102,241,0.25)]'
|
||||
: ' bg-white/25 border-white/40 shadow-[0_12px_40px_-12px_rgba(0,0,0,0.12)]',
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Input from './Input.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Shared/Input',
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Styles Input component',
|
||||
},
|
||||
story: { inline: false }, // Render stories in iframe for state isolation
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
placeholder: {
|
||||
control: 'text',
|
||||
description: "input's placeholder",
|
||||
},
|
||||
value: {
|
||||
control: 'text',
|
||||
description: "input's value",
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let value = $state('Initial value');
|
||||
const placeholder = 'Enter text';
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Default"
|
||||
args={{
|
||||
placeholder,
|
||||
value,
|
||||
}}
|
||||
>
|
||||
<Input value={value} placeholder={placeholder} />
|
||||
</Story>
|
||||
@@ -0,0 +1,54 @@
|
||||
<!--
|
||||
Component: Input
|
||||
Provides styled input component with all the shadcn input props
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Input } from '$shared/shadcn/ui/input';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
type Props = ComponentProps<typeof Input> & {
|
||||
/**
|
||||
* Current search value (bindable)
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* Additional CSS classes for the container
|
||||
*/
|
||||
class?: string;
|
||||
};
|
||||
|
||||
let {
|
||||
value = $bindable(''),
|
||||
class: className,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<Input
|
||||
bind:value={value}
|
||||
class={cn(
|
||||
'h-12 sm:h-14 md:h-16 w-full text-sm sm:text-base',
|
||||
'backdrop-blur-md bg-white/80',
|
||||
'border border-gray-300/50',
|
||||
'shadow-[0_1px_3px_rgba(0,0,0,0.04)]',
|
||||
'focus-visible:border-gray-400/60',
|
||||
'focus-visible:outline-none',
|
||||
'focus-visible:ring-1',
|
||||
'focus-visible:ring-gray-400/30',
|
||||
'focus-visible:bg-white/90',
|
||||
'hover:bg-white/90',
|
||||
'hover:border-gray-400/60',
|
||||
'text-gray-900',
|
||||
'placeholder:text-gray-400',
|
||||
'placeholder:font-mono',
|
||||
'placeholder:text-xs sm:placeholder:text-sm',
|
||||
'placeholder:tracking-wide',
|
||||
'pl-11 sm:pl-14 pr-4 sm:pr-6',
|
||||
'rounded-xl',
|
||||
'transition-all duration-200',
|
||||
'font-medium',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
@@ -34,8 +34,6 @@ const { Story } = defineMeta({
|
||||
|
||||
<script lang="ts">
|
||||
let defaultSearchValue = $state('');
|
||||
let withLabelValue = $state('');
|
||||
let noChildrenValue = $state('');
|
||||
</script>
|
||||
|
||||
<Story
|
||||
@@ -45,26 +43,5 @@ let noChildrenValue = $state('');
|
||||
placeholder: 'Type here...',
|
||||
}}
|
||||
>
|
||||
<SearchBar bind:value={defaultSearchValue} placeholder="Type here..."> </SearchBar>
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="With Label"
|
||||
args={{
|
||||
value: withLabelValue,
|
||||
placeholder: 'Search products...',
|
||||
label: 'Search',
|
||||
}}
|
||||
>
|
||||
<SearchBar bind:value={withLabelValue} placeholder="Search products..." label="Search"> </SearchBar>
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="Minimal Content"
|
||||
args={{
|
||||
value: noChildrenValue,
|
||||
placeholder: 'Quick search...',
|
||||
}}
|
||||
>
|
||||
<SearchBar bind:value={noChildrenValue} placeholder="Quick search..."> </SearchBar>
|
||||
<SearchBar bind:value={defaultSearchValue} placeholder="Type here..." />
|
||||
</Story>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- Component: SearchBar -->
|
||||
<script lang="ts">
|
||||
import { Input } from '$shared/shadcn/ui/input';
|
||||
import { Input } from '$shared/ui';
|
||||
import AsteriskIcon from '@lucide/svelte/icons/asterisk';
|
||||
|
||||
interface Props {
|
||||
@@ -32,44 +32,11 @@ let {
|
||||
class: className,
|
||||
placeholder,
|
||||
}: Props = $props();
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative w-full">
|
||||
<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={id}
|
||||
placeholder={placeholder}
|
||||
bind:value={value}
|
||||
onkeydown={handleKeyDown}
|
||||
class="
|
||||
h-12 sm:h-14 md:h-16 w-full text-sm sm:text-base
|
||||
backdrop-blur-md bg-white/80
|
||||
border border-gray-300/50
|
||||
shadow-[0_1px_3px_rgba(0,0,0,0.04)]
|
||||
focus-visible:border-gray-400/60
|
||||
focus-visible:outline-none
|
||||
focus-visible:ring-1
|
||||
focus-visible:ring-gray-400/30
|
||||
focus-visible:bg-white/90
|
||||
hover:bg-white/90
|
||||
hover:border-gray-400/60
|
||||
text-gray-900
|
||||
placeholder:text-gray-400
|
||||
placeholder:font-mono
|
||||
placeholder:text-xs sm:placeholder:text-sm
|
||||
placeholder:tracking-wide
|
||||
pl-11 sm:pl-14 pr-4 sm:pr-6
|
||||
rounded-xl
|
||||
transition-all duration-200
|
||||
font-medium
|
||||
"
|
||||
/>
|
||||
<Input {id} class={className} bind:value={value} placeholder={placeholder} />
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,11 @@ export { default as ComboControl } from './ComboControl/ComboControl.svelte';
|
||||
// ComboControlV2 might vary, assuming pattern holds or I'll fix later if build fails
|
||||
export { default as ComboControlV2 } from './ComboControlV2/ComboControlV2.svelte';
|
||||
export { default as ContentEditable } from './ContentEditable/ContentEditable.svelte';
|
||||
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 { default as Input } from './Input/Input.svelte';
|
||||
export { default as Loader } from './Loader/Loader.svelte';
|
||||
export { default as Logo } from './Logo/Logo.svelte';
|
||||
export { default as SearchBar } from './SearchBar/SearchBar.svelte';
|
||||
|
||||
@@ -3,6 +3,10 @@ import {
|
||||
fetchFontsByIds,
|
||||
unifiedFontStore,
|
||||
} from '$entities/Font';
|
||||
import {
|
||||
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
|
||||
createTypographyControlManager,
|
||||
} from '$features/SetupFont';
|
||||
import { createPersistentStore } from '$shared/lib';
|
||||
|
||||
/**
|
||||
@@ -30,6 +34,7 @@ class ComparisonStore {
|
||||
#fontB = $state<UnifiedFont | undefined>();
|
||||
#sampleText = $state('The quick brown fox jumps over the lazy dog');
|
||||
#isRestoring = $state(true);
|
||||
#typography = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, 'glyphdiff:comparison:typography');
|
||||
|
||||
constructor() {
|
||||
this.restoreFromStorage();
|
||||
@@ -102,6 +107,9 @@ class ComparisonStore {
|
||||
}
|
||||
|
||||
// --- Getters & Setters ---
|
||||
get typography() {
|
||||
return this.#typography;
|
||||
}
|
||||
|
||||
get fontA() {
|
||||
return this.#fontA;
|
||||
@@ -149,6 +157,13 @@ class ComparisonStore {
|
||||
this.restoreFromStorage();
|
||||
}
|
||||
}
|
||||
|
||||
resetAll() {
|
||||
this.#fontA = undefined;
|
||||
this.#fontB = undefined;
|
||||
storage.clear();
|
||||
this.#typography.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export const comparisonStore = new ComparisonStore();
|
||||
|
||||
@@ -14,14 +14,17 @@ import {
|
||||
createCharacterComparison,
|
||||
createTypographyControl,
|
||||
} from '$shared/lib';
|
||||
import type { LineData } from '$shared/lib';
|
||||
import type {
|
||||
LineData,
|
||||
ResponsiveManager,
|
||||
} from '$shared/lib';
|
||||
import { Loader } from '$shared/ui';
|
||||
import { comparisonStore } from '$widgets/ComparisonSlider/model';
|
||||
import { getContext } from 'svelte';
|
||||
import { Spring } from 'svelte/motion';
|
||||
import { fade } from 'svelte/transition';
|
||||
import CharacterSlot from './components/CharacterSlot.svelte';
|
||||
import ControlsWrapper from './components/ControlsWrapper.svelte';
|
||||
import Labels from './components/Labels.svelte';
|
||||
import Controls from './components/Controls.svelte';
|
||||
import SliderLine from './components/SliderLine.svelte';
|
||||
|
||||
// Pair of fonts to compare
|
||||
@@ -30,31 +33,13 @@ const fontB = $derived(comparisonStore.fontB);
|
||||
|
||||
const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady);
|
||||
|
||||
let container: HTMLElement | undefined = $state();
|
||||
let controlsWrapperElement = $state<HTMLDivElement | null>(null);
|
||||
let measureCanvas: HTMLCanvasElement | undefined = $state();
|
||||
let container = $state<HTMLElement>();
|
||||
let typographyControls = $state<HTMLDivElement | null>(null);
|
||||
let measureCanvas = $state<HTMLCanvasElement>();
|
||||
let isDragging = $state(false);
|
||||
const typography = $derived(comparisonStore.typography);
|
||||
|
||||
const weightControl = createTypographyControl({
|
||||
min: 100,
|
||||
max: 700,
|
||||
step: 100,
|
||||
value: 400,
|
||||
});
|
||||
|
||||
const heightControl = createTypographyControl({
|
||||
min: 1,
|
||||
max: 2,
|
||||
step: 0.05,
|
||||
value: 1.2,
|
||||
});
|
||||
|
||||
const sizeControl = createTypographyControl({
|
||||
min: 1,
|
||||
max: 112,
|
||||
step: 1,
|
||||
value: 64,
|
||||
});
|
||||
const responsive = getContext<ResponsiveManager>('responsive');
|
||||
|
||||
/**
|
||||
* Encapsulated helper for text splitting, measuring, and character proximity calculations.
|
||||
@@ -64,8 +49,8 @@ const charComparison = createCharacterComparison(
|
||||
() => comparisonStore.text,
|
||||
() => fontA,
|
||||
() => fontB,
|
||||
() => weightControl.value,
|
||||
() => sizeControl.value,
|
||||
() => typography.weight,
|
||||
() => typography.renderedSize,
|
||||
);
|
||||
|
||||
let lineElements = $state<(HTMLElement | undefined)[]>([]);
|
||||
@@ -88,8 +73,8 @@ function handleMove(e: PointerEvent) {
|
||||
|
||||
function startDragging(e: PointerEvent) {
|
||||
if (
|
||||
e.target === controlsWrapperElement
|
||||
|| controlsWrapperElement?.contains(e.target as Node)
|
||||
e.target === typographyControls
|
||||
|| typographyControls?.contains(e.target as Node)
|
||||
) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
@@ -99,6 +84,30 @@ function startDragging(e: PointerEvent) {
|
||||
handleMove(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the multiplier for slider font size based on the current responsive state
|
||||
*/
|
||||
$effect(() => {
|
||||
if (!responsive) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case responsive.isMobile:
|
||||
typography.multiplier = 0.5;
|
||||
break;
|
||||
case responsive.isTablet:
|
||||
typography.multiplier = 0.75;
|
||||
break;
|
||||
case responsive.isDesktop:
|
||||
typography.multiplier = 1;
|
||||
break;
|
||||
default:
|
||||
typography.multiplier = 1;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (isDragging) {
|
||||
window.addEventListener('pointermove', handleMove);
|
||||
@@ -115,9 +124,9 @@ $effect(() => {
|
||||
$effect(() => {
|
||||
// React on text and typography settings changes
|
||||
const _text = comparisonStore.text;
|
||||
const _weight = weightControl.value;
|
||||
const _size = sizeControl.value;
|
||||
const _height = heightControl.value;
|
||||
const _weight = typography.weight;
|
||||
const _size = typography.renderedSize;
|
||||
const _height = typography.height;
|
||||
|
||||
if (container && measureCanvas && fontA && fontB) {
|
||||
// Using rAF to ensure DOM is ready/stabilized
|
||||
@@ -143,8 +152,8 @@ $effect(() => {
|
||||
<div
|
||||
bind:this={lineElements[index]}
|
||||
class="relative flex w-full justify-center items-center whitespace-nowrap"
|
||||
style:height={`${heightControl.value}em`}
|
||||
style:line-height={`${heightControl.value}em`}
|
||||
style:height={`${typography.height}em`}
|
||||
style:line-height={`${typography.height}em`}
|
||||
>
|
||||
{#each line.text.split('') as char, charIndex}
|
||||
{@const { proximity, isPast } = charComparison.getCharState(charIndex, sliderPos, lineElements[index], container)}
|
||||
@@ -158,8 +167,8 @@ $effect(() => {
|
||||
{char}
|
||||
{proximity}
|
||||
{isPast}
|
||||
weight={weightControl.value}
|
||||
size={sizeControl.value}
|
||||
weight={typography.weight}
|
||||
size={typography.renderedSize}
|
||||
fontAName={fontA.name}
|
||||
fontBName={fontB.name}
|
||||
/>
|
||||
@@ -182,12 +191,12 @@ $effect(() => {
|
||||
class="
|
||||
group relative w-full py-8 px-4 sm:py-12 sm:px-8 md:py-16 md:px-12 lg:py-20 lg:px-24 overflow-hidden
|
||||
rounded-xl sm:rounded-2xl md:rounded-[2.5rem]
|
||||
select-none touch-none cursor-ew-resize min-h-[300px] sm:min-h-[400px] md:min-h-[500px] flex flex-col justify-center
|
||||
backdrop-blur-lg bg-gradient-to-br from-gray-100/70 via-white/50 to-gray-100/60
|
||||
select-none touch-none cursor-ew-resize min-h-72 sm:min-h-96 flex flex-col justify-center
|
||||
backdrop-blur-lg bg-linear-to-br from-gray-100/70 via-white/50 to-gray-100/60
|
||||
border border-gray-300/40
|
||||
shadow-[inset_0_4px_12px_0_rgba(0,0,0,0.12),inset_0_2px_4px_0_rgba(0,0,0,0.08),0_1px_2px_0_rgba(255,255,255,0.8)]
|
||||
before:absolute before:inset-0 before:rounded-xl sm:before:rounded-2xl md:before:rounded-[2.5rem] before:p-[1px]
|
||||
before:bg-gradient-to-br before:from-black/5 before:via-black/2 before:to-transparent
|
||||
before:absolute before:inset-0 before:rounded-xl sm:before:rounded-2xl md:before:rounded-[2.5rem] before:p-px
|
||||
before:bg-linear-to-br before:from-black/5 before:via-black/2 before:to-transparent
|
||||
before:-z-10 before:blur-sm
|
||||
"
|
||||
>
|
||||
@@ -209,7 +218,7 @@ $effect(() => {
|
||||
{#each charComparison.lines as line, lineIndex}
|
||||
<div
|
||||
class="relative w-full whitespace-nowrap"
|
||||
style:height={`${heightControl.value}em`}
|
||||
style:height={`${typography.height}em`}
|
||||
style:display="flex"
|
||||
style:align-items="center"
|
||||
style:justify-content="center"
|
||||
@@ -222,19 +231,6 @@ $effect(() => {
|
||||
<SliderLine {sliderPos} {isDragging} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if fontA && fontB && !isLoading}
|
||||
<Labels {fontA} {fontB} {sliderPos} weight={weightControl.value} />
|
||||
<!-- Since there're slider controls inside we put them outside the main one -->
|
||||
<ControlsWrapper
|
||||
bind:wrapper={controlsWrapperElement}
|
||||
{sliderPos}
|
||||
{isDragging}
|
||||
bind:text={comparisonStore.text}
|
||||
containerWidth={container?.clientWidth}
|
||||
{weightControl}
|
||||
{sizeControl}
|
||||
{heightControl}
|
||||
/>
|
||||
{/if}
|
||||
<!-- Since there're slider controls inside we put them outside the main one -->
|
||||
<Controls {sliderPos} {isDragging} {typographyControls} {container} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import type { ResponsiveManager } from '$shared/lib';
|
||||
import {
|
||||
Drawer,
|
||||
IconButton,
|
||||
} from '$shared/ui';
|
||||
import SlidersIcon from '@lucide/svelte/icons/sliders-vertical';
|
||||
import { getContext } from 'svelte';
|
||||
import { comparisonStore } from '../../../model';
|
||||
import SelectComparedFonts from './SelectComparedFonts.svelte';
|
||||
import TypographyControls from './TypographyControls.svelte';
|
||||
|
||||
interface Props {
|
||||
sliderPos: number;
|
||||
isDragging: boolean;
|
||||
typographyControls?: HTMLDivElement | null;
|
||||
container: HTMLElement;
|
||||
}
|
||||
|
||||
let { sliderPos, isDragging, typographyControls = $bindable<HTMLDivElement | null>(null), container }: Props = $props();
|
||||
|
||||
const fontA = $derived(comparisonStore.fontA);
|
||||
const fontB = $derived(comparisonStore.fontB);
|
||||
const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady);
|
||||
|
||||
const responsive = getContext<ResponsiveManager>('responsive');
|
||||
</script>
|
||||
|
||||
{#if responsive.isMobile}
|
||||
<Drawer>
|
||||
{#snippet trigger({ isOpen, onClick })}
|
||||
<IconButton class="absolute right-3 top-3" onclick={onClick}>
|
||||
{#snippet icon({ className })}
|
||||
<SlidersIcon class={className} />
|
||||
{/snippet}
|
||||
</IconButton>
|
||||
{/snippet}
|
||||
|
||||
{#snippet content({ isOpen })}
|
||||
<div class="px-2 py-4">
|
||||
<SelectComparedFonts {sliderPos} />
|
||||
</div>
|
||||
<TypographyControls
|
||||
{sliderPos}
|
||||
{isDragging}
|
||||
isActive={isOpen}
|
||||
bind:wrapper={typographyControls}
|
||||
containerWidth={container?.clientWidth}
|
||||
staticPosition
|
||||
/>
|
||||
{/snippet}
|
||||
</Drawer>
|
||||
{:else}
|
||||
{#if !isLoading}
|
||||
<div class="absolute top-3 sm:top-6 left-3 sm:left-6">
|
||||
<TypographyControls
|
||||
{sliderPos}
|
||||
{isDragging}
|
||||
bind:wrapper={typographyControls}
|
||||
containerWidth={container?.clientWidth}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isLoading}
|
||||
<div class="absolute bottom-3 sm:bottom-6 md:bottom-8 inset-x-3 sm:inset-x-6 md:inset-x-12">
|
||||
<SelectComparedFonts {sliderPos} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1,177 +0,0 @@
|
||||
<!--
|
||||
Component: ControlsWrapper
|
||||
Wrapper for the controls of the slider.
|
||||
- Input to change the text
|
||||
- Three combo controls with inputs and sliders for font-weight, font-size, and line-height
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { TypographyControl } from '$shared/lib';
|
||||
import { Input } from '$shared/shadcn/ui/input';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import { ComboControlV2 } from '$shared/ui';
|
||||
import { ExpandableWrapper } from '$shared/ui';
|
||||
import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
|
||||
import { Spring } from 'svelte/motion';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Ref
|
||||
*/
|
||||
wrapper?: HTMLDivElement | null;
|
||||
/**
|
||||
* Slider position
|
||||
*/
|
||||
sliderPos: number;
|
||||
/**
|
||||
* Whether slider is being dragged
|
||||
*/
|
||||
isDragging: boolean;
|
||||
/**
|
||||
* Text to display
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
* Container width
|
||||
*/
|
||||
containerWidth: number;
|
||||
/**
|
||||
* Weight control
|
||||
*/
|
||||
weightControl: TypographyControl;
|
||||
/**
|
||||
* Size control
|
||||
*/
|
||||
sizeControl: TypographyControl;
|
||||
/**
|
||||
* Height control
|
||||
*/
|
||||
heightControl: TypographyControl;
|
||||
}
|
||||
|
||||
let {
|
||||
sliderPos,
|
||||
isDragging,
|
||||
wrapper = $bindable(null),
|
||||
text = $bindable(),
|
||||
containerWidth = 0,
|
||||
weightControl,
|
||||
sizeControl,
|
||||
heightControl,
|
||||
}: Props = $props();
|
||||
|
||||
let panelWidth = $derived(wrapper?.clientWidth ?? 0);
|
||||
const margin = 24;
|
||||
let side = $state<'left' | 'right'>('left');
|
||||
// Unified active state for the entire wrapper
|
||||
let isActive = $state(false);
|
||||
let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const xSpring = new Spring(0, {
|
||||
stiffness: 0.14, // Lower is slower
|
||||
damping: 0.5, // Settle
|
||||
});
|
||||
|
||||
const rotateSpring = new Spring(0, {
|
||||
stiffness: 0.12,
|
||||
damping: 0.55,
|
||||
});
|
||||
|
||||
function handleInputFocus() {
|
||||
isActive = true;
|
||||
}
|
||||
|
||||
// Movement Logic
|
||||
$effect(() => {
|
||||
if (containerWidth === 0 || panelWidth === 0) return;
|
||||
const sliderX = (sliderPos / 100) * containerWidth;
|
||||
const buffer = 40;
|
||||
const leftTrigger = margin + panelWidth + buffer;
|
||||
const rightTrigger = containerWidth - (margin + panelWidth + buffer);
|
||||
|
||||
if (side === 'left' && sliderX < leftTrigger) {
|
||||
side = 'right';
|
||||
} else if (side === 'right' && sliderX > rightTrigger) {
|
||||
side = 'left';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const targetX = side === 'right' ? containerWidth - panelWidth - margin * 2 : 0;
|
||||
if (containerWidth > 0 && panelWidth > 0) {
|
||||
// On side change set the position and the rotation
|
||||
xSpring.target = targetX;
|
||||
rotateSpring.target = side === 'right' ? 3.5 : -3.5;
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
rotateSpring.target = 0;
|
||||
}, 600);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="absolute top-6 left-6 z-50 will-change-transform"
|
||||
style:transform="
|
||||
translateX({xSpring.current}px)
|
||||
rotateZ({rotateSpring.current}deg)
|
||||
"
|
||||
in:fade={{ duration: 300, delay: 300 }}
|
||||
out:fade={{ duration: 300, delay: 300 }}
|
||||
>
|
||||
<ExpandableWrapper
|
||||
bind:element={wrapper}
|
||||
bind:expanded={isActive}
|
||||
disabled={isDragging}
|
||||
aria-label="Font controls"
|
||||
rotation={side === 'right' ? 'counterclockwise' : 'clockwise'}
|
||||
class={cn(
|
||||
'transition-opacity flex items-top gap-1.5',
|
||||
panelWidth === 0 ? 'opacity-0' : 'opacity-100',
|
||||
)}
|
||||
>
|
||||
{#snippet badge()}
|
||||
<div
|
||||
class={cn(
|
||||
'animate-nudge relative transition-all',
|
||||
side === 'left' ? 'order-2' : 'order-0',
|
||||
isActive ? 'opacity-0' : 'opacity-100',
|
||||
isDragging && 'opacity-80 grayscale-[0.2]',
|
||||
)}
|
||||
>
|
||||
<AArrowUP class={cn('size-3', 'stroke-indigo-600')} />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet visibleContent()}
|
||||
<div class="relative px-2 py-1">
|
||||
<Input
|
||||
bind:value={text}
|
||||
disabled={isDragging}
|
||||
onfocusin={handleInputFocus}
|
||||
class={cn(
|
||||
isActive
|
||||
? 'h-7 sm:h-8 text-[11px] sm:text-xs text-center bg-white/40 border-none rounded-lg focus-visible:ring-indigo-500/50 text-slate-900'
|
||||
: 'bg-transparent shadow-none border-none p-0 h-auto text-sm sm:text-base font-medium focus-visible:ring-0 text-slate-900/50',
|
||||
' placeholder:text-slate-400 selection:bg-indigo-100 flex-1 transition-all duration-350 w-44 sm:w-56',
|
||||
)}
|
||||
placeholder="The quick brown fox..."
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet hiddenContent()}
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center-safe gap-2 sm:gap-0">
|
||||
<ComboControlV2 control={weightControl} />
|
||||
<ComboControlV2 control={sizeControl} />
|
||||
<ComboControlV2 control={heightControl} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</ExpandableWrapper>
|
||||
</div>
|
||||
+36
-46
@@ -1,13 +1,14 @@
|
||||
<!--
|
||||
Component: Labels
|
||||
Displays labels for font selection in the comparison slider.
|
||||
Component: SelectComparedFonts
|
||||
Displays selects that change the compared fonts
|
||||
-->
|
||||
<script lang="ts" generics="T extends UnifiedFont">
|
||||
<script lang="ts">
|
||||
import {
|
||||
FontVirtualList,
|
||||
type UnifiedFont,
|
||||
unifiedFontStore,
|
||||
} from '$entities/Font';
|
||||
import { getFontUrl } from '$entities/Font/lib';
|
||||
import FontApplicator from '$entities/Font/ui/FontApplicator/FontApplicator.svelte';
|
||||
import {
|
||||
Content as SelectContent,
|
||||
@@ -19,23 +20,19 @@ import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import { comparisonStore } from '$widgets/ComparisonSlider/model';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props<T> {
|
||||
/**
|
||||
* First font to compare
|
||||
*/
|
||||
fontA: T;
|
||||
/**
|
||||
* Second font to compare
|
||||
*/
|
||||
fontB: T;
|
||||
interface Props {
|
||||
/**
|
||||
* Position of the slider
|
||||
*/
|
||||
sliderPos: number;
|
||||
|
||||
weight: number;
|
||||
}
|
||||
let { fontA, fontB, sliderPos, weight }: Props<T> = $props();
|
||||
let { sliderPos }: Props = $props();
|
||||
|
||||
const typography = $derived(comparisonStore.typography);
|
||||
const fontA = $derived(comparisonStore.fontA);
|
||||
const fontAUrl = $derived(fontA && getFontUrl(fontA, typography.weight));
|
||||
const fontB = $derived(comparisonStore.fontB);
|
||||
const fontBUrl = $derived(fontB && getFontUrl(fontB, typography.weight));
|
||||
|
||||
const fontList = $derived(unifiedFontStore.fonts);
|
||||
|
||||
@@ -51,11 +48,10 @@ function selectFontB(font: UnifiedFont) {
|
||||
</script>
|
||||
|
||||
{#snippet fontSelector(
|
||||
name: string,
|
||||
id: string,
|
||||
url: string,
|
||||
font: UnifiedFont,
|
||||
fonts: UnifiedFont[],
|
||||
selectFont: (font: UnifiedFont) => void,
|
||||
url: string,
|
||||
onSelect: (f: UnifiedFont) => void,
|
||||
align: 'start' | 'end',
|
||||
)}
|
||||
<div
|
||||
@@ -74,15 +70,15 @@ function selectFontB(font: UnifiedFont) {
|
||||
)}
|
||||
>
|
||||
<div class="text-left flex-1 min-w-0">
|
||||
<FontApplicator {name} {id} {url}>
|
||||
{name}
|
||||
<FontApplicator name={font.name} id={font.id} {url}>
|
||||
{font.name}
|
||||
</FontApplicator>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
class={cn(
|
||||
'bg-white/95 backdrop-blur-xl border border-gray-300/50 shadow-xl',
|
||||
'w-44 sm:w-52 max-h-[240px] sm:max-h-[280px] overflow-hidden rounded-lg',
|
||||
'w-44 sm:w-52 max-h-60 sm:max-h-64 overflow-hidden rounded-lg',
|
||||
)}
|
||||
side="top"
|
||||
{align}
|
||||
@@ -90,16 +86,20 @@ function selectFontB(font: UnifiedFont) {
|
||||
size="small"
|
||||
>
|
||||
<div class="p-1 sm:p-1.5">
|
||||
<FontVirtualList items={fonts} {weight}>
|
||||
{#snippet children({ item: font })}
|
||||
{@const handleClick = () => selectFont(font)}
|
||||
<FontVirtualList items={fonts} weight={typography.weight}>
|
||||
{#snippet children({ item: fontListItem })}
|
||||
{@const handleClick = () => onSelect(fontListItem)}
|
||||
<SelectItem
|
||||
value={font.id}
|
||||
class="data-[highlighted]:bg-gray-100 font-mono text-[10px] sm:text-[11px] px-2 sm:px-3 py-2 sm:py-2.5 rounded-md cursor-pointer transition-colors"
|
||||
value={fontListItem.id}
|
||||
class="data-highlighted:bg-gray-100 font-mono text-[10px] sm:text-[11px] px-2 sm:px-3 py-2 sm:py-2.5 rounded-md cursor-pointer transition-colors"
|
||||
onclick={handleClick}
|
||||
>
|
||||
<FontApplicator name={font.name} id={font.id} url={font.styles.regular!}>
|
||||
{font.name}
|
||||
<FontApplicator
|
||||
name={fontListItem.name}
|
||||
id={fontListItem.id}
|
||||
url={getFontUrl(fontListItem, typography.weight) ?? ''}
|
||||
>
|
||||
{fontListItem.name}
|
||||
</FontApplicator>
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
@@ -110,7 +110,7 @@ function selectFontB(font: UnifiedFont) {
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="absolute bottom-4 sm:bottom-6 md:bottom-8 inset-x-4 sm:inset-x-6 md:inset-x-12 flex justify-between items-end pointer-events-none z-20">
|
||||
<div class="flex justify-between items-end pointer-events-none z-20">
|
||||
<div
|
||||
class="flex flex-col gap-1.5 sm:gap-2 transition-all duration-500 items-start"
|
||||
style:opacity={sliderPos < 20 ? 0 : 1}
|
||||
@@ -123,14 +123,9 @@ function selectFontB(font: UnifiedFont) {
|
||||
ch_01
|
||||
</span>
|
||||
</div>
|
||||
{@render fontSelector(
|
||||
fontB.name,
|
||||
fontB.id,
|
||||
fontB.styles.regular!,
|
||||
fontList,
|
||||
selectFontB,
|
||||
'start',
|
||||
)}
|
||||
{#if fontB && fontBUrl}
|
||||
{@render fontSelector(fontB, fontList, fontBUrl, selectFontB, 'start')}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -145,13 +140,8 @@ function selectFontB(font: UnifiedFont) {
|
||||
<div class="w-px h-2 sm:h-2.5 bg-gray-300/60"></div>
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-gray-900 shadow-[0_0_6px_rgba(0,0,0,0.4)]"></div>
|
||||
</div>
|
||||
{@render fontSelector(
|
||||
fontA.name,
|
||||
fontA.id,
|
||||
fontA.styles.regular!,
|
||||
fontList,
|
||||
selectFontA,
|
||||
'end',
|
||||
)}
|
||||
{#if fontA && fontAUrl}
|
||||
{@render fontSelector(fontA, fontList, fontAUrl, selectFontA, 'end')}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,14 +18,14 @@ interface Props {
|
||||
let { sliderPos, isDragging }: Props = $props();
|
||||
</script>
|
||||
<div
|
||||
class="absolute inset-y-4 pointer-events-none -translate-x-1/2 z-50 transition-all duration-300 ease-out flex flex-col justify-center items-center"
|
||||
class="absolute inset-y-2 sm:inset-y-4 pointer-events-none -translate-x-1/2 z-50 transition-all duration-300 ease-out flex flex-col justify-center items-center"
|
||||
style:left="{sliderPos}%"
|
||||
>
|
||||
<!-- We use part of lucide cursor svg icon as a handle -->
|
||||
<svg
|
||||
class={cn(
|
||||
'transition-all relative duration-300 text-black/80 drop-shadow-sm',
|
||||
isDragging ? 'w-12 h-12' : 'w-8 h-8',
|
||||
isDragging ? 'size-6 sm:size-12' : 'size-4 sm:size-8',
|
||||
)}
|
||||
viewBox="0 0 24 12"
|
||||
fill="none"
|
||||
@@ -41,12 +41,12 @@ let { sliderPos, isDragging }: Props = $props();
|
||||
<div
|
||||
class={cn(
|
||||
'relative h-full rounded-sm transition-all duration-500',
|
||||
'bg-white/[0.03] backdrop-blur-md',
|
||||
'bg-white/3 backdrop-blur-md',
|
||||
// These are the visible "edges" of the glass
|
||||
'shadow-[0_0_40px_rgba(0,0,0,0.1)_inset_0_0_20px_rgba(255,255,255,0.1)]',
|
||||
'shadow-[0_10px_30px_-10px_rgba(0,0,0,0.2),inset_0_1px_1px_rgba(255,255,255,0.4)]',
|
||||
'rounded-full',
|
||||
isDragging ? 'w-32' : 'w-16',
|
||||
isDragging ? 'w-16 sm:w-32' : 'w-12 sm:w-16',
|
||||
)}
|
||||
>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@ let { sliderPos, isDragging }: Props = $props();
|
||||
<svg
|
||||
class={cn(
|
||||
'transition-all relative duration-500 text-black/80 drop-shadow-sm',
|
||||
isDragging ? 'w-12 h-12' : 'w-8 h-8',
|
||||
isDragging ? 'size-6 sm:size-12' : 'size-4 sm:size-8',
|
||||
)}
|
||||
viewBox="0 0 24 12"
|
||||
fill="none"
|
||||
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
<!--
|
||||
Component: TypographyControls
|
||||
Wrapper for the controls of the slider.
|
||||
- Input to change the text
|
||||
- Three combo controls with inputs and sliders for font-weight, font-size, and line-height
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import {
|
||||
ComboControlV2,
|
||||
ExpandableWrapper,
|
||||
Input,
|
||||
} from '$shared/ui';
|
||||
import { comparisonStore } from '$widgets/ComparisonSlider/model';
|
||||
import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
|
||||
import { Spring } from 'svelte/motion';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Ref
|
||||
*/
|
||||
wrapper?: HTMLDivElement | null;
|
||||
/**
|
||||
* Slider position
|
||||
*/
|
||||
sliderPos: number;
|
||||
/**
|
||||
* Whether slider is being dragged
|
||||
*/
|
||||
isDragging: boolean;
|
||||
/** */
|
||||
isActive?: boolean;
|
||||
/**
|
||||
* Container width
|
||||
*/
|
||||
containerWidth: number;
|
||||
/**
|
||||
* Reduced animations flag
|
||||
*/
|
||||
staticPosition?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
sliderPos,
|
||||
isDragging,
|
||||
isActive = $bindable(false),
|
||||
wrapper = $bindable(null),
|
||||
containerWidth = 0,
|
||||
staticPosition = false,
|
||||
}: Props = $props();
|
||||
|
||||
const typography = $derived(comparisonStore.typography);
|
||||
const panelWidth = $derived(wrapper?.clientWidth ?? 0);
|
||||
const margin = 24;
|
||||
let side = $state<'left' | 'right'>('left');
|
||||
// Unified active state for the entire wrapper
|
||||
let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const xSpring = new Spring(0, {
|
||||
stiffness: 0.14, // Lower is slower
|
||||
damping: 0.5, // Settle
|
||||
});
|
||||
|
||||
const rotateSpring = new Spring(0, {
|
||||
stiffness: 0.12,
|
||||
damping: 0.55,
|
||||
});
|
||||
|
||||
function handleInputFocus() {
|
||||
isActive = true;
|
||||
}
|
||||
|
||||
// Movement Logic
|
||||
$effect(() => {
|
||||
if (containerWidth === 0 || panelWidth === 0 || staticPosition) return;
|
||||
const sliderX = (sliderPos / 100) * containerWidth;
|
||||
const buffer = 40;
|
||||
const leftTrigger = margin + panelWidth + buffer;
|
||||
const rightTrigger = containerWidth - (margin + panelWidth + buffer);
|
||||
|
||||
if (side === 'left' && sliderX < leftTrigger) {
|
||||
side = 'right';
|
||||
} else if (side === 'right' && sliderX > rightTrigger) {
|
||||
side = 'left';
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const targetX = side === 'right' ? containerWidth - panelWidth - margin * 2 : 0;
|
||||
if (containerWidth > 0 && panelWidth > 0) {
|
||||
// On side change set the position and the rotation
|
||||
xSpring.target = targetX;
|
||||
rotateSpring.target = side === 'right' ? 3.5 : -3.5;
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
rotateSpring.target = 0;
|
||||
}, 600);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="z-50 will-change-transform"
|
||||
style:transform="
|
||||
translateX({xSpring.current}px)
|
||||
rotateZ({rotateSpring.current}deg)
|
||||
"
|
||||
in:fade={{ duration: 300, delay: 300 }}
|
||||
out:fade={{ duration: 300, delay: 300 }}
|
||||
>
|
||||
{#if staticPosition}
|
||||
<div class="flex flex-col gap-6 px-2 py-4">
|
||||
<Input
|
||||
class="p-6"
|
||||
bind:value={comparisonStore.text}
|
||||
disabled={isDragging}
|
||||
onfocusin={handleInputFocus}
|
||||
placeholder="The quick brown fox..."
|
||||
/>
|
||||
{#if typography.weightControl && typography.sizeControl && typography.heightControl}
|
||||
<div class="flex flex-col justify-between items-center-safe gap-6">
|
||||
<ComboControlV2 control={typography.weightControl} orientation="horizontal" />
|
||||
<ComboControlV2 control={typography.sizeControl} orientation="horizontal" />
|
||||
<ComboControlV2 control={typography.heightControl} orientation="horizontal" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<ExpandableWrapper
|
||||
bind:element={wrapper}
|
||||
bind:expanded={isActive}
|
||||
disabled={isDragging}
|
||||
aria-label="Font controls"
|
||||
rotation={side === 'right' ? 'counterclockwise' : 'clockwise'}
|
||||
class={cn(
|
||||
'transition-opacity flex items-top gap-1.5',
|
||||
panelWidth === 0 ? 'opacity-0' : 'opacity-100',
|
||||
)}
|
||||
containerClassName={cn(!isActive && 'p-2 sm:p-0')}
|
||||
>
|
||||
{#snippet badge()}
|
||||
<div
|
||||
class={cn(
|
||||
'animate-nudge relative transition-all',
|
||||
side === 'left' ? 'order-2' : 'order-0',
|
||||
isActive ? 'opacity-0' : 'opacity-100',
|
||||
isDragging && 'opacity-80 grayscale-[0.2]',
|
||||
)}
|
||||
>
|
||||
<AArrowUP class={cn('size-3', 'stroke-indigo-600')} />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet visibleContent()}
|
||||
<div class="relative">
|
||||
<!--
|
||||
<Input
|
||||
bind:value={comparisonStore.text}
|
||||
disabled={isDragging}
|
||||
onfocusin={handleInputFocus}
|
||||
class={cn(
|
||||
isActive
|
||||
? 'h-7 sm:h-8 text-[11px] sm:text-xs text-center bg-white/40 border-none rounded-lg focus-visible:ring-indigo-500/50 text-slate-900'
|
||||
: 'bg-transparent shadow-none border-none p-0 h-auto text-sm sm:text-base font-medium focus-visible:ring-0 text-slate-900/50',
|
||||
' placeholder:text-slate-400 selection:bg-indigo-100 flex-1 transition-all duration-350 w-44 sm:w-56',
|
||||
)}
|
||||
placeholder="The quick brown fox..."
|
||||
/>
|
||||
-->
|
||||
<Input
|
||||
class={cn(
|
||||
'pl-1 sm:pl-3 pr-1 sm:pr-3',
|
||||
'h-6 sm:h-8 md:h-10',
|
||||
'rounded-lg',
|
||||
isActive
|
||||
? 'h-7 sm:h-8 text-[0.825rem]'
|
||||
: 'bg-transparent shadow-none border-none p-0 h-auto text-sm sm:text-base font-medium focus-visible:ring-0 text-slate-900/50',
|
||||
)}
|
||||
bind:value={comparisonStore.text}
|
||||
disabled={isDragging}
|
||||
onfocusin={handleInputFocus}
|
||||
placeholder="The quick brown fox..."
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet hiddenContent()}
|
||||
{#if typography.weightControl && typography.sizeControl && typography.heightControl}
|
||||
<div class="flex flex-row justify-between items-center-safe gap-2 sm:gap-0">
|
||||
<ComboControlV2 control={typography.weightControl} />
|
||||
<ComboControlV2 control={typography.sizeControl} />
|
||||
<ComboControlV2 control={typography.heightControl} />
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ExpandableWrapper>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,3 +0,0 @@
|
||||
import TypographyMenu from './ui/TypographyMenu.svelte';
|
||||
|
||||
export { TypographyMenu };
|
||||
@@ -1,3 +1,2 @@
|
||||
export { ComparisonSlider } from './ComparisonSlider';
|
||||
export { FontSearch } from './FontSearch';
|
||||
export { TypographyMenu } from './TypographySettings';
|
||||
|
||||
@@ -2470,6 +2470,7 @@ __metadata:
|
||||
tailwindcss: "npm:^4.1.18"
|
||||
tw-animate-css: "npm:^1.4.0"
|
||||
typescript: "npm:^5.9.3"
|
||||
vaul-svelte: "npm:^1.0.0-next.7"
|
||||
vite: "npm:^7.2.6"
|
||||
vitest: "npm:^4.0.16"
|
||||
vitest-browser-svelte: "npm:^2.0.1"
|
||||
@@ -3625,6 +3626,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"runed@npm:^0.23.2":
|
||||
version: 0.23.4
|
||||
resolution: "runed@npm:0.23.4"
|
||||
dependencies:
|
||||
esm-env: "npm:^1.0.0"
|
||||
peerDependencies:
|
||||
svelte: ^5.7.0
|
||||
checksum: 10c0/e27400af9e69b966dca449b851e82e09b3d2ddde4095ba72237599aa80fc248a23d0737c0286f751ca6c12721a5e09eb21b9d8cc872cbd70e7b161442818eece
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"runed@npm:^0.35.1":
|
||||
version: 0.35.1
|
||||
resolution: "runed@npm:0.35.1"
|
||||
@@ -3908,6 +3920,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"svelte-toolbelt@npm:^0.7.1":
|
||||
version: 0.7.1
|
||||
resolution: "svelte-toolbelt@npm:0.7.1"
|
||||
dependencies:
|
||||
clsx: "npm:^2.1.1"
|
||||
runed: "npm:^0.23.2"
|
||||
style-to-object: "npm:^1.0.8"
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
checksum: 10c0/a50db97c851fa65af7fbf77007bd76730a179ac0239c0121301bd26682c1078a4ffea77835492550b133849a42d3dffee0714ae076154d86be8d0b3a84c9a9bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"svelte2tsx@npm:^0.7.44, svelte2tsx@npm:~0.7.46":
|
||||
version: 0.7.46
|
||||
resolution: "svelte2tsx@npm:0.7.46"
|
||||
@@ -4232,6 +4257,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vaul-svelte@npm:^1.0.0-next.7":
|
||||
version: 1.0.0-next.7
|
||||
resolution: "vaul-svelte@npm:1.0.0-next.7"
|
||||
dependencies:
|
||||
runed: "npm:^0.23.2"
|
||||
svelte-toolbelt: "npm:^0.7.1"
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
checksum: 10c0/7a459122b39c9ef6bd830b525d5f6acbc07575491e05c758d9dfdb993cc98ab4dee4a9c022e475760faaf1d7bd8460a1434965431d36885a3ee48315ffa54eb3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.2.6":
|
||||
version: 7.3.0
|
||||
resolution: "vite@npm:7.3.0"
|
||||
|
||||
Reference in New Issue
Block a user