feature(ComboControl):
Some checks failed
Some checks failed
- create ComboControl component for typography settings (font size, font weight, line height) - integrate it to TypographyMenu and integrate it to Layout
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
import favicon from '$shared/assets/favicon.svg';
|
import favicon from '$shared/assets/favicon.svg';
|
||||||
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
||||||
import { FiltersSidebar } from '$widgets/FiltersSidebar';
|
import { FiltersSidebar } from '$widgets/FiltersSidebar';
|
||||||
|
import TypographyMenu from '$widgets/TypographySettings/ui/TypographyMenu.svelte';
|
||||||
|
|
||||||
/** Slot content for route pages to render */
|
/** Slot content for route pages to render */
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
@@ -25,13 +26,13 @@ let { children } = $props();
|
|||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="app">
|
<div id="app-root">
|
||||||
<header></header>
|
<header></header>
|
||||||
|
|
||||||
<Sidebar.Provider>
|
<Sidebar.Provider>
|
||||||
<FiltersSidebar />
|
<FiltersSidebar />
|
||||||
<main>
|
<main class="w-dvw">
|
||||||
<Sidebar.Trigger />
|
<TypographyMenu />
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</main>
|
</main>
|
||||||
</Sidebar.Provider>
|
</Sidebar.Provider>
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ export const FONT_WEIGHT_STEP = 100;
|
|||||||
export const DEFAULT_LINE_HEIGHT = 1.5;
|
export const DEFAULT_LINE_HEIGHT = 1.5;
|
||||||
export const MIN_LINE_HEIGHT = 1;
|
export const MIN_LINE_HEIGHT = 1;
|
||||||
export const MAX_LINE_HEIGHT = 2;
|
export const MAX_LINE_HEIGHT = 2;
|
||||||
export const LINE_HEIGHT_STEP = 0.1;
|
export const LINE_HEIGHT_STEP = 0.05;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const lineHeight = $derived($lineHeightStore);
|
|||||||
<Separator orientation="vertical" class="h-full" />
|
<Separator orientation="vertical" class="h-full" />
|
||||||
<ComboControl
|
<ComboControl
|
||||||
value={fontSize.value}
|
value={fontSize.value}
|
||||||
|
minValue={fontSize.min}
|
||||||
|
maxValue={fontSize.max}
|
||||||
onChange={fontSizeStore.setValue}
|
onChange={fontSizeStore.setValue}
|
||||||
onIncrease={fontSizeStore.increase}
|
onIncrease={fontSizeStore.increase}
|
||||||
onDecrease={fontSizeStore.decrease}
|
onDecrease={fontSizeStore.decrease}
|
||||||
@@ -29,6 +31,8 @@ const lineHeight = $derived($lineHeightStore);
|
|||||||
/>
|
/>
|
||||||
<ComboControl
|
<ComboControl
|
||||||
value={fontWeight.value}
|
value={fontWeight.value}
|
||||||
|
minValue={fontWeight.min}
|
||||||
|
maxValue={fontWeight.max}
|
||||||
onChange={fontWeightStore.setValue}
|
onChange={fontWeightStore.setValue}
|
||||||
onIncrease={fontWeightStore.increase}
|
onIncrease={fontWeightStore.increase}
|
||||||
onDecrease={fontWeightStore.decrease}
|
onDecrease={fontWeightStore.decrease}
|
||||||
@@ -39,6 +43,9 @@ const lineHeight = $derived($lineHeightStore);
|
|||||||
/>
|
/>
|
||||||
<ComboControl
|
<ComboControl
|
||||||
value={lineHeight.value}
|
value={lineHeight.value}
|
||||||
|
minValue={lineHeight.min}
|
||||||
|
maxValue={lineHeight.max}
|
||||||
|
step={lineHeight.step}
|
||||||
onChange={lineHeightStore.setValue}
|
onChange={lineHeightStore.setValue}
|
||||||
onIncrease={lineHeightStore.increase}
|
onIncrease={lineHeightStore.increase}
|
||||||
onDecrease={lineHeightStore.decrease}
|
onDecrease={lineHeightStore.decrease}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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<typeof Separator> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="button-group-separator"
|
||||||
|
{orientation}
|
||||||
|
class={cn('bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
33
src/shared/shadcn/ui/button-group/button-group-text.svelte
Normal file
33
src/shared/shadcn/ui/button-group/button-group-text.svelte
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
type WithElementRef,
|
||||||
|
cn,
|
||||||
|
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
child,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const mergedProps = $derived({
|
||||||
|
...restProps,
|
||||||
|
class: cn(
|
||||||
|
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if child}
|
||||||
|
{@render child({ props: mergedProps })}
|
||||||
|
{:else}
|
||||||
|
<div bind:this={ref} {...mergedProps}>
|
||||||
|
{@render mergedProps.children?.()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
53
src/shared/shadcn/ui/button-group/button-group.svelte
Normal file
53
src/shared/shadcn/ui/button-group/button-group.svelte
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import {
|
||||||
|
type VariantProps,
|
||||||
|
tv,
|
||||||
|
} from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const buttonGroupVariants = tv({
|
||||||
|
base:
|
||||||
|
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-e-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
horizontal:
|
||||||
|
'[&>*:not(:first-child)]:rounded-s-none [&>*:not(:first-child)]:border-s-0 [&>*:not(:last-child)]:rounded-e-none',
|
||||||
|
vertical:
|
||||||
|
'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
orientation: 'horizontal',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonGroupOrientation = VariantProps<typeof buttonGroupVariants>['orientation'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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,
|
||||||
|
orientation = 'horizontal',
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
orientation?: ButtonGroupOrientation;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
role="group"
|
||||||
|
data-slot="button-group"
|
||||||
|
data-orientation={orientation}
|
||||||
|
class={cn(buttonGroupVariants({ orientation }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
13
src/shared/shadcn/ui/button-group/index.ts
Normal file
13
src/shared/shadcn/ui/button-group/index.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
34
src/shared/shadcn/ui/item/index.ts
Normal file
34
src/shared/shadcn/ui/item/index.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
23
src/shared/shadcn/ui/item/item-actions.svelte
Normal file
23
src/shared/shadcn/ui/item/item-actions.svelte
Normal file
@@ -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="item-actions"
|
||||||
|
class={cn('flex items-center gap-2', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
23
src/shared/shadcn/ui/item/item-content.svelte
Normal file
23
src/shared/shadcn/ui/item/item-content.svelte
Normal file
@@ -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="item-content"
|
||||||
|
class={cn('flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
27
src/shared/shadcn/ui/item/item-description.svelte
Normal file
27
src/shared/shadcn/ui/item/item-description.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<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<HTMLParagraphElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="item-description"
|
||||||
|
class={cn(
|
||||||
|
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
|
||||||
|
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
23
src/shared/shadcn/ui/item/item-footer.svelte
Normal file
23
src/shared/shadcn/ui/item/item-footer.svelte
Normal file
@@ -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="item-footer"
|
||||||
|
class={cn('flex basis-full items-center justify-between gap-2', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
24
src/shared/shadcn/ui/item/item-group.svelte
Normal file
24
src/shared/shadcn/ui/item/item-group.svelte
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<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}
|
||||||
|
role="list"
|
||||||
|
data-slot="item-group"
|
||||||
|
class={cn('group/item-group flex flex-col', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
23
src/shared/shadcn/ui/item/item-header.svelte
Normal file
23
src/shared/shadcn/ui/item/item-header.svelte
Normal file
@@ -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="item-header"
|
||||||
|
class={cn('flex basis-full items-center justify-between gap-2', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
49
src/shared/shadcn/ui/item/item-media.svelte
Normal file
49
src/shared/shadcn/ui/item/item-media.svelte
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import {
|
||||||
|
type VariantProps,
|
||||||
|
tv,
|
||||||
|
} from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const itemMediaVariants = tv({
|
||||||
|
base:
|
||||||
|
'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none',
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-transparent',
|
||||||
|
icon: "bg-muted size-8 rounded-sm border [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
image: 'size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ItemMediaVariant = VariantProps<typeof itemMediaVariants>['variant'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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,
|
||||||
|
variant = 'default',
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: ItemMediaVariant } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="item-media"
|
||||||
|
data-variant={variant}
|
||||||
|
class={cn(itemMediaVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
19
src/shared/shadcn/ui/item/item-separator.svelte
Normal file
19
src/shared/shadcn/ui/item/item-separator.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof Separator> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="item-separator"
|
||||||
|
orientation="horizontal"
|
||||||
|
class={cn('my-0', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
23
src/shared/shadcn/ui/item/item-title.svelte
Normal file
23
src/shared/shadcn/ui/item/item-title.svelte
Normal file
@@ -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="item-title"
|
||||||
|
class={cn('flex w-fit items-center gap-2 text-sm leading-snug font-medium', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
67
src/shared/shadcn/ui/item/item.svelte
Normal file
67
src/shared/shadcn/ui/item/item.svelte
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import {
|
||||||
|
type VariantProps,
|
||||||
|
tv,
|
||||||
|
} from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const itemVariants = tv({
|
||||||
|
base:
|
||||||
|
'group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 flex flex-wrap items-center rounded-md border border-transparent text-sm transition-colors duration-100 outline-none focus-visible:ring-[3px] [a]:transition-colors',
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-transparent',
|
||||||
|
outline: 'border-border',
|
||||||
|
muted: 'bg-muted/50',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'gap-4 p-4',
|
||||||
|
sm: 'gap-2.5 px-4 py-3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ItemSize = VariantProps<typeof itemVariants>['size'];
|
||||||
|
export type ItemVariant = VariantProps<typeof itemVariants>['variant'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
type WithElementRef,
|
||||||
|
cn,
|
||||||
|
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
child,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||||
|
variant?: ItemVariant;
|
||||||
|
size?: ItemSize;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const mergedProps = $derived({
|
||||||
|
class: cn(itemVariants({ variant, size }), className),
|
||||||
|
'data-slot': 'item',
|
||||||
|
'data-variant': variant,
|
||||||
|
'data-size': size,
|
||||||
|
...restProps,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if child}
|
||||||
|
{@render child({ props: mergedProps })}
|
||||||
|
{:else}
|
||||||
|
<div bind:this={ref} {...mergedProps}>
|
||||||
|
{@render mergedProps.children?.()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
19
src/shared/shadcn/ui/popover/index.ts
Normal file
19
src/shared/shadcn/ui/popover/index.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
7
src/shared/shadcn/ui/popover/popover-close.svelte
Normal file
7
src/shared/shadcn/ui/popover/popover-close.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
|
||||||
34
src/shared/shadcn/ui/popover/popover-content.svelte
Normal file
34
src/shared/shadcn/ui/popover/popover-content.svelte
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
type WithoutChildrenOrChild,
|
||||||
|
cn,
|
||||||
|
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||||
|
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import PopoverPortal from './popover-portal.svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
sideOffset = 4,
|
||||||
|
align = 'center',
|
||||||
|
portalProps,
|
||||||
|
...restProps
|
||||||
|
}: PopoverPrimitive.ContentProps & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPortal {...portalProps}>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="popover-content"
|
||||||
|
{sideOffset}
|
||||||
|
{align}
|
||||||
|
class={cn(
|
||||||
|
'bg-popover text-popover-foreground 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=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</PopoverPortal>
|
||||||
7
src/shared/shadcn/ui/popover/popover-portal.svelte
Normal file
7
src/shared/shadcn/ui/popover/popover-portal.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Portal {...restProps} />
|
||||||
17
src/shared/shadcn/ui/popover/popover-trigger.svelte
Normal file
17
src/shared/shadcn/ui/popover/popover-trigger.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
|
||||||
|
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: PopoverPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Trigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="popover-trigger"
|
||||||
|
class={cn('', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
7
src/shared/shadcn/ui/popover/popover.svelte
Normal file
7
src/shared/shadcn/ui/popover/popover.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PopoverPrimitive.Root bind:open {...restProps} />
|
||||||
7
src/shared/shadcn/ui/slider/index.ts
Normal file
7
src/shared/shadcn/ui/slider/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from './slider.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Slider,
|
||||||
|
};
|
||||||
51
src/shared/shadcn/ui/slider/slider.svelte
Normal file
51
src/shared/shadcn/ui/slider/slider.svelte
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
type WithoutChildrenOrChild,
|
||||||
|
cn,
|
||||||
|
} from '$shared/shadcn/utils/shadcn-utils.js';
|
||||||
|
import { Slider as SliderPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value,
|
||||||
|
orientation = 'horizontal',
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SliderPrimitive.RootProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
value={value as never}
|
||||||
|
data-slot="slider"
|
||||||
|
{orientation}
|
||||||
|
class={cn(
|
||||||
|
'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ thumbs })}
|
||||||
|
<span
|
||||||
|
data-orientation={orientation}
|
||||||
|
data-slot="slider-track"
|
||||||
|
class={cn(
|
||||||
|
'bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Range
|
||||||
|
data-slot="slider-range"
|
||||||
|
class={cn(
|
||||||
|
'bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{#each thumbs as thumb (thumb)}
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
index={thumb}
|
||||||
|
class="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/snippet}
|
||||||
|
</SliderPrimitive.Root>
|
||||||
117
src/shared/store/createControlStore.ts
Normal file
117
src/shared/store/createControlStore.ts
Normal file
@@ -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<ControlModel<TValue>>
|
||||||
|
& {
|
||||||
|
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<TValue>,
|
||||||
|
): ControlStoreModel<TValue> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
155
src/shared/ui/ComboControl/ComboControl.svelte
Normal file
155
src/shared/ui/ComboControl/ComboControl.svelte
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$shared/shadcn/ui/button';
|
||||||
|
import * as ButtonGroup from '$shared/shadcn/ui/button-group';
|
||||||
|
import { Input } from '$shared/shadcn/ui/input';
|
||||||
|
import * as Popover from '$shared/shadcn/ui/popover';
|
||||||
|
import { Slider } from '$shared/shadcn/ui/slider';
|
||||||
|
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||||
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||||
|
import type { ChangeEventHandler } from 'svelte/elements';
|
||||||
|
|
||||||
|
interface ComboControlProps {
|
||||||
|
/**
|
||||||
|
* Controlled value
|
||||||
|
*/
|
||||||
|
value: number;
|
||||||
|
/**
|
||||||
|
* Callback function to handle value change
|
||||||
|
*/
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
/**
|
||||||
|
* Callback function to handle increase
|
||||||
|
*/
|
||||||
|
onIncrease: () => void;
|
||||||
|
/**
|
||||||
|
* Callback function to handle decrease
|
||||||
|
*/
|
||||||
|
onDecrease: () => void;
|
||||||
|
/**
|
||||||
|
* Text for increase button aria-label
|
||||||
|
*/
|
||||||
|
increaseLabel?: string;
|
||||||
|
/**
|
||||||
|
* Text for decrease button aria-label
|
||||||
|
*/
|
||||||
|
decreaseLabel?: string;
|
||||||
|
/**
|
||||||
|
* Flag for disabling increase button
|
||||||
|
*/
|
||||||
|
increaseDisabled?: boolean;
|
||||||
|
/**
|
||||||
|
* Flag for disabling decrease button
|
||||||
|
*/
|
||||||
|
decreaseDisabled?: boolean;
|
||||||
|
/**
|
||||||
|
* Text for control button aria-label
|
||||||
|
*/
|
||||||
|
controlLabel?: string;
|
||||||
|
/**
|
||||||
|
* Minimum value for the input
|
||||||
|
*/
|
||||||
|
minValue?: number;
|
||||||
|
/**
|
||||||
|
* Maximum value for the input
|
||||||
|
*/
|
||||||
|
maxValue?: number;
|
||||||
|
/**
|
||||||
|
* Step value for the slider
|
||||||
|
*/
|
||||||
|
step?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onIncrease,
|
||||||
|
onDecrease,
|
||||||
|
increaseLabel,
|
||||||
|
decreaseLabel,
|
||||||
|
increaseDisabled,
|
||||||
|
decreaseDisabled,
|
||||||
|
controlLabel,
|
||||||
|
minValue = 0,
|
||||||
|
maxValue = 100,
|
||||||
|
step = 1,
|
||||||
|
}: ComboControlProps = $props();
|
||||||
|
|
||||||
|
// Local state for the slider to prevent infinite loops
|
||||||
|
let sliderValue = $state(value);
|
||||||
|
|
||||||
|
// Sync sliderValue when external value changes
|
||||||
|
$effect(() => {
|
||||||
|
sliderValue = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
||||||
|
const parsedValue = parseFloat(event.currentTarget.value);
|
||||||
|
if (!isNaN(parsedValue)) {
|
||||||
|
onChange(parsedValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle slider value change.
|
||||||
|
* The Slider component passes the value as a number directly.
|
||||||
|
*/
|
||||||
|
const handleSliderChange = (value: number) => {
|
||||||
|
onChange(value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ButtonGroup.Root>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
aria-label={decreaseLabel}
|
||||||
|
onclick={onDecrease}
|
||||||
|
disabled={decreaseDisabled}
|
||||||
|
>
|
||||||
|
<MinusIcon />
|
||||||
|
</Button>
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
aria-label={controlLabel}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content class="w-auto p-4">
|
||||||
|
<div class="flex flex-col items-center gap-3">
|
||||||
|
<Slider
|
||||||
|
min={minValue}
|
||||||
|
max={maxValue}
|
||||||
|
step={step}
|
||||||
|
value={sliderValue}
|
||||||
|
onValueChange={handleSliderChange}
|
||||||
|
type="single"
|
||||||
|
orientation="vertical"
|
||||||
|
class="h-48"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={String(value)}
|
||||||
|
min={minValue}
|
||||||
|
max={maxValue}
|
||||||
|
onchange={handleInputChange}
|
||||||
|
class="w-16 text-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
aria-label={increaseLabel}
|
||||||
|
onclick={onIncrease}
|
||||||
|
disabled={increaseDisabled}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup.Root>
|
||||||
0
src/widgets/TypographySettings/index.ts
Normal file
0
src/widgets/TypographySettings/index.ts
Normal file
10
src/widgets/TypographySettings/ui/TypographyMenu.svelte
Normal file
10
src/widgets/TypographySettings/ui/TypographyMenu.svelte
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SetupFontMenu from '$features/SetupFont/ui/SetupFontMenu.svelte';
|
||||||
|
import * as Item from '$shared/shadcn/ui/item';
|
||||||
|
import { Separator } from '$shared/shadcn/ui/separator/index';
|
||||||
|
import * as Sidebar from '$shared/shadcn/ui/sidebar/index';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full p-2">
|
||||||
|
<SetupFontMenu />
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user