feat(ui): direction-aware icons (#804)

This commit is contained in:
Mees Frensel
2026-09-14 16:22:28 -04:00
committed by GitHub
parent cd1f66dde5
commit d2efd8cc85
10 changed files with 84 additions and 11 deletions
@@ -5,6 +5,8 @@
import basicExample from './BasicExample.svelte?raw';
import ColorExample from './ColorExample.svelte';
import colorExample from './ColorExample.svelte?raw';
import DirectionalExample from './DirectionalExample.svelte';
import directionalExample from './DirectionalExample.svelte?raw';
import IndicatorExample from './IndicatorExample.svelte';
import indicatorExample from './IndicatorExample.svelte?raw';
import OtherExample from './OtherExample.svelte';
@@ -20,6 +22,7 @@
{ title: 'Sizes', code: sizeExample, component: SizeExample },
{ title: 'Colors', code: colorExample, component: ColorExample },
{ title: 'Indicator', code: indicatorExample, component: IndicatorExample },
{ title: 'Directionality', code: directionalExample, component: DirectionalExample },
{ title: 'Other', code: otherExample, component: OtherExample },
]}
/>
@@ -0,0 +1,47 @@
<script lang="ts">
import { Heading, HStack, Icon, Stack, Text } from '@immich/ui';
import { mdiArrowRight } from '@mdi/js';
const icon = mdiArrowRight;
const size = '2em';
</script>
<HStack gap={8}>
<Stack>
<Heading size="tiny">Auto</Heading>
<div class="flex place-items-center gap-2">
<Icon {icon} {size} directional />
<Text>Normal</Text>
</div>
<div class="flex place-items-center gap-2">
<Icon {icon} {size} directional flipped />
<Text>Flipped</Text>
</div>
</Stack>
<div dir="ltr">
<Stack>
<Heading size="tiny">Left-to-right</Heading>
<div class="flex place-items-center gap-2">
<Icon {icon} {size} directional />
<Text>Normal</Text>
</div>
<div class="flex place-items-center gap-2">
<Icon {icon} {size} directional flipped />
<Text>Flipped</Text>
</div>
</Stack>
</div>
<div dir="rtl">
<Stack>
<Heading size="tiny">Right-to-left</Heading>
<div class="flex place-items-center gap-2">
<Icon {icon} {size} directional />
<Text>Normal</Text>
</div>
<div class="flex place-items-center gap-2">
<Icon {icon} {size} directional flipped />
<Text>Flipped</Text>
</div>
</Stack>
</div>
</HStack>
@@ -25,7 +25,7 @@
{#if typeof separator === 'object' && 'text' in separator}
<span class="mx-1">{separator.text}</span>
{:else}
<Icon icon={separator} size="1rem" />
<Icon icon={separator} size="1rem" directional />
{/if}
{/if}
{#if item.href}
@@ -66,6 +66,7 @@
{:else if onClose}
<IconButton
icon={closeIcon ?? mdiClose}
directional
shape="round"
variant="ghost"
color="secondary"
@@ -10,6 +10,7 @@
viewBox = '0 0 24 24',
class: className = '',
indicator: indicatorColor,
directional = false,
flipped = false,
flopped = false,
spin = false,
@@ -36,13 +37,25 @@
color: styleVariants.textColor,
},
});
const flip = $derived.by(() => {
if (directional && flipped) {
return 'ltr:-scale-x-100 rtl:scale-x-100';
}
if (directional) {
return 'rtl:-scale-x-100 ltr:scale-x-100';
}
if (flipped) {
return '-scale-x-100';
}
});
</script>
<svg
width={size}
height={size}
{viewBox}
class={cleanClass(className, flipped && '-scale-x-100', flopped && 'rotate-180', spin && 'animate-spin')}
class={cleanClass(className, flip, flopped && 'rotate-180', spin && 'animate-spin')}
stroke={strokeColor}
stroke-width={strokeWidth}
{role}
@@ -5,6 +5,7 @@
const {
icon,
directional,
flipped,
flopped,
indicator,
@@ -18,5 +19,5 @@
</script>
<Button icon {color} {...buttonProps} title={buttonTitle} aria-label={ariaLabel}>
<Icon {icon} {flipped} {flopped} {indicator} size="60%" aria-hidden />
<Icon {icon} {directional} {flipped} {flopped} {indicator} size="60%" aria-hidden />
</Button>
@@ -50,11 +50,12 @@
onscroll={onScroll}
>
{#if canScrollLeft || canScrollRight}
<div class="sticky start-0 {zIndex.CarouselImage}">
<div class="sticky inset-s-0 light {zIndex.CarouselImage}">
{#if canScrollLeft}
<div class="light absolute start-4 top-27 -translate-y-1/2 max-md:top-19" transition:fade={{ duration: 200 }}>
<div class="absolute inset-s-4 top-27 -translate-y-1/2 max-md:top-19" transition:fade={{ duration: 200 }}>
<IconButton
icon={mdiChevronLeft}
directional
shape="round"
variant="outline"
color="secondary"
@@ -66,12 +67,10 @@
</div>
{/if}
{#if canScrollRight}
<div
class="light absolute end-4 top-27 {zIndex.CarouselImage} -translate-y-1/2 max-md:top-19"
transition:fade={{ duration: 200 }}
>
<div class="absolute inset-e-4 top-27 -translate-y-1/2 max-md:top-19" transition:fade={{ duration: 200 }}>
<IconButton
icon={mdiChevronRight}
directional
shape="round"
variant="outline"
color="secondary"
@@ -68,6 +68,7 @@
>
<Icon
icon={expanded ? mdiChevronDown : mdiChevronRight}
directional
size="1em"
class="shrink-0 delay-100 duration-100 "
aria-hidden
@@ -18,7 +18,7 @@
href={previous.href}
class="hover:border-primary hover:text-primary flex items-center gap-2 rounded-xl border p-4 transition-colors"
>
<Icon icon={mdiChevronLeft} size="1.5rem" />
<Icon icon={mdiChevronLeft} size="1.5rem" directional />
<div class="min-w-0">
<Text color="muted" size="tiny">{t('navigate_previous', translations)}</Text>
<Text fontWeight="semi-bold" class="truncate">{previous.title}</Text>
@@ -36,7 +36,7 @@
<Text color="muted" size="tiny">{t('navigate_next', translations)}</Text>
<Text fontWeight="semi-bold" class="truncate">{next.title}</Text>
</div>
<Icon icon={mdiChevronRight} size="1.5rem" />
<Icon icon={mdiChevronRight} size="1.5rem" directional />
</a>
{/if}
</nav>
+8
View File
@@ -69,6 +69,10 @@ export type IconProps = {
size?: string;
color?: Color | 'currentColor' | string;
indicator?: Color;
/**
* Flip the icon in RTL layouts
*/
directional?: boolean;
flipped?: boolean;
flopped?: boolean;
spin?: boolean;
@@ -116,6 +120,10 @@ export type ContextMenuButtonProps = ButtonBase & {
export type IconButtonProps = ButtonBase & {
icon: IconLike;
/**
* Flip the icon in RTL layouts
*/
directional?: boolean;
flipped?: boolean;
flopped?: boolean;
indicator?: Color;