-
-
+
+
{@render children?.()}
diff --git a/src/features/SetupFont/model/const/const.ts b/src/features/SetupFont/model/const/const.ts
new file mode 100644
index 0000000..e9b9085
--- /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.05;
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..67a440c
--- /dev/null
+++ b/src/features/SetupFont/ui/SetupFontMenu.svelte
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/shadcn/ui/button-group/button-group-separator.svelte b/src/shared/shadcn/ui/button-group/button-group-separator.svelte
new file mode 100644
index 0000000..ef281f5
--- /dev/null
+++ b/src/shared/shadcn/ui/button-group/button-group-separator.svelte
@@ -0,0 +1,20 @@
+
+
+
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 })}
+
+ {value}
+
+ {/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 @@
+
+
+
+
+