From d8e5f5a0b5807d2636fbb1246ff5d1f9a24a974c Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 4 Jan 2026 10:27:46 +0300 Subject: [PATCH 1/2] fix(SetupFont): correct line height increase handler - Fixed copy-paste error in SetupFontMenu.svelte line 43 - Changed onIncrease from fontSizeStore.increase to lineHeightStore.increase - Line height control now correctly modifies line height instead of font size Closes #? --- src/features/SetupFont/model/const/const.ts | 14 +++++ .../SetupFont/model/stores/fontSizeStore.ts | 17 ++++++ .../SetupFont/model/stores/fontWeightStore.ts | 19 +++++++ .../SetupFont/model/stores/lineHeightStore.ts | 19 +++++++ .../SetupFont/ui/SetupFontMenu.svelte | 52 +++++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 src/features/SetupFont/model/const/const.ts create mode 100644 src/features/SetupFont/model/stores/fontSizeStore.ts create mode 100644 src/features/SetupFont/model/stores/fontWeightStore.ts create mode 100644 src/features/SetupFont/model/stores/lineHeightStore.ts create mode 100644 src/features/SetupFont/ui/SetupFontMenu.svelte diff --git a/src/features/SetupFont/model/const/const.ts b/src/features/SetupFont/model/const/const.ts new file mode 100644 index 0000000..762016b --- /dev/null +++ b/src/features/SetupFont/model/const/const.ts @@ -0,0 +1,14 @@ +export const DEFAULT_FONT_SIZE = 16; +export const MIN_FONT_SIZE = 8; +export const MAX_FONT_SIZE = 100; +export const FONT_SIZE_STEP = 1; + +export const DEFAULT_FONT_WEIGHT = 400; +export const MIN_FONT_WEIGHT = 100; +export const MAX_FONT_WEIGHT = 900; +export const FONT_WEIGHT_STEP = 100; + +export const DEFAULT_LINE_HEIGHT = 1.5; +export const MIN_LINE_HEIGHT = 1; +export const MAX_LINE_HEIGHT = 2; +export const LINE_HEIGHT_STEP = 0.1; diff --git a/src/features/SetupFont/model/stores/fontSizeStore.ts b/src/features/SetupFont/model/stores/fontSizeStore.ts new file mode 100644 index 0000000..b7cbea3 --- /dev/null +++ b/src/features/SetupFont/model/stores/fontSizeStore.ts @@ -0,0 +1,17 @@ +import { + type ControlModel, + createControlStore, +} from '$shared/store/createControlStore'; +import { + DEFAULT_FONT_SIZE, + MAX_FONT_SIZE, + MIN_FONT_SIZE, +} from '../const/const'; + +const initialValue: ControlModel = { + value: DEFAULT_FONT_SIZE, + max: MAX_FONT_SIZE, + min: MIN_FONT_SIZE, +}; + +export const fontSizeStore = createControlStore(initialValue); diff --git a/src/features/SetupFont/model/stores/fontWeightStore.ts b/src/features/SetupFont/model/stores/fontWeightStore.ts new file mode 100644 index 0000000..4434088 --- /dev/null +++ b/src/features/SetupFont/model/stores/fontWeightStore.ts @@ -0,0 +1,19 @@ +import { + type ControlModel, + createControlStore, +} from '$shared/store/createControlStore'; +import { + DEFAULT_FONT_WEIGHT, + FONT_WEIGHT_STEP, + MAX_FONT_WEIGHT, + MIN_FONT_WEIGHT, +} from '../const/const'; + +const initialValue: ControlModel = { + value: DEFAULT_FONT_WEIGHT, + max: MAX_FONT_WEIGHT, + min: MIN_FONT_WEIGHT, + step: FONT_WEIGHT_STEP, +}; + +export const fontWeightStore = createControlStore(initialValue); diff --git a/src/features/SetupFont/model/stores/lineHeightStore.ts b/src/features/SetupFont/model/stores/lineHeightStore.ts new file mode 100644 index 0000000..557ed76 --- /dev/null +++ b/src/features/SetupFont/model/stores/lineHeightStore.ts @@ -0,0 +1,19 @@ +import { + type ControlModel, + createControlStore, +} from '$shared/store/createControlStore'; +import { + DEFAULT_LINE_HEIGHT, + LINE_HEIGHT_STEP, + MAX_LINE_HEIGHT, + MIN_LINE_HEIGHT, +} from '../const/const'; + +const initialValue: ControlModel = { + value: DEFAULT_LINE_HEIGHT, + max: MAX_LINE_HEIGHT, + min: MIN_LINE_HEIGHT, + step: LINE_HEIGHT_STEP, +}; + +export const lineHeightStore = createControlStore(initialValue); diff --git a/src/features/SetupFont/ui/SetupFontMenu.svelte b/src/features/SetupFont/ui/SetupFontMenu.svelte new file mode 100644 index 0000000..9de8b06 --- /dev/null +++ b/src/features/SetupFont/ui/SetupFontMenu.svelte @@ -0,0 +1,52 @@ + + +
+ + + + + + + + + +
From 3d35f1901dacbf4e887b8e6438474238757ae3fd Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 5 Jan 2026 09:03:31 +0300 Subject: [PATCH 2/2] feature(ComboControl): - create ComboControl component for typography settings (font size, font weight, line height) - integrate it to TypographyMenu and integrate it to Layout --- src/app/ui/Layout.svelte | 7 +- src/features/SetupFont/model/const/const.ts | 2 +- .../SetupFont/ui/SetupFontMenu.svelte | 7 + .../button-group-separator.svelte | 20 +++ .../ui/button-group/button-group-text.svelte | 33 ++++ .../ui/button-group/button-group.svelte | 53 ++++++ src/shared/shadcn/ui/button-group/index.ts | 13 ++ src/shared/shadcn/ui/item/index.ts | 34 ++++ src/shared/shadcn/ui/item/item-actions.svelte | 23 +++ src/shared/shadcn/ui/item/item-content.svelte | 23 +++ .../shadcn/ui/item/item-description.svelte | 27 +++ src/shared/shadcn/ui/item/item-footer.svelte | 23 +++ src/shared/shadcn/ui/item/item-group.svelte | 24 +++ src/shared/shadcn/ui/item/item-header.svelte | 23 +++ src/shared/shadcn/ui/item/item-media.svelte | 49 ++++++ .../shadcn/ui/item/item-separator.svelte | 19 +++ src/shared/shadcn/ui/item/item-title.svelte | 23 +++ src/shared/shadcn/ui/item/item.svelte | 67 ++++++++ src/shared/shadcn/ui/popover/index.ts | 19 +++ .../shadcn/ui/popover/popover-close.svelte | 7 + .../shadcn/ui/popover/popover-content.svelte | 34 ++++ .../shadcn/ui/popover/popover-portal.svelte | 7 + .../shadcn/ui/popover/popover-trigger.svelte | 17 ++ src/shared/shadcn/ui/popover/popover.svelte | 7 + src/shared/shadcn/ui/slider/index.ts | 7 + src/shared/shadcn/ui/slider/slider.svelte | 51 ++++++ src/shared/store/createControlStore.ts | 117 +++++++++++++ .../ui/ComboControl/ComboControl.svelte | 155 ++++++++++++++++++ src/widgets/TypographySettings/index.ts | 0 .../ui/TypographyMenu.svelte | 10 ++ 30 files changed, 897 insertions(+), 4 deletions(-) create mode 100644 src/shared/shadcn/ui/button-group/button-group-separator.svelte create mode 100644 src/shared/shadcn/ui/button-group/button-group-text.svelte create mode 100644 src/shared/shadcn/ui/button-group/button-group.svelte create mode 100644 src/shared/shadcn/ui/button-group/index.ts create mode 100644 src/shared/shadcn/ui/item/index.ts create mode 100644 src/shared/shadcn/ui/item/item-actions.svelte create mode 100644 src/shared/shadcn/ui/item/item-content.svelte create mode 100644 src/shared/shadcn/ui/item/item-description.svelte create mode 100644 src/shared/shadcn/ui/item/item-footer.svelte create mode 100644 src/shared/shadcn/ui/item/item-group.svelte create mode 100644 src/shared/shadcn/ui/item/item-header.svelte create mode 100644 src/shared/shadcn/ui/item/item-media.svelte create mode 100644 src/shared/shadcn/ui/item/item-separator.svelte create mode 100644 src/shared/shadcn/ui/item/item-title.svelte create mode 100644 src/shared/shadcn/ui/item/item.svelte create mode 100644 src/shared/shadcn/ui/popover/index.ts create mode 100644 src/shared/shadcn/ui/popover/popover-close.svelte create mode 100644 src/shared/shadcn/ui/popover/popover-content.svelte create mode 100644 src/shared/shadcn/ui/popover/popover-portal.svelte create mode 100644 src/shared/shadcn/ui/popover/popover-trigger.svelte create mode 100644 src/shared/shadcn/ui/popover/popover.svelte create mode 100644 src/shared/shadcn/ui/slider/index.ts create mode 100644 src/shared/shadcn/ui/slider/slider.svelte create mode 100644 src/shared/store/createControlStore.ts create mode 100644 src/shared/ui/ComboControl/ComboControl.svelte create mode 100644 src/widgets/TypographySettings/index.ts create mode 100644 src/widgets/TypographySettings/ui/TypographyMenu.svelte diff --git a/src/app/ui/Layout.svelte b/src/app/ui/Layout.svelte index b188574..920662e 100644 --- a/src/app/ui/Layout.svelte +++ b/src/app/ui/Layout.svelte @@ -16,6 +16,7 @@ import favicon from '$shared/assets/favicon.svg'; import * as Sidebar from '$shared/shadcn/ui/sidebar/index'; import { FiltersSidebar } from '$widgets/FiltersSidebar'; +import TypographyMenu from '$widgets/TypographySettings/ui/TypographyMenu.svelte'; /** Slot content for route pages to render */ let { children } = $props(); @@ -25,13 +26,13 @@ let { children } = $props(); -
+
-
- +
+ {@render children?.()}
diff --git a/src/features/SetupFont/model/const/const.ts b/src/features/SetupFont/model/const/const.ts index 762016b..e9b9085 100644 --- a/src/features/SetupFont/model/const/const.ts +++ b/src/features/SetupFont/model/const/const.ts @@ -11,4 +11,4 @@ export const FONT_WEIGHT_STEP = 100; export const DEFAULT_LINE_HEIGHT = 1.5; export const MIN_LINE_HEIGHT = 1; export const MAX_LINE_HEIGHT = 2; -export const LINE_HEIGHT_STEP = 0.1; +export const LINE_HEIGHT_STEP = 0.05; diff --git a/src/features/SetupFont/ui/SetupFontMenu.svelte b/src/features/SetupFont/ui/SetupFontMenu.svelte index 9de8b06..67a440c 100644 --- a/src/features/SetupFont/ui/SetupFontMenu.svelte +++ b/src/features/SetupFont/ui/SetupFontMenu.svelte @@ -19,6 +19,8 @@ const lineHeight = $derived($lineHeightStore); +import { Separator } from '$shared/shadcn/ui/separator/index.js'; +import { cn } from '$shared/shadcn/utils/shadcn-utils.js'; +import type { ComponentProps } from 'svelte'; + +let { + ref = $bindable(null), + class: className, + orientation = 'vertical', + ...restProps +}: ComponentProps = $props(); + + + diff --git a/src/shared/shadcn/ui/button-group/button-group-text.svelte b/src/shared/shadcn/ui/button-group/button-group-text.svelte new file mode 100644 index 0000000..17c9cd2 --- /dev/null +++ b/src/shared/shadcn/ui/button-group/button-group-text.svelte @@ -0,0 +1,33 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render mergedProps.children?.()} +
+{/if} diff --git a/src/shared/shadcn/ui/button-group/button-group.svelte b/src/shared/shadcn/ui/button-group/button-group.svelte new file mode 100644 index 0000000..60b4e29 --- /dev/null +++ b/src/shared/shadcn/ui/button-group/button-group.svelte @@ -0,0 +1,53 @@ + + + + +
+ {@render children?.()} +
diff --git a/src/shared/shadcn/ui/button-group/index.ts b/src/shared/shadcn/ui/button-group/index.ts new file mode 100644 index 0000000..76299b7 --- /dev/null +++ b/src/shared/shadcn/ui/button-group/index.ts @@ -0,0 +1,13 @@ +import Separator from './button-group-separator.svelte'; +import Text from './button-group-text.svelte'; +import Root from './button-group.svelte'; + +export { + Root, + // + Root as ButtonGroup, + Separator, + Separator as ButtonGroupSeparator, + Text, + Text as ButtonGroupText, +}; diff --git a/src/shared/shadcn/ui/item/index.ts b/src/shared/shadcn/ui/item/index.ts new file mode 100644 index 0000000..e5c35f9 --- /dev/null +++ b/src/shared/shadcn/ui/item/index.ts @@ -0,0 +1,34 @@ +import Actions from './item-actions.svelte'; +import Content from './item-content.svelte'; +import Description from './item-description.svelte'; +import Footer from './item-footer.svelte'; +import Group from './item-group.svelte'; +import Header from './item-header.svelte'; +import Media from './item-media.svelte'; +import Separator from './item-separator.svelte'; +import Title from './item-title.svelte'; +import Root from './item.svelte'; + +export { + Actions, + Actions as ItemActions, + Content, + Content as ItemContent, + Description, + Description as ItemDescription, + Footer, + Footer as ItemFooter, + Group, + Group as ItemGroup, + Header, + Header as ItemHeader, + Media, + Media as ItemMedia, + Root, + // + Root as Item, + Separator, + Separator as ItemSeparator, + Title, + Title as ItemTitle, +}; diff --git a/src/shared/shadcn/ui/item/item-actions.svelte b/src/shared/shadcn/ui/item/item-actions.svelte new file mode 100644 index 0000000..fd10882 --- /dev/null +++ b/src/shared/shadcn/ui/item/item-actions.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/shared/shadcn/ui/item/item-content.svelte b/src/shared/shadcn/ui/item/item-content.svelte new file mode 100644 index 0000000..cbfd5f3 --- /dev/null +++ b/src/shared/shadcn/ui/item/item-content.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/shared/shadcn/ui/item/item-description.svelte b/src/shared/shadcn/ui/item/item-description.svelte new file mode 100644 index 0000000..9a51cbe --- /dev/null +++ b/src/shared/shadcn/ui/item/item-description.svelte @@ -0,0 +1,27 @@ + + +

a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4', + className, + )} + {...restProps} +> + {@render children?.()} +

diff --git a/src/shared/shadcn/ui/item/item-footer.svelte b/src/shared/shadcn/ui/item/item-footer.svelte new file mode 100644 index 0000000..42844ca --- /dev/null +++ b/src/shared/shadcn/ui/item/item-footer.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/shared/shadcn/ui/item/item-group.svelte b/src/shared/shadcn/ui/item/item-group.svelte new file mode 100644 index 0000000..6f44cad --- /dev/null +++ b/src/shared/shadcn/ui/item/item-group.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/src/shared/shadcn/ui/item/item-header.svelte b/src/shared/shadcn/ui/item/item-header.svelte new file mode 100644 index 0000000..a4a3cff --- /dev/null +++ b/src/shared/shadcn/ui/item/item-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/shared/shadcn/ui/item/item-media.svelte b/src/shared/shadcn/ui/item/item-media.svelte new file mode 100644 index 0000000..052321b --- /dev/null +++ b/src/shared/shadcn/ui/item/item-media.svelte @@ -0,0 +1,49 @@ + + + + +
+ {@render children?.()} +
diff --git a/src/shared/shadcn/ui/item/item-separator.svelte b/src/shared/shadcn/ui/item/item-separator.svelte new file mode 100644 index 0000000..cbb5d87 --- /dev/null +++ b/src/shared/shadcn/ui/item/item-separator.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/shared/shadcn/ui/item/item-title.svelte b/src/shared/shadcn/ui/item/item-title.svelte new file mode 100644 index 0000000..90ad16b --- /dev/null +++ b/src/shared/shadcn/ui/item/item-title.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/shared/shadcn/ui/item/item.svelte b/src/shared/shadcn/ui/item/item.svelte new file mode 100644 index 0000000..c01acbf --- /dev/null +++ b/src/shared/shadcn/ui/item/item.svelte @@ -0,0 +1,67 @@ + + + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render mergedProps.children?.()} +
+{/if} diff --git a/src/shared/shadcn/ui/popover/index.ts b/src/shared/shadcn/ui/popover/index.ts new file mode 100644 index 0000000..e5456bb --- /dev/null +++ b/src/shared/shadcn/ui/popover/index.ts @@ -0,0 +1,19 @@ +import Close from './popover-close.svelte'; +import Content from './popover-content.svelte'; +import Portal from './popover-portal.svelte'; +import Trigger from './popover-trigger.svelte'; +import Root from './popover.svelte'; + +export { + Close, + Close as PopoverClose, + Content, + Content as PopoverContent, + Portal, + Portal as PopoverPortal, + Root, + // + Root as Popover, + Trigger, + Trigger as PopoverTrigger, +}; diff --git a/src/shared/shadcn/ui/popover/popover-close.svelte b/src/shared/shadcn/ui/popover/popover-close.svelte new file mode 100644 index 0000000..71124e2 --- /dev/null +++ b/src/shared/shadcn/ui/popover/popover-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/shared/shadcn/ui/popover/popover-content.svelte b/src/shared/shadcn/ui/popover/popover-content.svelte new file mode 100644 index 0000000..304e895 --- /dev/null +++ b/src/shared/shadcn/ui/popover/popover-content.svelte @@ -0,0 +1,34 @@ + + + + + diff --git a/src/shared/shadcn/ui/popover/popover-portal.svelte b/src/shared/shadcn/ui/popover/popover-portal.svelte new file mode 100644 index 0000000..8e68a02 --- /dev/null +++ b/src/shared/shadcn/ui/popover/popover-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/shared/shadcn/ui/popover/popover-trigger.svelte b/src/shared/shadcn/ui/popover/popover-trigger.svelte new file mode 100644 index 0000000..1192af2 --- /dev/null +++ b/src/shared/shadcn/ui/popover/popover-trigger.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/shared/shadcn/ui/popover/popover.svelte b/src/shared/shadcn/ui/popover/popover.svelte new file mode 100644 index 0000000..7cd4812 --- /dev/null +++ b/src/shared/shadcn/ui/popover/popover.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/shared/shadcn/ui/slider/index.ts b/src/shared/shadcn/ui/slider/index.ts new file mode 100644 index 0000000..414e720 --- /dev/null +++ b/src/shared/shadcn/ui/slider/index.ts @@ -0,0 +1,7 @@ +import Root from './slider.svelte'; + +export { + Root, + // + Root as Slider, +}; diff --git a/src/shared/shadcn/ui/slider/slider.svelte b/src/shared/shadcn/ui/slider/slider.svelte new file mode 100644 index 0000000..49e30bc --- /dev/null +++ b/src/shared/shadcn/ui/slider/slider.svelte @@ -0,0 +1,51 @@ + + + + {#snippet children({ thumbs })} + + + + {#each thumbs as thumb (thumb)} + + {/each} + {/snippet} + diff --git a/src/shared/store/createControlStore.ts b/src/shared/store/createControlStore.ts new file mode 100644 index 0000000..a7e7463 --- /dev/null +++ b/src/shared/store/createControlStore.ts @@ -0,0 +1,117 @@ +import { + type Writable, + get, + writable, +} from 'svelte/store'; + +/** + * Model for a control value with min/max bounds + */ +export type ControlModel< + TValue extends number = number, +> = { + value: TValue; + min: TValue; + max: TValue; + step?: TValue; +}; + +/** + * Store model with methods for control manipulation + */ +export type ControlStoreModel< + TValue extends number, +> = + & Writable> + & { + increase: () => void; + decrease: () => void; + /** Set a specific value */ + setValue: (newValue: TValue) => void; + isAtMax: () => boolean; + isAtMin: () => boolean; + }; + +/** + * Create a writable store for numeric control values with bounds + * + * @template TValue - The value type (extends number) + * @param initialState - Initial state containing value, min, and max + */ +/** + * Get the number of decimal places in a number + * + * For example: + * - 1 -> 0 + * - 0.1 -> 1 + * - 0.01 -> 2 + * - 0.05 -> 2 + * + * @param step - The step number to analyze + * @returns The number of decimal places + */ +function getDecimalPlaces(step: number): number { + const str = step.toString(); + const decimalPart = str.split('.')[1]; + return decimalPart ? decimalPart.length : 0; +} + +/** + * Round a value to the precision of the given step + * + * This fixes floating-point precision errors that occur with decimal steps. + * For example, with step=0.05, adding it repeatedly can produce values like + * 1.3499999999999999 instead of 1.35. + * + * We use toFixed() to round to the appropriate decimal places instead of + * Math.round(value / step) * step, which doesn't always work correctly + * due to floating-point arithmetic errors. + * + * @param value - The value to round + * @param step - The step to round to (defaults to 1) + * @returns The rounded value + */ +function roundToStepPrecision(value: number, step: number = 1): number { + if (step <= 0) { + return value; + } + const decimals = getDecimalPlaces(step); + return parseFloat(value.toFixed(decimals)); +} + +export function createControlStore< + TValue extends number = number, +>( + initialState: ControlModel, +): ControlStoreModel { + const store = writable(initialState); + const { subscribe, set, update } = store; + + const clamp = (value: number): TValue => { + return Math.max(initialState.min, Math.min(value, initialState.max)) as TValue; + }; + + return { + subscribe, + set, + update, + increase: () => + update(m => { + const step = m.step ?? 1; + const newValue = clamp(m.value + step); + return { ...m, value: roundToStepPrecision(newValue, step) as TValue }; + }), + decrease: () => + update(m => { + const step = m.step ?? 1; + const newValue = clamp(m.value - step); + return { ...m, value: roundToStepPrecision(newValue, step) as TValue }; + }), + setValue: (v: TValue) => { + const step = initialState.step ?? 1; + update(m => ({ ...m, value: roundToStepPrecision(clamp(v), step) as TValue })); + }, + isAtMin: () => get(store).value === initialState.min, + isAtMax: () => get(store).value === initialState.max, + }; +} diff --git a/src/shared/ui/ComboControl/ComboControl.svelte b/src/shared/ui/ComboControl/ComboControl.svelte new file mode 100644 index 0000000..e600d00 --- /dev/null +++ b/src/shared/ui/ComboControl/ComboControl.svelte @@ -0,0 +1,155 @@ + + + + + + + {#snippet child({ props })} + + {/snippet} + + +
+ + +
+
+
+ +
diff --git a/src/widgets/TypographySettings/index.ts b/src/widgets/TypographySettings/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/widgets/TypographySettings/ui/TypographyMenu.svelte b/src/widgets/TypographySettings/ui/TypographyMenu.svelte new file mode 100644 index 0000000..4398c5c --- /dev/null +++ b/src/widgets/TypographySettings/ui/TypographyMenu.svelte @@ -0,0 +1,10 @@ + + +
+ +