Files
frontend-svelte/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte
Ilia Mashkov 9b90080c57
All checks were successful
Workflow / build (pull_request) Successful in 3m29s
Workflow / publish (pull_request) Has been skipped
chore: change hex colors to tailwind bariables
2026-03-04 16:51:49 +03:00

194 lines
7.5 KiB
Svelte

<!--
Component: TypographyMenu
Floating controls bar for typography settings.
Warm surface, sharp corners, Settings icon header, dividers between units.
Mobile: popover with slider controls anchored to settings button.
Desktop: inline bar with combo controls.
-->
<script lang="ts">
import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import {
Button,
ComboControl,
ControlGroup,
Slider,
} from '$shared/ui';
import Settings2Icon from '@lucide/svelte/icons/settings-2';
import XIcon from '@lucide/svelte/icons/x';
import { Popover } from 'bits-ui';
import { getContext } from 'svelte';
import { cubicOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import {
MULTIPLIER_L,
MULTIPLIER_M,
MULTIPLIER_S,
controlManager,
} from '../../model';
interface Props {
/**
* CSS classes
*/
class?: string;
/**
* Hidden state
* @default false
*/
hidden?: boolean;
}
const { class: className, hidden = false }: Props = $props();
const responsive = getContext<ResponsiveManager>('responsive');
let isOpen = $state(false);
/**
* Sets the common font size multiplier based on the current responsive state.
*/
$effect(() => {
if (!responsive) return;
switch (true) {
case responsive.isMobile:
controlManager.multiplier = MULTIPLIER_S;
break;
case responsive.isTablet:
controlManager.multiplier = MULTIPLIER_M;
break;
case responsive.isDesktop:
controlManager.multiplier = MULTIPLIER_L;
break;
default:
controlManager.multiplier = MULTIPLIER_L;
}
});
</script>
{#if !hidden}
{#if responsive.isMobile}
<Popover.Root bind:open={isOpen}>
<Popover.Trigger>
{#snippet child({ props })}
<button
{...props}
class={cn(
'inline-flex items-center justify-center',
'size-8 p-0',
'border border-transparent rounded-none',
'transition-colors duration-150',
'hover:bg-white/50 dark:hover:bg-white/5',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30',
isOpen && 'bg-paper dark:bg-dark-card border-black/5 dark:border-white/10 shadow-sm',
className,
)}
>
<Settings2Icon class="size-4" />
</button>
{/snippet}
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
side="top"
align="start"
sideOffset={8}
class={cn(
'z-50 w-72',
'bg-surface dark:bg-dark-card',
'border border-black/5 dark:border-white/10',
'shadow-[0_20px_40px_-10px_rgba(0,0,0,0.15)]',
'rounded-none p-4',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
'data-[side=top]:slide-in-from-bottom-2',
'data-[side=bottom]:slide-in-from-top-2',
)}
interactOutsideBehavior="close"
escapeKeydownBehavior="close"
>
<!-- Header -->
<div class="flex items-center justify-between mb-3 pb-3 border-b border-black/5 dark:border-white/10">
<div class="flex items-center gap-1.5">
<Settings2Icon size={12} class="text-swiss-red" />
<span
class="text-[0.5625rem] font-mono uppercase tracking-widest font-bold text-swiss-black dark:text-neutral-200"
>
CONTROLS
</span>
</div>
<Popover.Close>
{#snippet child({ props })}
<button
{...props}
class="inline-flex items-center justify-center size-6 rounded-none hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
aria-label="Close controls"
>
<XIcon class="size-3.5 text-neutral-500" />
</button>
{/snippet}
</Popover.Close>
</div>
<!-- Controls -->
{#each controlManager.controls as control (control.id)}
<ControlGroup label={control.controlLabel ?? ''}>
<Slider
bind:value={control.instance.value}
min={control.instance.min}
max={control.instance.max}
step={control.instance.step}
/>
</ControlGroup>
{/each}
</Popover.Content>
</Popover.Portal>
</Popover.Root>
{:else}
<div
class={cn('w-full md:w-auto', className)}
transition:fly={{ y: 100, duration: 200, easing: cubicOut }}
>
<div
class={cn(
'flex items-center gap-1 md:gap-2 p-1.5 md:p-2',
'bg-surface/95 dark:bg-dark-bg/95 backdrop-blur-xl',
'border border-black/5 dark:border-white/10',
'shadow-[0_20px_40px_-10px_rgba(0,0,0,0.1)]',
'rounded-none ring-1 ring-black/5 dark:ring-white/5',
)}
>
<!-- Header: icon + label -->
<div class="px-2 md:px-3 flex items-center gap-1.5 md:gap-2 border-r border-black/5 dark:border-white/10 mr-1 text-swiss-black dark:text-neutral-200 shrink-0">
<Settings2Icon
size={14}
class="text-swiss-red"
/>
<span
class="text-[0.5625rem] md:text-[0.625rem] font-mono uppercase tracking-widest font-bold hidden sm:inline whitespace-nowrap"
>
GLOBAL_CONTROLS
</span>
</div>
<!-- Controls with dividers between each -->
{#each controlManager.controls as control, i (control.id)}
{#if i > 0}
<div class="w-px h-6 md:h-8 bg-black/5 dark:bg-white/10 mx-0.5 md:mx-1 shrink-0"></div>
{/if}
<ComboControl
control={control.instance}
label={control.controlLabel}
increaseLabel={control.increaseLabel}
decreaseLabel={control.decreaseLabel}
controlLabel={control.controlLabel}
/>
{/each}
</div>
</div>
{/if}
{/if}